├── .nvmrc ├── assets ├── sip-9 │ ├── .gitignore │ ├── example-snap │ │ ├── dist │ │ │ └── bundle.js │ │ ├── package.json │ │ └── snap.manifest.json │ ├── source.js │ └── snap.manifest.schema.json ├── sip-4 │ ├── source.js │ ├── package.json │ └── package.schema.json ├── sip-19 │ └── implementation │ │ ├── .yarnrc.yml │ │ ├── .gitattributes │ │ ├── .editorconfig │ │ ├── package.json │ │ ├── .gitignore │ │ ├── tsconfig.json │ │ └── implementation.ts ├── sip-23 │ └── interface-structure.png ├── sip-26 │ └── components-diagram.png └── sip-2 │ └── permission.schema.json ├── tools └── validate │ ├── test │ ├── sip-1.md │ ├── copyright │ │ └── sip-1.md │ ├── preamble │ │ ├── sip-14.md │ │ ├── sip-13.md │ │ ├── sip-1.md │ │ ├── sip-5.md │ │ ├── sip-7.md │ │ ├── sip-4.md │ │ ├── sip-6.md │ │ ├── sip-8.md │ │ ├── sip-12.md │ │ ├── sip-2.md │ │ ├── sip-3.md │ │ ├── sip-11.md │ │ ├── sip-9.md │ │ └── sip-10.md │ └── filename │ │ └── misnamed.md │ ├── src │ ├── reporters │ │ ├── index.ts │ │ └── console.ts │ ├── rules │ │ ├── filename.ts │ │ ├── index.ts │ │ ├── preamble-exists.ts │ │ ├── preamble-filename.ts │ │ ├── preamble-order.ts │ │ ├── copyright.ts │ │ ├── git-updated.ts │ │ ├── bad-link.ts │ │ └── preamble-data.ts │ ├── plugins │ │ └── parseYaml.ts │ ├── main.ts │ ├── validate.ts │ └── utils.ts │ ├── README.md │ └── package.json ├── _data └── statuses.yml ├── .github ├── CODEOWNERS └── workflows │ └── validate.yml ├── index.html ├── _includes ├── discussion_links.html ├── sipnums.html ├── authorlist.html ├── siptable.html ├── anchor_headings.html └── toc.html ├── .editorconfig ├── .yarnrc.yml ├── 404.html ├── package.json ├── .yarn ├── patches │ └── unified-lint-rule-npm-2.1.1-237c9e9f5f.patch └── plugins │ └── @yarnpkg │ └── plugin-allow-scripts.cjs ├── README.md ├── Gemfile ├── sip-template.md ├── _config.yml ├── SIPS ├── sip-21.md ├── sip-19.md ├── sip-10.md ├── sip-15.md ├── sip-32.md ├── sip-22.md ├── sip-28.md ├── sip-11.md ├── sip-31.md ├── sip-8.md ├── sip-14.md ├── sip-1.md ├── sip-23.md ├── sip-3.md ├── sip-9.md ├── sip-26.md ├── sip-5.md ├── sip-4.md ├── sip-12.md ├── sip-30.md ├── sip-16.md ├── sip-6.md └── sip-20.md ├── Gemfile.lock ├── .gitignore ├── _layouts └── sip.html └── LICENSE /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* 2 | -------------------------------------------------------------------------------- /assets/sip-9/.gitignore: -------------------------------------------------------------------------------- 1 | !example-snap/dist/ 2 | -------------------------------------------------------------------------------- /tools/validate/test/sip-1.md: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /assets/sip-9/example-snap/dist/bundle.js: -------------------------------------------------------------------------------- 1 | console.log("Hello, World!"); 2 | -------------------------------------------------------------------------------- /assets/sip-4/source.js: -------------------------------------------------------------------------------- 1 | module.exports.onRpcRequest = async ({ request }) => 42; 2 | -------------------------------------------------------------------------------- /assets/sip-9/source.js: -------------------------------------------------------------------------------- 1 | module.exports.onRpcRequest = async ({ request }) => 42; 2 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/.yarnrc.yml: -------------------------------------------------------------------------------- 1 | yarnPath: .yarn/releases/yarn-4.0.2.cjs 2 | -------------------------------------------------------------------------------- /tools/validate/src/reporters/index.ts: -------------------------------------------------------------------------------- 1 | export { default as console } from "./console.js"; 2 | -------------------------------------------------------------------------------- /_data/statuses.yml: -------------------------------------------------------------------------------- 1 | - Draft 2 | - Review 3 | - Implementation 4 | - Final 5 | - Withdrawn 6 | - Living 7 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Current SIP editors 2 | * @metamask/snaps-devs 3 | SIPS/ @Montoya @metamask/snaps-devs 4 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Home 4 | --- 5 | 6 | {% include siptable.html sips=site.pages %} 7 | -------------------------------------------------------------------------------- /assets/sip-23/interface-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaMask/SIPs/HEAD/assets/sip-23/interface-structure.png -------------------------------------------------------------------------------- /assets/sip-26/components-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MetaMask/SIPs/HEAD/assets/sip-26/components-diagram.png -------------------------------------------------------------------------------- /tools/validate/README.md: -------------------------------------------------------------------------------- 1 | # validate 2 | 3 | Use this tool to validate SIPs 4 | 5 | ```bash 6 | yarn install 7 | yarn build 8 | yarn lint 9 | ``` 10 | -------------------------------------------------------------------------------- /tools/validate/test/copyright/sip-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1 3 | title: "Foo" 4 | status: Draft 5 | author: Foo (@bar) 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-14.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 7 | 8 | ## Copyright 9 | 10 | Copyright and related rights waived via [CC0](../LICENSE). 11 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/** linguist-vendored 2 | /.yarn/releases/* binary 3 | /.yarn/plugins/**/* binary 4 | /.pnp.* binary linguist-generated 5 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-13.md: -------------------------------------------------------------------------------- 1 | --- 2 | 6 3 | --- 4 | 5 | 8 | 9 | ## Copyright 10 | 11 | Copyright and related rights waived via [CC0](../LICENSE). 12 | -------------------------------------------------------------------------------- /_includes/discussion_links.html: -------------------------------------------------------------------------------- 1 | {% assign links=include.links|split:"," %} 2 | {% for link in links %} 3 | {{ link | strip | xml_escape }} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /_includes/sipnums.html: -------------------------------------------------------------------------------- 1 | {% assign sips=include.sips|split:"," %} 2 | {% for num in sips %} 3 | {{num|strip}}{% if forloop.last == false %}, {% endif %} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [*.{js,json,yml}] 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | end_of_line = lf 8 | 9 | [*.{js,jsom,yml,ts}] 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /tools/validate/test/filename/misnamed.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-5.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: "asd" 3 | title: 123 4 | status: 123 5 | author: 123 6 | created: 123 7 | updated: 123 8 | --- 9 | 10 | 13 | 14 | ## Copyright 15 | 16 | Copyright and related rights waived via [CC0](../LICENSE). 17 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-7.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1.5 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-4.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: -1 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-6.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 6 3 | title: Foo 4 | status: Foo 5 | author: Olaf Tomalka (@ritave) 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-8.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | created: asd 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-12.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 11 3 | title: Foo 4 | status: Living 5 | author: Olaf Tomalka (@ritave) 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 2 3 | title: Foo 4 | status: Draft 5 | author: Ola32492i50943u dsnflkdsn gl.,,. 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 123 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | created: 2022-10-02 7 | --- 8 | 9 | 12 | 13 | ## Copyright 14 | 15 | Copyright and related rights waived via [CC0](../LICENSE). 16 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-11.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 11 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | updated: 2022-10-01 7 | created: 2022-10-02 8 | --- 9 | 10 | 13 | 14 | ## Copyright 15 | 16 | Copyright and related rights waived via [CC0](../LICENSE). 17 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-9.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | discussions-to: Bar 7 | created: 2022-10-02 8 | --- 9 | 10 | 13 | 14 | ## Copyright 15 | 16 | Copyright and related rights waived via [CC0](../LICENSE). 17 | -------------------------------------------------------------------------------- /assets/sip-9/example-snap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-snap", 3 | "version": "0.2.2", 4 | "scripts": { 5 | "build": "tsc", 6 | "clean": "rimraf dist/", 7 | "build:clean": "yarn clean && yarn build" 8 | }, 9 | "devDependencies": { 10 | "rimraf": "^3.0.2", 11 | "typescript": "^4.7.3" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tools/validate/test/preamble/sip-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 10 3 | title: Foo 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | discussions-to: http://google.com 7 | created: 2022-10-02 8 | --- 9 | 10 | 13 | 14 | ## Copyright 15 | 16 | Copyright and related rights waived via [CC0](../LICENSE). 17 | -------------------------------------------------------------------------------- /tools/validate/src/rules/filename.ts: -------------------------------------------------------------------------------- 1 | import { lintRule } from "unified-lint-rule"; 2 | 3 | const SIP_FILENAME = /^sip-[1-9][0-9]*\.md$/; 4 | 5 | const rule = lintRule("sip:filename", (_, file) => { 6 | if (file.basename !== undefined && !SIP_FILENAME.test(file.basename)) { 7 | file.message(`File doesn't have a filename in "sip-N.md" format`); 8 | } 9 | }); 10 | 11 | export default rule; 12 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "implementation", 3 | "packageManager": "yarn@4.0.2", 4 | "devDependencies": { 5 | "@types/node": "^20.10.0", 6 | "typescript": "^5.3.2" 7 | }, 8 | "dependencies": { 9 | "@metamask/utils": "^8.2.1", 10 | "@noble/hashes": "^1.3.2", 11 | "@scure/base": "^1.1.3", 12 | "fast-json-stable-stringify": "^2.1.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/.gitignore: -------------------------------------------------------------------------------- 1 | .yarn/* 2 | !.yarn/patches 3 | !.yarn/plugins 4 | !.yarn/releases 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | # Swap the comments on the following lines if you wish to use zero-installs 9 | # In that case, don't forget to run `yarn config set enableGlobalCache false`! 10 | # Documentation here: https://yarnpkg.com/features/caching#zero-installs 11 | 12 | #!.yarn/cache 13 | .pnp.* 14 | node_modules/ 15 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | enableScripts: false 6 | 7 | enableTelemetry: false 8 | 9 | logFilters: 10 | - code: YN0004 11 | level: discard 12 | 13 | nodeLinker: node-modules 14 | 15 | plugins: 16 | - path: .yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs 17 | spec: "https://raw.githubusercontent.com/LavaMoat/LavaMoat/main/packages/yarn-plugin-allow-scripts/bundles/@yarnpkg/plugin-allow-scripts.js" 18 | -------------------------------------------------------------------------------- /tools/validate/src/rules/index.ts: -------------------------------------------------------------------------------- 1 | export { default as badLink } from "./bad-link.js"; 2 | export { default as copyright } from "./copyright.js"; 3 | export { default as filename } from "./filename.js"; 4 | export { default as gitUpdated } from "./git-updated.js"; 5 | export { default as preambleData } from "./preamble-data.js"; 6 | export { default as preambleExists } from "./preamble-exists.js"; 7 | export { default as preambleFilename } from "./preamble-filename.js"; 8 | export { default as preambleOrder } from "./preamble-order.js"; 9 | -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /tools/validate/src/rules/preamble-exists.ts: -------------------------------------------------------------------------------- 1 | import { Root } from "mdast"; 2 | import { lintRule } from "unified-lint-rule"; 3 | import { selectAll } from "unist-util-select"; 4 | 5 | const rule = lintRule("sip:preamble-exists", (tree, file) => { 6 | const nodes = selectAll("parsedYaml", tree); 7 | 8 | if (!nodes.length) { 9 | file.message("No preamble found", tree); 10 | } else if (nodes.length > 1) { 11 | nodes.slice(1).forEach((node) => file.message("Too many preambles", node)); 12 | } 13 | }); 14 | 15 | export default rule; 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "tools/validate" 6 | ], 7 | "resolutions": { 8 | "unified-lint-rule@2.1.1": "patch:unified-lint-rule@npm:2.1.1#.yarn/patches/unified-lint-rule-npm-2.1.1-237c9e9f5f.patch" 9 | }, 10 | "devDependencies": { 11 | "@lavamoat/allow-scripts": "^3.3.1", 12 | "@lavamoat/preinstall-always-fail": "^2.1.0" 13 | }, 14 | "packageManager": "yarn@4.6.0", 15 | "lavamoat": { 16 | "allowScripts": { 17 | "@lavamoat/preinstall-always-fail": false 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /_includes/authorlist.html: -------------------------------------------------------------------------------- 1 | {%- assign authors=include.authors|split:"," -%} 2 | {%- for author in authors -%} 3 | {%- if author contains "<" -%} 4 | {%- assign authorparts=author|split:"<" -%} 5 | "}}">{{authorparts[0]|strip}} 6 | {%- elsif author contains "(@" -%} 7 | {%- assign authorparts=author|split:"(@" -%} 8 | {{authorparts[0]|strip}} 9 | {%- else -%} 10 | {{author}} 11 | {%- endif -%} 12 | {% if forloop.last == false %}, {% endif %} 13 | {%- endfor -%} 14 | -------------------------------------------------------------------------------- /.yarn/patches/unified-lint-rule-npm-2.1.1-237c9e9f5f.patch: -------------------------------------------------------------------------------- 1 | diff --git a/lib/index.js b/lib/index.js 2 | index ac57a60dc789d6f828bccd29dfd8f250a992b74a..0ca39fb04030e3e7064c73f9628abc3ccc324ac4 100644 3 | --- a/lib/index.js 4 | +++ b/lib/index.js 5 | @@ -63,7 +63,7 @@ export function lintRule(meta, rule) { 6 | } 7 | 8 | while (++index < messages.length) { 9 | - Object.assign(messages[index], {ruleId, source, fatal, url}) 10 | + Object.assign(messages[index], {ruleId, source, fatal: messages[index].fatal === null ? null : fatal, url}) 11 | } 12 | 13 | next() 14 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "module": "commonjs" /* Specify what module code is generated. */, 5 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 6 | "strict": true /* Enable all strict type-checking options. */, 7 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /assets/sip-4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./package.schema.json", 3 | "name": "my-snap", 4 | "version": "1.0.0", 5 | "description": "An example snap description", 6 | "main": "dist/index.js", 7 | "files": [ 8 | "dist/", 9 | "images/" 10 | ], 11 | "engines": { 12 | "node": ">=16.0.0", 13 | "snaps": "^1.0.0" 14 | }, 15 | "snap": { 16 | "proposedName": "My Snap", 17 | "checksum": { 18 | "algorithm": "sha-256", 19 | "hash": "qNYd+o17O8kEwz40rxgBftRiyss2sYS7ir++rlLBQXo=" 20 | }, 21 | "permissions": { 22 | "snap_confirm": {} 23 | }, 24 | "icon": "images/icon.png" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /assets/sip-9/example-snap/snap.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../snap.manifest.schema.json", 3 | "version": "0.2.2", 4 | "proposedName": "@metamask/example-snap", 5 | "description": "An example snap.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/MetaMask/example-snap.git" 9 | }, 10 | "source": { 11 | "shasum": "w3FltkDjKQZiPwM+AThnmypt0OFF7hj4ycg/kxxv+nU=", 12 | "location": { 13 | "npm": { 14 | "filePath": "dist/bundle.js", 15 | "iconPath": "images/icon.svg", 16 | "packageName": "@metamask/example-snap", 17 | "registry": "https://registry.npmjs.org/" 18 | } 19 | } 20 | }, 21 | "initialPermissions": { 22 | "snap_confirm": {} 23 | }, 24 | "manifestVersion": "0.1" 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snaps Improvement Proposals 2 | 3 | [Website](https://metamask.github.io/SIPs) 4 | 5 | ## Purpose 6 | 7 | Shape MetaMask Snap's end user APIs before release with open discussion and involvement from the community. 8 | 9 | Snaps in current state are being researched by MetaMask, and feedback from the community directs the features that are developed. This repository serves as an open list of design documents pertaining to new developments in the snap ecosystem, written both by MetaMask team as well as including high quality proposals from the community. 10 | 11 | **We invite everyone to participate in discussions.** 12 | 13 | ## Contributing 14 | 15 | Please read [SIP-1](./SIPS/sip-1.md) on guidelines and approval process. 16 | 17 | The [SIP template](./sip-template.md) is a good starting point for your new SIP. 18 | -------------------------------------------------------------------------------- /_includes/siptable.html: -------------------------------------------------------------------------------- 1 | 10 | {% for status in site.data.statuses %} 11 | {% assign sips = include.sips|where:"status",status|sort:"sip" %} 12 | {% assign count = sips|size %} 13 | {% if count > 0 %} 14 |

{{status}}

15 | 16 | 17 | 18 | 19 | {% for page in sips %} 20 | 21 | 22 | 23 | 24 | 25 | {% endfor %} 26 |
NumberTitleAuthor
{{page.sip|xml_escape}}{{page.title|xml_escape}}{% include authorlist.html authors=page.author %}
27 | {% endif %} 28 | {% endfor %} 29 | -------------------------------------------------------------------------------- /assets/sip-19/implementation/implementation.ts: -------------------------------------------------------------------------------- 1 | import stableStringify from "fast-json-stable-stringify"; 2 | import { sha256 } from "@noble/hashes/sha256"; 3 | import { concatBytes } from "@metamask/utils"; 4 | import { base64 } from "@scure/base"; 5 | import assert from "assert"; 6 | 7 | export type VFile = { path: string; contents: string }; 8 | 9 | function stableManifest({ path, contents }: VFile): VFile { 10 | const structure = JSON.parse(contents); 11 | delete structure.result.source.shasum; 12 | return { path, contents: stableStringify(structure) }; 13 | } 14 | 15 | export function checksumFiles(manifest: VFile, auxiliary: VFile[]): string { 16 | return base64.encode( 17 | sha256( 18 | concatBytes( 19 | [stableManifest(manifest), ...auxiliary] 20 | .sort((a, b) => { 21 | assert(a.path !== b.path, "Duplicate paths detected"); 22 | return a.path < b.path ? -1 : 1; 23 | }) 24 | .map(({ contents }) => sha256(contents)) 25 | ) 26 | ) 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate SIP 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - "SIPS/**" 7 | 8 | jobs: 9 | validate: 10 | name: Validate SIPs 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout and setup environment 14 | uses: MetaMask/action-checkout-and-setup@v1 15 | with: 16 | is-high-risk-environment: true 17 | - name: Build 18 | run: yarn workspace validate build 19 | - name: Validate all SIPs 20 | run: yarn workspace validate lint 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | GITHUB_PULL_REQUEST: ${{ github.event.pull_request.number }} 24 | GITHUB_SHA_PULL_REQUEST: ${{ github.event.pull_request.head.sha }} 25 | FORCE_COLOR: 3 26 | - name: Require clean working directory 27 | shell: bash 28 | run: | 29 | if ! git diff --exit-code; then 30 | echo "Working tree dirty at end of job" 31 | exit 1 32 | fi 33 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-allow-scripts.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-allow-scripts", 5 | factory: function (require) { 6 | var plugin=(()=>{var l=Object.defineProperty;var s=Object.getOwnPropertyDescriptor;var a=Object.getOwnPropertyNames;var c=Object.prototype.hasOwnProperty;var p=(t=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(t,{get:(o,e)=>(typeof require<"u"?require:o)[e]}):t)(function(t){if(typeof require<"u")return require.apply(this,arguments);throw new Error('Dynamic require of "'+t+'" is not supported')});var u=(t,o)=>{for(var e in o)l(t,e,{get:o[e],enumerable:!0})},f=(t,o,e,r)=>{if(o&&typeof o=="object"||typeof o=="function")for(let i of a(o))!c.call(t,i)&&i!==e&&l(t,i,{get:()=>o[i],enumerable:!(r=s(o,i))||r.enumerable});return t};var m=t=>f(l({},"__esModule",{value:!0}),t);var g={};u(g,{default:()=>d});var n=p("@yarnpkg/shell"),x={hooks:{afterAllInstalled:async()=>{let t=await(0,n.execute)("yarn run allow-scripts");t!==0&&process.exit(t)}}},d=x;return m(g);})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /tools/validate/src/reporters/console.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import { VFile } from "vfile"; 3 | 4 | const reporter = (files: VFile[]) => { 5 | let output = ""; 6 | for (const file of files) { 7 | for (const message of file.messages) { 8 | output += 9 | chalk.bold( 10 | (message.fatal === null ? chalk.blue("info") : chalk.red("error")) + 11 | `: ${message.reason} ` + 12 | chalk.gray(`(${message.source}:${message.ruleId})`) 13 | ) + "\n"; 14 | output += ` ➜ ${file.path}`; 15 | if (message.line !== null) { 16 | output += `:${message.line}`; 17 | if (message.column !== null) { 18 | output += `:${message.column}`; 19 | } 20 | } 21 | let at: any = message; 22 | do { 23 | if (at.stack) { 24 | output += chalk.gray("\n" + at.stack + "\n---"); 25 | } 26 | at = at.cause; 27 | } while (at && at.cause !== undefined); 28 | output += "\n\n"; 29 | } 30 | } 31 | return output.slice(undefined, -2); // remove last '\n\n' 32 | }; 33 | 34 | export default reporter; 35 | -------------------------------------------------------------------------------- /tools/validate/src/plugins/parseYaml.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import Debug from "debug"; 3 | import { Literal, Root } from "mdast"; 4 | import { CONTINUE, visit } from "unist-util-visit"; 5 | import { VFile } from "vfile"; 6 | import YAML from "yaml"; 7 | 8 | const debug = Debug("validate:plugin:parse-yaml"); 9 | 10 | declare module "mdast" { 11 | interface FrontmatterContentMap { 12 | parsedYaml: ParsedYaml; 13 | } 14 | } 15 | 16 | export interface ParsedYaml extends Literal { 17 | type: "parsedYaml"; 18 | data: { parsed: unknown }; 19 | } 20 | 21 | export default function parseYaml() { 22 | return (tree: Root, file: VFile) => { 23 | visit(tree, "yaml", (node) => { 24 | try { 25 | const parsed = YAML.parse(node.value, { prettyErrors: true }); 26 | assert(node.data === undefined); 27 | Object.assign(node, { data: { parsed }, type: "parsedYaml" }); 28 | } catch (error) { 29 | debug(error); 30 | assert(error instanceof Error); 31 | file.message("Front-matter is not valid YAML", node, "sip:yaml"); 32 | } 33 | 34 | return CONTINUE; 35 | }); 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /tools/validate/src/rules/preamble-filename.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import Debug from "debug"; 3 | import { Root } from "mdast"; 4 | import { is, pick } from "superstruct"; 5 | import { lintRule } from "unified-lint-rule"; 6 | import { select } from "unist-util-select"; 7 | import { ParsedYaml } from "../plugins/parseYaml.js"; 8 | import { Preamble } from "./preamble-data.js"; 9 | 10 | const debug = Debug("validate:rule:preamble-filename"); 11 | 12 | const EXTRACT_PATH_SIP = /sip-(?[1-9][0-9]*)\.md$/; 13 | 14 | const Sip = pick(Preamble, ["sip"]); 15 | 16 | const rule = lintRule("sip:preamble-filename", (tree, file) => { 17 | const node = select("parsedYaml", tree) as ParsedYaml | null; 18 | if (!node) { 19 | return; 20 | } 21 | if (!is(node.data.parsed, Sip)) { 22 | debug("Malformed preamble, silently dropping"); 23 | return; 24 | } 25 | 26 | const match = file.basename?.match(EXTRACT_PATH_SIP); 27 | let extractedSip: number | undefined = undefined; 28 | if (match !== null && match !== undefined) { 29 | assert(match.groups?.sipNumber !== undefined); 30 | extractedSip = Number(match.groups.sipNumber); 31 | } 32 | 33 | if (extractedSip !== node.data.parsed.sip) { 34 | file.message( 35 | 'Front-matter property "sip" doesn\'t match the filename number', 36 | node 37 | ); 38 | } 39 | }); 40 | 41 | export default rule; 42 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 4.2.2" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | # gem "github-pages", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", "~> 1.2" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem 32 | # do not have a Java counterpart. 33 | gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] 34 | -------------------------------------------------------------------------------- /tools/validate/src/rules/preamble-order.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { Root } from "mdast"; 3 | import { lintRule } from "unified-lint-rule"; 4 | import { select } from "unist-util-select"; 5 | import { ParsedYaml } from "../plugins/parseYaml.js"; 6 | import { Preamble } from "./preamble-data.js"; 7 | 8 | const HEADER_ORDER = [ 9 | "sip", 10 | "title", 11 | "status", 12 | "discussions-to", 13 | "author", 14 | "created", 15 | "updated", 16 | ]; 17 | 18 | assert( 19 | [...Object.keys(Preamble.schema)].every((header) => 20 | HEADER_ORDER.includes(header) 21 | ), 22 | "Some headers don't have specified order" 23 | ); 24 | 25 | /** 26 | * Used for extracting the headers from preamble to check order. 27 | */ 28 | const HEADERS_REGEX = /^([a-z]+):.+$/m; 29 | 30 | const rule = lintRule("sip:preamble-order", (tree, file) => { 31 | const node = select("parsedYaml", tree) as ParsedYaml | null; 32 | if (!node) { 33 | return; 34 | } 35 | const orderedHeaders = HEADERS_REGEX.exec(node.value)?.slice(1) ?? []; 36 | let orderIndex = 0; 37 | for (const header of orderedHeaders) { 38 | if ( 39 | HEADER_ORDER[orderIndex] in orderedHeaders && 40 | header !== HEADER_ORDER[orderIndex] 41 | ) { 42 | file.message( 43 | `Front-matter property \"${header}\" is not in proper order`, 44 | node 45 | ); 46 | break; 47 | } 48 | orderIndex++; 49 | } 50 | }); 51 | export default rule; 52 | -------------------------------------------------------------------------------- /tools/validate/src/rules/copyright.ts: -------------------------------------------------------------------------------- 1 | import { Root } from "mdast"; 2 | import { lintRule } from "unified-lint-rule"; 3 | import { location } from "vfile-location"; 4 | import { deepContains } from "../utils.js"; 5 | 6 | const COPYRIGHT_POSTAMBLE = [ 7 | { 8 | type: "heading", 9 | depth: 2, 10 | children: [ 11 | { 12 | type: "text", 13 | value: "Copyright", 14 | }, 15 | ], 16 | }, 17 | { 18 | type: "paragraph", 19 | children: [ 20 | { type: "text", value: "Copyright and related rights waived via " }, 21 | { 22 | type: "link", 23 | url: "../LICENSE", 24 | children: [{ type: "text", value: "CC0" }], 25 | }, 26 | ], 27 | }, 28 | ]; 29 | 30 | const rule = lintRule("sip:copyright", (tree, file) => { 31 | // There might be definitions and footnotes after a copyright, they are allowed as they're markup and not content 32 | let definitionsCount = 0; 33 | while ( 34 | ["definition", "footnoteDefinition"].includes( 35 | tree.children.at(-1 - definitionsCount)?.type ?? "" 36 | ) 37 | ) { 38 | definitionsCount++; 39 | } 40 | 41 | if ( 42 | tree.children.length - definitionsCount < 2 || 43 | !deepContains( 44 | tree.children.slice(-2 - definitionsCount), 45 | COPYRIGHT_POSTAMBLE 46 | ) 47 | ) { 48 | const place = location(file); 49 | file.message( 50 | "No copyright postamble found or is malformed", 51 | place.toPoint(Math.max(file.value.length - 1, 0)) 52 | ); 53 | } 54 | }); 55 | 56 | export default rule; 57 | -------------------------------------------------------------------------------- /sip-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: (To be assigned) 3 | title: (The tile of this SIP) 4 | status: Draft 5 | discussions-to (*optional): (Http/Https URL) 6 | author: (Comma-separated list of authors in following format `Name Surname (Github username)`) 7 | created: (Date created on in ISO 8601 format. `yyyy-mm-dd`) 8 | updated (*optional): (Date last updated on in https://en.wikipedia.org/wiki/ISO_8601 format. `yyyy-mm-dd`. This should be only used on SIPs with `Living` status) 9 | --- 10 | 11 | ## Abstract 12 | 13 | A few terse sentences that are a technical summary of the proposal. Someone should be able to read this paragraph and understand the gist of this SIP. 14 | 15 | ## Motivation 16 | 17 | The "why" 18 | 19 | ## Specification 20 | 21 | > Indented sections like this are considered non-normative. 22 | 23 | Formal specification of the proposed changes in the SIP. The specification must be complete enough that an implementation can be created from it. 24 | 25 | ### Language 26 | 27 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 28 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 29 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 30 | 31 | ## Backwards compatibility 32 | 33 | Any SIPs that break backwards compatibility MUST include a section describing those incompatibilities and their severity. The SIP SHOULD describe how the author plans on proposes to deal with such these incompatibilities. 34 | 35 | ## Copyright 36 | 37 | Copyright and related rights waived via [CC0](../LICENSE). 38 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | # 11 | # If you need help with YAML syntax, here are some quick references for you: 12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml 13 | # https://learnxinyminutes.com/docs/yaml/ 14 | # 15 | # Site settings 16 | # These are used to personalize your new site. If you look in the HTML files, 17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 18 | # You can create any custom variable you would like, and they will be accessible 19 | # in the templates via {{ site.myvariable }}. 20 | 21 | title: Snaps Improvements Proposals 22 | description: >- # this means to ignore newlines until "baseurl:" 23 | Snaps Improvement Proposals describe specs implemented in the MetaMask Snaps Platform 24 | url: "" # the base hostname & protocol for your site, e.g. http://example.com 25 | github_username: MetaMask 26 | repository: MetaMask/SIPs 27 | 28 | header_pages: 29 | - index.html 30 | 31 | # Build settings 32 | theme: minima 33 | highlighter: rouge 34 | markdown: kramdown 35 | plugins: 36 | - jekyll-feed 37 | 38 | permalink: /:slug 39 | 40 | defaults: 41 | - scope: 42 | path: "SIPS" 43 | values: 44 | layout: "sip" 45 | 46 | exclude: 47 | - sip-template.md 48 | - tools/ 49 | -------------------------------------------------------------------------------- /tools/validate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validate", 3 | "type": "module", 4 | "bin": "dist/main.js", 5 | "private": true, 6 | "scripts": { 7 | "build": "tsc", 8 | "lint": "validate '../../SIPS/**'", 9 | "clean": "rimraf dist/", 10 | "build:clean": "yarn clean && yarn build" 11 | }, 12 | "files": [ 13 | "dist/" 14 | ], 15 | "engines": { 16 | "node": ">=18.0.0" 17 | }, 18 | "devDependencies": { 19 | "@types/glob": "^7.2.0", 20 | "@types/is-valid-path": "^0.1.0", 21 | "@types/node": "^17.0.41", 22 | "@types/unist": "^2.0.6", 23 | "@types/yargs": "^17.0.10", 24 | "mdast": "^3.0.0", 25 | "rimraf": "^3.0.2", 26 | "typescript": "^4.7.3" 27 | }, 28 | "dependencies": { 29 | "async-mutex": "^0.5.0", 30 | "chalk": "^5.0.1", 31 | "debug": "^4.3.4", 32 | "glob": "^8.0.3", 33 | "is-valid-path": "^0.1.1", 34 | "remark-frontmatter": "^4.0.1", 35 | "remark-gfm": "^3.0.1", 36 | "remark-lint-hard-break-spaces": "^3.1.1", 37 | "remark-lint-no-duplicate-definitions": "^3.1.1", 38 | "remark-lint-no-heading-content-indent": "^4.1.1", 39 | "remark-lint-no-inline-padding": "^4.1.1", 40 | "remark-lint-no-shortcut-reference-image": "^3.1.1", 41 | "remark-lint-no-shortcut-reference-link": "^3.1.1", 42 | "remark-lint-no-undefined-references": "^4.2.0", 43 | "remark-lint-no-unused-definitions": "^3.1.1", 44 | "remark-parse": "^10.0.1", 45 | "simple-git": "^3.16.0", 46 | "superstruct": "^0.16.5", 47 | "to-vfile": "^7.2.3", 48 | "unified": "^10.1.2", 49 | "unified-lint-rule": "^2.1.1", 50 | "unist-util-select": "^4.0.1", 51 | "unist-util-visit": "^4.1.1", 52 | "vfile": "^5.3.5", 53 | "vfile-location": "^4.0.1", 54 | "yaml": "^2.2.2", 55 | "yargs": "^17.5.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tools/validate/src/rules/git-updated.ts: -------------------------------------------------------------------------------- 1 | import Debug from "debug"; 2 | import { Root } from "mdast"; 3 | import simpleGit from "simple-git"; 4 | import { is, pick } from "superstruct"; 5 | import { lintRule } from "unified-lint-rule"; 6 | import { select } from "unist-util-select"; 7 | import { ParsedYaml } from "../plugins/parseYaml.js"; 8 | import { Preamble } from "./preamble-data.js"; 9 | const debug = Debug("validate:rule:git-updated"); 10 | 11 | const Updated = pick(Preamble, ["updated"]); 12 | 13 | const rule = lintRule("sip:git-updated", async (tree, file) => { 14 | const node = select("parsedYaml", tree) as ParsedYaml | null; 15 | if (!node) { 16 | return; 17 | } 18 | if (!is(node.data.parsed, Updated)) { 19 | debug("Malformed preamble, silently dropping"); 20 | return; 21 | } 22 | const updated = node.data.parsed.updated; 23 | if (updated === undefined) { 24 | return; 25 | } 26 | 27 | const git = simpleGit(file.cwd); 28 | 29 | if (!(await git.checkIsRepo())) { 30 | file.info("The file is not in a git repository"); 31 | return; 32 | } 33 | if ((await git.revparse("--is-shallow-repository")) === "true") { 34 | file.info( 35 | "File is located in shallow repository. Can't get correct last edited time" 36 | ); 37 | return; 38 | } 39 | const result = await git.log({ 40 | format: { date: "%as" }, 41 | file: file.path, 42 | maxCount: 1, 43 | }); 44 | const gitDate = result.latest; 45 | if (gitDate === null) { 46 | file.info("File is not committed to git repository"); 47 | return; 48 | } 49 | 50 | if (new Date(updated) < new Date(gitDate.date)) { 51 | file.message( 52 | 'The "updated" preamble property is older than last git updated time', 53 | node 54 | ); 55 | } 56 | }); 57 | 58 | export default rule; 59 | -------------------------------------------------------------------------------- /tools/validate/src/main.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import assert from "assert"; 3 | import Debug from "debug"; 4 | import glob from "glob"; 5 | import process from "process"; 6 | import { read } from "to-vfile"; 7 | import { promisify } from "util"; 8 | import { hideBin } from "yargs/helpers"; 9 | import yargs from "yargs/yargs"; 10 | import * as reporters from "./reporters/index.js"; 11 | import { validate } from "./validate.js"; 12 | 13 | const debug = Debug("validate:main"); 14 | 15 | const globAsync = promisify(glob); 16 | 17 | const argv = await yargs(hideBin(process.argv)) 18 | .help() 19 | .version() 20 | .command("$0 ", "", (argv) => 21 | argv 22 | .positional("glob", { 23 | describe: "Filepath of SIP or glob pattern of files to validate", 24 | type: "string", 25 | }) 26 | .demandOption("glob") 27 | .option("reporter", { 28 | choices: Object.keys(reporters), 29 | array: true, 30 | describe: "One or more reporters used to print out results", 31 | default: ["console"], 32 | }) 33 | .coerce("glob", (arg) => globAsync(arg, { nodir: true })) 34 | ).argv; 35 | 36 | assert(argv.glob !== undefined); 37 | 38 | const filePaths: string[] = argv.glob as unknown as string[]; // Typescript types fail here 39 | if (!filePaths.length) { 40 | debug("No files analyzed"); 41 | } 42 | const files = await Promise.all(filePaths.map((path) => read(path))); 43 | const results = await validate(files); 44 | const isFatal = results.some((file) => 45 | file.messages.some((message) => message.fatal !== null) 46 | ); 47 | 48 | let output = ""; 49 | for (const reporter of argv.reporter) { 50 | output += await (reporters as any)[reporter](results); 51 | } 52 | 53 | if (output.length > 0) { 54 | process.stdout.write(`${output}\n`); 55 | } 56 | 57 | if (isFatal) { 58 | process.exit(1); 59 | } 60 | -------------------------------------------------------------------------------- /tools/validate/src/validate.ts: -------------------------------------------------------------------------------- 1 | import remarkFrontMatter from "remark-frontmatter"; 2 | import remarkGfm from "remark-gfm"; 3 | import remarkLintHardBreakSpaces from "remark-lint-hard-break-spaces"; 4 | import remarkLintNoDuplicateDefinitions from "remark-lint-no-duplicate-definitions"; 5 | import remarkLintNoHeadingContentIndent from "remark-lint-no-heading-content-indent"; 6 | import remarkLintNoInlinePadding from "remark-lint-no-inline-padding"; 7 | import remarkLintNoShortcutReferenceImage from "remark-lint-no-shortcut-reference-image"; 8 | import remarkLintNoShortcutReferenceLink from "remark-lint-no-shortcut-reference-link"; 9 | import remarkLintNoUndefinedReferences from "remark-lint-no-undefined-references"; 10 | import remarkLintNoUnusedDefinitions from "remark-lint-no-unused-definitions"; 11 | import remarkParse from "remark-parse"; 12 | import { Processor, unified } from "unified"; 13 | import { VFile, VFileCompatible } from "vfile"; 14 | import parseYaml from "./plugins/parseYaml.js"; 15 | import * as sipRules from "./rules/index.js"; 16 | 17 | const parser = unified() 18 | .use(remarkParse) 19 | .use(voidCompiler) 20 | .use(remarkGfm) 21 | .use(remarkFrontMatter, "yaml") 22 | .use(parseYaml) 23 | .use({ 24 | plugins: [ 25 | remarkLintHardBreakSpaces, 26 | remarkLintNoDuplicateDefinitions, 27 | remarkLintNoHeadingContentIndent, 28 | remarkLintNoInlinePadding, 29 | remarkLintNoShortcutReferenceImage, 30 | remarkLintNoShortcutReferenceLink, 31 | [remarkLintNoUndefinedReferences, { allow: [{ source: '^!' }] }], 32 | remarkLintNoUnusedDefinitions, 33 | ], 34 | }) // like remark-preset-lint-recommended but only for critical mistakes 35 | .use(Object.values(sipRules)) 36 | .freeze(); 37 | 38 | export function validate(files: VFileCompatible[]): Promise { 39 | return Promise.all(files.map((file) => parser.process(file))); 40 | } 41 | 42 | function voidCompiler(this: Processor) { 43 | const compiler = () => undefined; 44 | Object.assign(this, { Compiler: compiler }); 45 | } 46 | -------------------------------------------------------------------------------- /SIPS/sip-21.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 21 3 | title: Snap-defined timeouts 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/128 6 | author: Frederik Bolding (@frederikbolding) 7 | created: 2024-01-12 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP describes a way for Snaps to customize the request timeouts that constrain the Snap lifecycle. Effectively allowing the Snap to opt-in to slightly longer or shorter timeouts where needed. 13 | 14 | ## Motivation 15 | 16 | Snaps that do CPU intensive computation are currently limited by the 1 minute timeout, causing certain use-cases to be unsupportable by Snaps (e.g., computing ZK proofs). By letting Snaps customize the request timeout (within reason) for each type of handler that they expose, the Snap developer gets more control over the user experience while the Snap platform lifecycle requirements can remain strict. 17 | 18 | ## Specification 19 | 20 | > Formal specifications are written in TypeScript. 21 | 22 | ### Language 23 | 24 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 25 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 26 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 27 | 28 | ### Snap Manifest 29 | 30 | This SIP specifies an addendum that MUST be applied to existing and future Snap handler permissions (`endowment:rpc`, `endowment:transaction-insight` etc.). The addendum is an optional caveat value called `maxRequestTime`. 31 | 32 | The value MUST be a valid integer between `5000` (5 seconds) and `180000` (3 minutes) and specifies the request timeout for the given handler in milliseconds. If no value is provided the default timeout of 1 minute (`60000` ms) MUST be used. 33 | 34 | The caveat is specified as follows in the manifest: 35 | 36 | ```json 37 | { 38 | "initialPermissions": { 39 | "endowment:rpc": { 40 | "dapps": true, 41 | "snaps": true, 42 | "maxRequestTime": 120000 43 | }, 44 | "endowment:transaction-insight": { 45 | "maxRequestTime": 30000 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## Copyright 52 | 53 | Copyright and related rights waived via [CC0](../LICENSE). 54 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.1) 5 | public_suffix (>= 2.0.2, < 6.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.1.10) 8 | em-websocket (0.5.3) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0) 11 | eventmachine (1.2.7) 12 | ffi (1.15.5) 13 | forwardable-extended (2.6.0) 14 | http_parser.rb (0.8.0) 15 | i18n (1.12.0) 16 | concurrent-ruby (~> 1.0) 17 | jekyll (4.2.2) 18 | addressable (~> 2.4) 19 | colorator (~> 1.0) 20 | em-websocket (~> 0.5) 21 | i18n (~> 1.0) 22 | jekyll-sass-converter (~> 2.0) 23 | jekyll-watch (~> 2.0) 24 | kramdown (~> 2.3) 25 | kramdown-parser-gfm (~> 1.0) 26 | liquid (~> 4.0) 27 | mercenary (~> 0.4.0) 28 | pathutil (~> 0.9) 29 | rouge (~> 3.0) 30 | safe_yaml (~> 1.0) 31 | terminal-table (~> 2.0) 32 | jekyll-feed (0.16.0) 33 | jekyll (>= 3.7, < 5.0) 34 | jekyll-sass-converter (2.2.0) 35 | sassc (> 2.0.1, < 3.0) 36 | jekyll-seo-tag (2.8.0) 37 | jekyll (>= 3.8, < 5.0) 38 | jekyll-watch (2.2.1) 39 | listen (~> 3.0) 40 | kramdown (2.4.0) 41 | rexml 42 | kramdown-parser-gfm (1.1.0) 43 | kramdown (~> 2.0) 44 | liquid (4.0.3) 45 | listen (3.7.1) 46 | rb-fsevent (~> 0.10, >= 0.10.3) 47 | rb-inotify (~> 0.9, >= 0.9.10) 48 | mercenary (0.4.0) 49 | minima (2.5.1) 50 | jekyll (>= 3.5, < 5.0) 51 | jekyll-feed (~> 0.9) 52 | jekyll-seo-tag (~> 2.1) 53 | pathutil (0.16.2) 54 | forwardable-extended (~> 2.6) 55 | public_suffix (5.0.0) 56 | rb-fsevent (0.11.2) 57 | rb-inotify (0.10.1) 58 | ffi (~> 1.0) 59 | rexml (3.3.9) 60 | rouge (3.30.0) 61 | safe_yaml (1.0.5) 62 | sassc (2.4.0) 63 | ffi (~> 1.9) 64 | terminal-table (2.0.0) 65 | unicode-display_width (~> 1.1, >= 1.1.1) 66 | unicode-display_width (1.8.0) 67 | 68 | PLATFORMS 69 | universal-darwin-21 70 | x86_64-linux 71 | 72 | DEPENDENCIES 73 | http_parser.rb (~> 0.6.0) 74 | jekyll (~> 4.2.2) 75 | jekyll-feed (~> 0.12) 76 | minima (~> 2.5) 77 | tzinfo (~> 1.2) 78 | tzinfo-data 79 | wdm (~> 0.1.1) 80 | 81 | BUNDLED WITH 82 | 2.3.22 83 | -------------------------------------------------------------------------------- /SIPS/sip-19.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 19 3 | title: Multi-file Snap checksum 4 | status: Final 5 | author: Olaf Tomalka (@ritave) 6 | created: 2023-11-28 7 | --- 8 | 9 | ## Abstract 10 | 11 | This SIP describes an algorithm used to checksum a snap in a reproducible manner in a way that includes all files required to run the snap. 12 | 13 | ## Specification 14 | 15 | > Indented sections like this are considered non-normative. 16 | 17 | ### Language 18 | 19 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 20 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 21 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 22 | 23 | ### Steps 24 | 25 | #### Checksum `snap.manifest.json` 26 | 27 | > This algorithm works on `snap.manifest.json` version `0.1`. 28 | 29 | 1. Parse `snap.manifest.json` into JavaScript object using `JSON.parse`. 30 | 2. Delete `.result.source.shasum` field from `snap.manifest.json`. 31 | 3. Convert rest of the structure back to JSON using [`fast-json-stable-stringify@^2.1.0`](https://www.npmjs.com/package/fast-json-stable-stringify) algorithm. 32 | 4. Checksum the resulting string using [auxiliary file](#checksum-auxiliary-files) algorithm. 33 | 34 | #### Auxiliary files 35 | 36 | 1. Calculate [rfc4634 SHA-256](https://datatracker.ietf.org/doc/html/rfc4634) over the raw file data. 37 | 38 | #### Joining files 39 | 40 | 1. Sort all the files by their paths. 41 | 1. The sorting of paths is done using JavaScript's [Less Than over UTF-16 Code Units](https://tc39.es/ecma262/#sec-islessthan) 42 | 2. Two files MUST NOT have the same path. 43 | 2. Calculate [SHA-256 checksum of each file separately](#checksum-auxiliary-files). 44 | 3. Concatenate all the checksums into one buffer and SHA-256 that buffer. 45 | 4. Encode the buffer using [RFC4648, Section 4: Base64 Encoding](https://datatracker.ietf.org/doc/html/rfc4648#section-4) algorithm. 46 | 47 | ## Implementation 48 | 49 | - [Standardized implementation](../assets/sip-19/implementation/implementation.ts) 50 | - [Live implementation in MetaMask](https://github.com/MetaMask/snaps/blob/6e0257741c7eb0fb71df5826fcfabb7658abd03e/packages/snaps-utils/src/snaps.ts#L175-L190) 51 | 52 | ## Copyright 53 | 54 | Copyright and related rights waived via [CC0](../LICENSE). 55 | -------------------------------------------------------------------------------- /assets/sip-4/package.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema#", 3 | "title": "Snap's package.json", 4 | "type": "object", 5 | "required": ["snap", "main", "description", "name", "version", "engines"], 6 | "allOf": [{ "$ref": "https://json.schemastore.org/package.json#" }], 7 | "properties": { 8 | "engines": { 9 | "type": "object", 10 | "required": ["snaps"], 11 | "properties": { 12 | "snaps": { 13 | "type": "string", 14 | "description": "SemVer range specifying this snaps' compatibility with a wallet" 15 | } 16 | } 17 | }, 18 | "main": { 19 | "description": "The bundled snap code that will be executed by the wallet" 20 | }, 21 | "snap": { 22 | "description": "Snap specific metadata", 23 | "type": "object", 24 | "required": ["proposedName", "checksum", "permissions"], 25 | "properties": { 26 | "proposedName": { 27 | "type": "string", 28 | "description": "User readable name for this snap. Shown in the UI", 29 | "minLength": 3, 30 | "maxLength": 214 31 | }, 32 | "checksum": { 33 | "type": "object", 34 | "description": "The checksum verifying the integrity of the bundled code located in \".main\" property", 35 | "markdownDescription": "The checksum verifying the integrity of the bundled code located in `.main` property", 36 | "additionalProperties": false, 37 | "required": ["algorithm", "hash"], 38 | "properties": { 39 | "algorithm": { 40 | "description": "The algorithm used for calculating the checksum", 41 | "const": "sha-256" 42 | }, 43 | "hash": { 44 | "description": "The actual checksum hash", 45 | "$ref": "#/$defs/base64", 46 | "minLength": 44, 47 | "maxLength": 44 48 | } 49 | } 50 | }, 51 | "permissions": { 52 | "description": "An object containing permissions that the snap needs to run", 53 | "type": "object" 54 | }, 55 | "icon": { 56 | "description": "Relative location of the icon file identifying the snap in the UI", 57 | "type": "string" 58 | } 59 | } 60 | } 61 | }, 62 | "$defs": { 63 | "base64": { 64 | "type": "string", 65 | "pattern": "^[A-Za-z0-9+\\/]*={0,2}$" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tools/validate/src/utils.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | 3 | export function deepContains(obj: unknown, test: unknown): boolean { 4 | if (typeof obj !== typeof test) { 5 | return false; 6 | } 7 | // Primitives, strict equality 8 | if ( 9 | [ 10 | "bigint", 11 | "boolean", 12 | "number", 13 | "string", 14 | "symbol", 15 | "undefined", 16 | "function", 17 | ].includes(typeof obj) || 18 | obj === null 19 | ) { 20 | return obj === test; 21 | } 22 | 23 | // Arrays, test must be smaller length than obj and each index of test must deepContains as well 24 | if (Array.isArray(obj)) { 25 | if (!Array.isArray(test) || test.length > obj.length) { 26 | return false; 27 | } 28 | for (const [objChild, testChild] of zip(obj, test)) { 29 | if (!deepContains(objChild, testChild)) { 30 | return false; 31 | } 32 | } 33 | return true; 34 | } 35 | 36 | // Objects, all keys in test must be in obj and all values of those keys must deepContains as well 37 | assert( 38 | typeof obj === "object" && 39 | obj !== null && 40 | typeof test === "object" && 41 | test !== null 42 | ); 43 | for (const [testKey, testValue] of Object.entries(test)) { 44 | if (!(testKey in obj) || !deepContains((obj as any)[testKey], testValue)) { 45 | return false; 46 | } 47 | } 48 | return true; 49 | } 50 | 51 | export function* zip>( 52 | ...iterables: { [K in keyof T]: Iterable } 53 | ): Generator { 54 | const iterators = iterables.map((i) => i[Symbol.iterator]()); 55 | while (true) { 56 | const results = iterators.map((i) => i.next()); 57 | 58 | if (results.some(({ done }) => done)) { 59 | break; 60 | } 61 | 62 | yield results.map(({ value }) => value) as T; 63 | } 64 | } 65 | 66 | const ISO8601 = /^\d\d\d\d\-\d\d\-\d\d$/; 67 | 68 | export function isValidISO8601Date(data: string): boolean { 69 | const result = data.match(ISO8601); 70 | if (result === null) { 71 | return false; 72 | } 73 | const date = new Date(data); 74 | const now = new Date(); 75 | return date <= now; 76 | } 77 | 78 | export function isValidURL(url: string, protocols?: string[]): boolean { 79 | try { 80 | const parsed = new URL(url); 81 | return protocols === undefined 82 | ? true 83 | : protocols.includes(parsed.protocol.slice(0, -1)); // remove ':' at end 84 | } catch (e) { 85 | return false; 86 | } 87 | } 88 | 89 | export function lowercaseFirst(string: string) { 90 | return string.charAt(0).toLowerCase() + string.slice(1); 91 | } 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Yarn files 107 | .pnp.* 108 | .yarn/* 109 | !.yarn/patches 110 | !.yarn/plugins 111 | !.yarn/releases 112 | !.yarn/sdks 113 | !.yarn/versions 114 | 115 | # Swap the comments on the following lines if you don't wish to use zero-installs 116 | # Documentation here: https://yarnpkg.com/features/zero-installs 117 | #!**/.yarn/cache 118 | #.pnp.* 119 | 120 | _site 121 | .sass-cache 122 | .jekyll-cache 123 | .jekyll-metadata 124 | vendor 125 | 126 | .DS_Store 127 | -------------------------------------------------------------------------------- /SIPS/sip-10.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 10 3 | title: snap_getLocale 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/99 6 | author: Frederik Bolding (@FrederikBolding), Hassan Malik (@hmalik88) 7 | created: 2023-06-30 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes an RPC method that lets snaps access the user selected locale in MetaMask, as a way to inform their localization efforts. This proposal will outline implementation details of said RPC method. 13 | 14 | ## Motivation 15 | 16 | Snaps that want to localize their copy used in custom interfaces etc. currently have to implement their own system for letting a user select their preferred language. The proposed RPC method provides a developer experience improvement to snap developers by letting them use the user's existing and preferred localization settings. 17 | 18 | ## Specification 19 | 20 | > Formal specifications are written in TypeScript. 21 | 22 | ### Language 23 | 24 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 25 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 26 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 27 | 28 | ### Proposed implementation 29 | 30 | The proposed RPC method `snap_getLocale` SHOULD be a restricted RPC method requiring user consent before usage via the permission system. The RPC method SHOULD only be available to Snaps. 31 | 32 | The implementation MUST use a `getLocale` method hook for accessing the user locale. This lets each implementing client specify the function themselves, letting the RPC implementation remain platform-agnostic. 33 | 34 | The client MUST return an [IETF BCP 47](https://www.ietf.org/rfc/bcp/bcp47.txt) formatted locale string. 35 | 36 | ```tsx 37 | /** 38 | * Builds the method implementation for `snap_getLocale`. 39 | * 40 | * @param hooks - The RPC method hooks. 41 | * @param hooks.getLocale - A function that returns the user selected locale. 42 | * @returns The user selected locale. 43 | */ 44 | export function getImplementation({ getLocale }: GetLocaleMethodHooks) { 45 | return async function implementation( 46 | _args: RestrictedMethodOptions, 47 | ): Promise { 48 | return getLocale(); 49 | }; 50 | } 51 | ``` 52 | 53 | ```tsx 54 | /** 55 | * Method hook for accessing the user locale from the PreferencesController. 56 | * 57 | * @returns The user selected locale. 58 | */ 59 | function getLocale(): string { 60 | return this.preferencesController.store.getState().currentLocale; 61 | } 62 | ``` 63 | 64 | ## Copyright 65 | 66 | Copyright and related rights waived via [CC0](../LICENSE). 67 | -------------------------------------------------------------------------------- /SIPS/sip-15.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 15 3 | title: Snaps Home Page 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/115 6 | author: Frederik Bolding (@frederikbolding) 7 | created: 2023-10-26 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes a new API that allows snaps to surface a static UI to display data that may be useful to users of the snap. This proposal outlines some of the details around this feature. 13 | 14 | ## Motivation 15 | 16 | Snaps currently tend to leverage separate websites for showing details relating to the status of the snap. For example, snaps that manage new blockchain accounts for users have to rely almost exclusively on these to show balances on new kinds of networks. This proposal aims to start improving on this by giving snaps a surface directly in the client where they can render custom UI. 17 | 18 | This new surface differs from the existing custom UI surfaces as it isn't reactionary, instead of being triggered by an RPC call or a transaction being confirmed the user can choose to view the home page screen at any time. 19 | 20 | ## Specification 21 | 22 | > Formal specifications are written in Typescript. 23 | 24 | ### Language 25 | 26 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 27 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 28 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 29 | 30 | ### Snap Manifest 31 | 32 | This SIP specifies a permission named `endowment:page-home`. 33 | The permission signals to the platform that the snap wants to use the home page functionality. 34 | 35 | This permission is specified as follows in `snap.manifest.json` files: 36 | 37 | ```json 38 | { 39 | "initialPermissions": { 40 | "endowment:page-home": {} 41 | } 42 | } 43 | ``` 44 | 45 | ### Snap Implementation 46 | 47 | When a user navigates to the snap home page, the `onHomePage` handler will be invoked. This handler MUST be used to generate the UI content for the home page. 48 | 49 | Any snap that wishes to expose a home page MUST implement the following API: 50 | 51 | ```typescript 52 | import { panel, text } from "@metamask/snap-ui"; 53 | import { OnHomePageHandler } from "@metamask/snap-types"; 54 | 55 | export const onHomePage: OnHomePageHandler = async () => { 56 | const content = panel([text('Hello world!')]) 57 | return { content }; 58 | }; 59 | ``` 60 | 61 | The `onHomePage` handler takes no arguments and MUST return a value that matches the following interface: 62 | 63 | ```typescript 64 | import { Component } from "@metamask/snap-ui"; 65 | interface OnHomePageResponse { 66 | content: Component; 67 | } 68 | ``` 69 | 70 | 71 | ## Copyright 72 | 73 | Copyright and related rights waived via [CC0](../LICENSE). 74 | -------------------------------------------------------------------------------- /assets/sip-2/permission.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Keyring Permission", 3 | "type": "object", 4 | "additionalProperties": false, 5 | "required": ["namespaces"], 6 | "properties": { 7 | "namespaces": { 8 | "title": "Namespaces", 9 | "description": "List of supported CAIP-2 namespaces", 10 | "type": "object", 11 | "additionalProperties": false, 12 | "patternProperties": { 13 | "[-a-z0-9]{3,8}": { 14 | "title": "Namespace", 15 | "description": "Description of one CAIP-2 namespace", 16 | "type": "object", 17 | "additionalProperties": false, 18 | "required": ["chains"], 19 | "properties": { 20 | "chains": { 21 | "title": "Supported chains", 22 | "description": "A list of supported chains inside that namespace", 23 | "type": "array", 24 | "items": { 25 | "title": "Chain", 26 | "description": "Description of a specific chain inside namespace", 27 | "type": "object", 28 | "additionalProperties": false, 29 | "required": ["id", "name"], 30 | "properties": { 31 | "name": { 32 | "title": "Chain name", 33 | "description": "User readable name of the supported chain", 34 | "type": "string", 35 | "minLength": 1, 36 | "maxLength": 40 37 | }, 38 | "id": { 39 | "title": "Chain ID", 40 | "description": "A fully qualified chain ID using CAIP-2 specification", 41 | "type": "string", 42 | "pattern": "[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,32}" 43 | } 44 | } 45 | } 46 | }, 47 | "methods": { 48 | "title": "RPC methods", 49 | "description": "A list of supported RPC methods on this namespace that a DApp can call", 50 | "type": "array", 51 | "items": { 52 | "title": "Method name", 53 | "type": "string", 54 | "minLength": 1, 55 | "maxLength": 40 56 | } 57 | }, 58 | "events": { 59 | "title": "RPC events", 60 | "description": "A list of supported RPC events on this namespace that a DApp can listen to", 61 | "type": "array", 62 | "items": { 63 | "title": "Event name", 64 | "type": "string", 65 | "minLength": 1, 66 | "maxLength": 40 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /SIPS/sip-32.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 32 3 | title: Event tracking method for pre-installed Snaps 4 | status: Draft 5 | author: Daniel Rocha (@danroc) 6 | created: 2025-03-20 7 | --- 8 | 9 | ## Abstract 10 | 11 | This Snap Improvement Proposal (SIP) introduces a new method, 12 | `snap_trackEvent`, allowing pre-installed Snaps to submit tracking events 13 | through the client. The client will determine how to handle these events. 14 | 15 | This feature enables Snaps to utilize the existing client infrastructure for 16 | event tracking while maintaining user privacy. 17 | 18 | ## Motivation 19 | 20 | Currently, there is no standardized way for pre-installed Snaps to submit 21 | analytics or tracking events. This proposal aims to: 22 | 23 | - Enable pre-installed Snaps to leverage the client's event tracking 24 | infrastructure. 25 | 26 | - Ensure that event handling is controlled at the client level and conforms to 27 | security and privacy controls enforced by the client. 28 | 29 | ## Specification 30 | 31 | ### Language 32 | 33 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 34 | "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" written 35 | in uppercase in this document are to be interpreted as described in [RFC 36 | 2119](https://www.ietf.org/rfc/rfc2119.txt) 37 | 38 | ### `snap_trackEvent` 39 | 40 | The `snap_trackEvent` method allows pre-installed Snaps to submit structured 41 | tracking events to the client. It is the client’s responsibility to process 42 | these events accordingly. 43 | 44 | The `snap_trackEvent` method is endowed on any Snap that the client considers 45 | pre-installed. 46 | 47 | #### Parameters 48 | 49 | - `event` - Event object to be tracked. 50 | - `event: string` - The name of the event to track. 51 | 52 | - `properties: Record` - (**Optional**) Custom values to track. 53 | The client MUST enforce that all keys in this object are in the 54 | `snake_case` format. 55 | 56 | - `sensitiveProperties: Record` - (**Optional**) Sensitive 57 | values to track. These properties will be sent in an additional event that 58 | excludes the user's `metaMetricsId`. The client MUST enforce that all keys 59 | in this object are in the `snake_case` format. 60 | 61 | #### Returns 62 | 63 | Resolves to `null` when the event is successfully handled by the client. 64 | 65 | #### Example 66 | 67 | ```typescript 68 | await snap.request({ 69 | method: 'snap_trackEvent', 70 | params: { 71 | event: { 72 | event: 'Account Added', 73 | properties: { 74 | message: 'Snap account added', 75 | }, 76 | sensitiveProperties: { 77 | account_type: 'Hardware Wallet', 78 | }, 79 | }, 80 | }, 81 | }); 82 | ``` 83 | 84 | ## Copyright 85 | 86 | Copyright and related rights waived via [CC0](../LICENSE). 87 | -------------------------------------------------------------------------------- /SIPS/sip-22.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 22 3 | title: MetaMask URL scheme 4 | status: Draft 5 | author: Christian Montoya (@Montoya), Hassan Malik (@hmalik88) 6 | created: 2024-09-13 7 | --- 8 | 9 | ## Abstract 10 | 11 | This SIP describes a URL scheme for extension navigation. The described URL will allow for navigation to Snaps entry points and elsewhere within the MetaMask extension. This scheme can potentially be used by Snap methods in the future to navigate the user to a location in-client and trigger the entry point. 12 | 13 | ## Motivation 14 | 15 | While some Snap entry points are reactive, meaning they can be triggered by a method, others can only be triggered by a user navigating to the location where that entry point is displayed. In order for a Snap to programmatically navigate a user to that entry point, the Snap needs a way to identify that location. The location is identified by a URL scheme, and an in-client link pointing to this location is called a _deep link_. 16 | 17 | ## Specification 18 | 19 | ### Language 20 | 21 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 22 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 23 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 24 | 25 | ### URL Scheme 26 | 27 | [URL syntax](https://en.wikipedia.org/wiki/URL#Syntax) is defined with a scheme, authority, path, and with optional (query and fragment) portions. This SIP defines a new scheme: `metamask:`, the syntax is defined below: 28 | 29 | `metamask://[authority]/[path]` 30 | 31 | Where: 32 | 33 | - `[authority]` refers to either `client` or `snap` (in the current case, `client` can mean either the extension or mobile version of MetaMask) 34 | - `[path]` refers to the entire path which depending on the authority can be different 35 | - For the `client` authority, the following paths are available: 36 | - `/` - links to the client's home page 37 | - For the `snap` authority, the path starts with the snap ID and has the following paths available to it: 38 | - `/home` - leads to the snap's [home page](/SIPS/sip-15.md) (which is its settings page if it doesn't have a home page) 39 | 40 | > [!NOTE] 41 | > 1. In the future, fragments can potentially be used for navigation to specific portions of a page and params can be provided from a calling method for exports such as `onHome`. 42 | > 2. This URL scheme assumes parity between `client` locations. 43 | 44 | ### Examples 45 | 46 | The proposed URL for the Starknet Snap home page: 47 | 48 | `metamask://snap/npm:@consensys/starknet-snap/home` 49 | 50 | The URL for navigating to the client's home page: 51 | 52 | `metamask://client/` 53 | 54 | ### Using Deep Links 55 | 56 | A Snap will be able to navigate to these links using the [SIP-7](https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-7.md) `Link` component. 57 | 58 | ## Copyright 59 | 60 | Copyright and related rights waived via [CC0](../LICENSE). -------------------------------------------------------------------------------- /SIPS/sip-28.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 28 3 | title: Background Events 4 | status: Draft 5 | author: Olaf Tomalka (@ritave) 6 | created: 2024-10-09 7 | --- 8 | 9 | ## Abstract 10 | 11 | This SIP introduces a way to schedule non-recurring events in the future. 12 | 13 | ## Motivation 14 | 15 | Scheduled recurring events are already supported in the Snaps platform via the Cronjobs feature. By introducing non-recurring events we will allow novel use-cases for Snap developers, such as allowing a snap that sets a reminder for ENS domain expiration date. 16 | 17 | ## Specification 18 | 19 | > Indented sections like this are considered non-normative. 20 | 21 | ### Language 22 | 23 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 24 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 25 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 26 | 27 | ### Snap Manifest 28 | 29 | This SIP doesn't introduce any new permissions, but rather extends `endowment:cronjob` with new capabilities through two new RPC methods. 30 | 31 | ### RPC Methods 32 | 33 | #### `snap_scheduleBackgroundEvent` 34 | 35 | This method allows a Snap to schedule a one-off callback to `onCronjob` handler in the future with a JSON-RPC request object as a parameter. 36 | 37 | ```typescript 38 | type ScheduleBackgroundEventParams = { 39 | date: string; 40 | request: JsonRpcRequest; 41 | }; 42 | 43 | type ScheduleBackgroundEventResult = string; 44 | ``` 45 | 46 | The RPC method takes two parameters: 47 | 48 | - `date` - An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date and time and optional timezone offset. 49 | - The time's precision SHALL be truncated on the extension side to minutes. 50 | - If no timezone is provided, the time SHALL be understood to be local-time. 51 | > Use ISO's `Z` identifier if you want to use UTC time. 52 | - `request` - A JSON object that will provided as-is to `onCronjob` handler as parameter. 53 | 54 | An example of usage is given below. 55 | 56 | ```typescript 57 | snap.request({ 58 | method: "snap_scheduleBackgroundEvent", 59 | params: { 60 | date: "2024-10-09T09:59", 61 | request: { 62 | method: "foobar", 63 | params: { 64 | foo: "bar", 65 | }, 66 | }, 67 | }, 68 | }); 69 | ``` 70 | 71 | The RPC method call returns a `string` that is a unique ID representing that specific background event, allowing it to be cancelled. 72 | 73 | #### `snap_cancelBackgroundEvent` 74 | 75 | This method allows to cancel an already scheduled background event using the unique ID returned from `snap_backgroundEventSchedule` 76 | 77 | ```typescript 78 | type CancelBackgroundEventParams = { id: string }; 79 | ``` 80 | 81 | This RPC method takes one argument: 82 | 83 | - `id` - The id that was returned during schedule. 84 | 85 | An example of usage is given below. 86 | 87 | ```typescript 88 | snap.request({ 89 | method: "snap_cancelBackgroundEvent", 90 | params: { 91 | id: myReturnedId, 92 | }, 93 | }); 94 | ``` 95 | 96 | ### `onCronjob` handler 97 | 98 | This SIP doesn't modify `onCronjob` handler in any way. 99 | 100 | ## Copyright 101 | 102 | Copyright and related rights waived via [CC0](../LICENSE). 103 | -------------------------------------------------------------------------------- /SIPS/sip-11.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 11 3 | title: Transaction insight severity levels 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/101 6 | author: Hassan Malik (@hmalik88) 7 | created: 2023-07-16 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes a way for snaps to provide extra friction for executing potentially dangerous transactions within MetaMask with the addition of a `severity` field to the already existing transaction insights API outlined in SIP-3. 13 | 14 | ## Motivation 15 | 16 | One of the biggest issues with wallet users is a loss of funds through executing some sketchy or seemingly innocuous transaction. With the benefit of the transaction insights API, a snap can already run analysis on an unsigned transaction payload. By adding `severity` to the existing API, we allow for the snap to provide extra friction to the user if it determines that a transaction is malicious. Users often click through things without reading, by allowing for a warning we can add friction at points that we really think a user should have a second glance. 17 | 18 | This warning could be injected at any point where there are insights provided. 19 | 20 | ### MetaMask Integration 21 | 22 | The `severity` key is added to the return object to indicate the severity level of the content being returned to the extension. This will help trigger certain UI in the extension. Currently, a warning modal will be triggered for content with a severity level of `critical`. The modal will require a checkbox to be checked before the user can continue with the transaction. 23 | 24 | In future SIPs, the `SeverityLevel` enum can be expanded to include other levels that can be also be used to influence the UI in the extension. 25 | 26 | Transaction insight snaps were previously triggered on view of their respective tabs, but with the addition of the `severity` key, execution would become unprompted in order to determine if a modal needs to be displayed as you reach the confirmation screen. 27 | 28 | ### Snap Implementation 29 | 30 | The following is an example implementation of the API: 31 | 32 | ```typescript 33 | import { OnTransactionHandler } from "@metamask/snap-types"; 34 | 35 | enum SeverityLevel { 36 | Critical = 'critical', 37 | } 38 | 39 | export const onTransaction: OnTransactionHandler = async ({ 40 | transaction, 41 | chainId, 42 | }) => { 43 | const content = /* Get UI component with insights */; 44 | const isContentCritical = /* Boolean checking if content is critical */ 45 | return isContentCritical ? { content, severity: SeverityLevel.Critical } : { content }; 46 | }; 47 | ``` 48 | 49 | The interface for the return value of an `onTransaction` export is: 50 | 51 | ```typescript 52 | interface OnTransactionResponse { 53 | content: Component | null; 54 | severity?: SeverityLevel; 55 | } 56 | ``` 57 | 58 | **Note:** `severity` is an optional field and the omission of such means that there is no escalation of the content being returned. 59 | 60 | ## Specification 61 | 62 | Please see [SIP-3](https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-3.md) for more information on the original transaction insights API. 63 | 64 | Please see the [SIP-7](https://github.com/MetaMask/SIPs/blob/main/SIPS/sip-7.md) package for more information on the `Component` type returned in the `OnTransactionResponse`. 65 | 66 | ## Copyright 67 | 68 | Copyright and related rights waived via [CC0](../LICENSE). 69 | -------------------------------------------------------------------------------- /tools/validate/src/rules/bad-link.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import Debug from "debug"; 3 | import isValidPath from "is-valid-path"; 4 | import { Definition, Image, Link, Root } from "mdast"; 5 | import { lintRule } from "unified-lint-rule"; 6 | import { Node } from "unist"; 7 | import { visit } from "unist-util-visit"; 8 | import { Semaphore } from 'async-mutex'; 9 | 10 | const debug = Debug("validate:rule:bad-link"); 11 | 12 | function isFragment(url: string) { 13 | return url.startsWith("#"); 14 | } 15 | 16 | function isLocal(url: string | URL) { 17 | url = new URL(url); 18 | return ["127.0.0.1", "localhost", "::1"].includes(url.hostname); 19 | } 20 | 21 | function isExternal(url: string | URL): boolean { 22 | try { 23 | url = new URL(url); 24 | } catch (e) { 25 | debug(e, typeof url, url); 26 | assert(typeof url === "string"); 27 | // relative path 28 | if (isValidPath(url)) { 29 | return false; 30 | } 31 | // #paragraph-references 32 | if (isFragment(url)) { 33 | return false; 34 | } 35 | debug("Invalid url", url, e); 36 | throw e; 37 | } 38 | 39 | return !isLocal(url); 40 | } 41 | 42 | const fetchCache = new Map>(); 43 | const fetchSemaphore = new Semaphore(3); 44 | 45 | const fetchWithSemaphore = async (url: string, options?: RequestInit) => { 46 | // Queue requests to reduce spam. 47 | return fetchSemaphore.runExclusive(() => fetch(url, options)); 48 | } 49 | 50 | const rule = lintRule("sip:bad-link", async (tree, file) => { 51 | const fetchWithLog = 52 | (node: Node) => 53 | async ( 54 | url: string, options?: RequestInit 55 | ): ReturnType => { 56 | try { 57 | const key = `${options?.method ?? 'GET'}-${url}`; 58 | if (fetchCache.has(key)) { 59 | return fetchCache.get(key)!; 60 | } 61 | const response = fetchWithSemaphore(url, options); 62 | fetchCache.set(key, response); 63 | return response; 64 | } catch (e) { 65 | file.message( 66 | `Url "${url}" is invalid, the server doesn't exist or there's no internet access.`, 67 | node 68 | ); 69 | debug("Fetch failed", url, e); 70 | 71 | throw e; 72 | } 73 | }; 74 | 75 | const testUrl = async (url: string, node: Node) => { 76 | if (!isExternal(url)) { 77 | return; 78 | } 79 | const fetch = fetchWithLog(node); 80 | let response = await fetch(url, { method: "HEAD" }); 81 | debug("Fetch responded", "url:", url, "is ok:", response.ok); 82 | // Don't treat 429 responses as invalid links. 83 | if (!response.ok && response.status !== 429) { 84 | debug("Fetch (HEAD) not ok, trying GET", url, response.status); 85 | response = await fetch(url); 86 | if (!response.ok && response.status !== 429) { 87 | file.message( 88 | `Url "${url}" is invalid, the server responded with ${response.status}`, 89 | node 90 | ); 91 | } 92 | } 93 | }; 94 | 95 | const tests: Promise[] = []; 96 | 97 | visit(tree, ["definition", "link", "image"], (( 98 | node: Definition | Link | Image 99 | ) => { 100 | tests.push(testUrl(node.url, node)); 101 | }) as any); // casting to any because the node has specific types 102 | 103 | await Promise.all(tests); 104 | }); 105 | export default rule; 106 | -------------------------------------------------------------------------------- /SIPS/sip-31.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 31 3 | title: Client-only RPC Entrypoint 4 | status: Draft 5 | author: Daniel Rocha (@danroc) 6 | created: 2025-03-19 7 | --- 8 | 9 | ## Abstract 10 | 11 | This proposal introduces a new entrypoint, `onClientRequest`, allowing Snaps to 12 | expose a handler that can only be called by the client. 13 | 14 | While the request `origin` can currently be used to differentiate requests from 15 | clients and dapps, this new entrypoint increases separation and minimizes 16 | the risks associated with `origin` spoofing or request routing bugs. 17 | 18 | ## Motivation 19 | 20 | Currently, Snaps rely on the `origin` field of incoming requests to 21 | differentiate whether a request originates from the client, another 22 | Snap, or an external dapp. However, this approach has potential risks: 23 | 24 | - **Origin Spoofing**: Bugs or vulnerabilities in request validation could 25 | allow dapps to masquerade as the client or a different dapp. 26 | 27 | - **Unintended Exposure**: If a Snap processes requests without strict 28 | validation, it may unintentionally expose methods to dapps or other Snaps 29 | that are meant only for the client. 30 | 31 | - **Cleaner Separation**: Having a dedicated entrypoint for client requests 32 | enforces stricter request isolation at the API level. 33 | 34 | By introducing `onClientRequest`, we ensure that Snaps can define handlers 35 | exclusively accessible by the client, reducing potential attack vectors. 36 | 37 | ## Specification 38 | 39 | This proposal introduces a new OPTIONAL handler function, `onClientRequest`, 40 | which a Snap MAY implement. 41 | 42 | ### Language 43 | 44 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 45 | "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" written 46 | in uppercase in this document are to be interpreted as described in [RFC 47 | 2119](https://www.ietf.org/rfc/rfc2119.txt). 48 | 49 | ### Snap Implementation 50 | 51 | #### On Client Request 52 | 53 | This specification introduces to the Snap Platform a dedicated handler that 54 | Snaps MAY implement to process JSON-RPC requests originating exclusively from 55 | the client. 56 | 57 | Example: 58 | 59 | ```typescript 60 | import { OnClientRequestHandler } from "@metamask/snaps-sdk"; 61 | 62 | export const onClientRequest: OnClientRequestHandler = async ({ 63 | request, 64 | }) => { 65 | const result = /* Handle `request` */; 66 | return result; 67 | }; 68 | ``` 69 | 70 | The type of the `onClientRequest` handler is: 71 | 72 | ```typescript 73 | type OnClientRequestHandler = ( 74 | args: OnClientRequestArguments, 75 | ) => Promise; 76 | ``` 77 | 78 | The type for an `onClientRequest` handler function’s arguments is: 79 | 80 | ```typescript 81 | type OnClientRequestArguments = { 82 | request: JsonRpcRequest; 83 | }; 84 | ``` 85 | 86 | The type for an `onClientRequest` handler function’s return value is: 87 | 88 | ```typescript 89 | type OnClientRequestResponse = Json; 90 | ``` 91 | 92 | #### Behavior 93 | 94 | - The client MUST ensure that it is the origin of a request before invoking 95 | `onClientRequest`. 96 | 97 | - Requests from other origins MUST be rejected. 98 | 99 | - If a Snap does not implement `onClientRequest`, the client MUST throw an 100 | exception for requests directed to that entrypoint, regardless of the origin. 101 | 102 | ## Copyright 103 | 104 | Copyright and related rights waived via [CC0](../LICENSE). 105 | -------------------------------------------------------------------------------- /_layouts/sip.html: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | --- 4 | 5 |
6 |

7 | SIP-{{ page.sip | xml_escape }}: {{ page.title | xml_escape }} 8 | 10 | Source 11 | 13 | 14 |

15 |

{{ page.description | xml_escape }}

16 | 17 | 18 | 19 | 20 | 21 | {% if page["discussions-to"] != undefined %} 22 | 23 | 24 | 25 | 26 | {% endif %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {% if page.updated != undefined %} 36 | 37 | 38 | 39 | 40 | {% endif %} 41 | {% if page.requires != undefined %} 42 | 43 | 44 | 45 | 46 | {% endif %} 47 | {% if page["withdrawal-reason"] != undefined %} 48 | 49 | 50 | 51 | 52 | {% endif %} 53 |
Author{% include authorlist.html authors=page.author %}
Discussions-To{% include discussion_links.html links=page.discussions-to %}
Status{{ page.status | xml_escape }}
Created{{ page.created | xml_escape }}
Updated{{ page.updated | xml_escape }}
Requires{% include sipnums.html sips=page.requires %}
Withdrawal reason{{ page["withdrawal-reason"] | xml_escape }}
54 | 55 |
56 |

Table of Contents

57 | {% include toc.html html=content sanitize=true h_max=3 %} 58 |
59 | 60 | {% include anchor_headings.html html=content anchorClass="anchor-link" beforeHeading=true %} 61 | 62 |

Citation

63 |

Please cite this document as:

64 | {% comment %} 65 | IEEE specification for reference formatting: 66 | https://ieee-dataport.org/sites/default/files/analysis/27/IEEE%20Citation%20Guidelines.pdf 67 | {% endcomment %} 68 |

{% include authorlist.html authors=page.author %}, "SIP-{{ page.sip | xml_escape }}: {{ page.title | xml_escape 69 | }}{% if page.status == "Draft" or page.status == "Review" or page.status == "Implementation" %} [DRAFT]{% endif %}," Snaps Improvement Proposals, no. {{ 70 | page.sip | xml_escape }}, {{ page.created | date: "%B %Y" }}. [Online serial]. Available: 71 | https://github.com/MetaMask/SIPs/blob/master/SIPS/sip-{{ page.sip | xml_escape }}.md

72 |
73 | {% comment %} 74 | Article schema specification: 75 | https://schema.org/TechArticle 76 | {% endcomment %} 77 | 94 | -------------------------------------------------------------------------------- /tools/validate/src/rules/preamble-data.ts: -------------------------------------------------------------------------------- 1 | import Debug from "debug"; 2 | import { Root } from "mdast"; 3 | import { 4 | enums, 5 | number, 6 | object, 7 | optional, 8 | refine, 9 | string, 10 | Struct, 11 | validate, 12 | } from "superstruct"; 13 | import { lintRule } from "unified-lint-rule"; 14 | import { select } from "unist-util-select"; 15 | import { ParsedYaml } from "../plugins/parseYaml.js"; 16 | import { isValidISO8601Date, isValidURL, lowercaseFirst } from "../utils.js"; 17 | 18 | const debug = Debug("validate:rule:preamble"); 19 | 20 | const url = (protocols?: string[]) => 21 | refine( 22 | string(), 23 | "url", 24 | (value) => 25 | isValidURL(value, protocols) || `not a valid ${protocols?.join("/")} url` 26 | ); 27 | const date = () => 28 | refine( 29 | string(), 30 | "date", 31 | (value) => isValidISO8601Date(value) || "not a valid iso-8601 date" 32 | ); 33 | const positive = (struct: Struct) => 34 | refine(struct, "positive", (value) => value > 0 || "not a positive number"); 35 | const integer = () => 36 | refine( 37 | number(), 38 | "integer", 39 | (value) => Number.isSafeInteger(value) || "not an integer" 40 | ); 41 | 42 | const GITHUB_USERNAME = String.raw`[a-zA-Z\d](?:[a-zA-Z\d]|-(?=[a-zA-Z\d])){0,38}`; 43 | const EMAIL = String.raw`.+@.+`; 44 | const AUTHOR = new RegExp( 45 | String.raw`^\w[.\w\s]*(?: (?:\<${EMAIL}\>(?: \((\@${GITHUB_USERNAME})\))?)|\((\@${GITHUB_USERNAME})\))?$` 46 | ); 47 | const author = () => 48 | refine(string(), "author", (value) => { 49 | const authors: string[] = value.split(","); 50 | let hasGithub = false; 51 | for (const author of authors) { 52 | const match = author.trim().match(AUTHOR); 53 | if (match === null) { 54 | return "is malformed"; 55 | } 56 | if (match[1] !== undefined || match[2] !== undefined) { 57 | hasGithub = true; 58 | } 59 | } 60 | return hasGithub || "doesn't have at least one GitHub account"; 61 | }); 62 | 63 | export const Preamble = refine( 64 | object({ 65 | sip: positive(integer()), 66 | title: string(), 67 | status: enums([ 68 | "Draft", 69 | "Review", 70 | "Implementation", 71 | "Final", 72 | "Withdrawn", 73 | "Living", 74 | ]), 75 | "discussions-to": optional(url(["http", "https"])), 76 | author: author(), 77 | created: date(), 78 | updated: optional(date()), 79 | }), 80 | "preamble", 81 | (value) => { 82 | const errors = []; 83 | 84 | if ( 85 | value.updated !== undefined && 86 | new Date(value.updated) < new Date(value.created) 87 | ) { 88 | errors.push('property "updated" is earlier than "created"'); 89 | } 90 | 91 | if (value.status === "Living" && value.updated === undefined) { 92 | errors.push( 93 | 'has status of "Living" but doesn\'t have "updated" preamble header' 94 | ); 95 | } 96 | return errors.length === 0 || errors; 97 | } 98 | ); 99 | 100 | const rule = lintRule("sip:preamble-data", (tree, file) => { 101 | const node = select("parsedYaml", tree) as ParsedYaml | null; 102 | if (!node) { 103 | return; 104 | } 105 | const [error] = validate(node.data.parsed, Preamble); 106 | 107 | if (error !== undefined) { 108 | const failures = error.failures(); 109 | debug(failures); 110 | failures.forEach((failure) => { 111 | const prop = failure.path.at(-1); 112 | let msg; 113 | if (prop === undefined) { 114 | msg = `Front-matter ${lowercaseFirst(failure.message)}`; 115 | } else { 116 | msg = `Front-matter property "${prop}" ${lowercaseFirst( 117 | failure.message 118 | )}`; 119 | } 120 | file.message(msg, node); 121 | }); 122 | } 123 | }); 124 | 125 | export default rule; 126 | -------------------------------------------------------------------------------- /SIPS/sip-8.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 8 3 | title: Snap Locations 4 | status: Implementation 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/75 6 | author: Olaf Tomalka (@ritave) 7 | created: 2022-11-03 8 | --- 9 | 10 | ## Abstract 11 | 12 | A specification of URIs the DApp can use under which the wallet looks for snaps. 13 | It describes location URIs which can be used to locate snap as well as specifying how to access relative files. 14 | 15 | ## Motivation 16 | 17 | Currently Snap IDs are used both as the unique identifier as well as location to look for a snap. This introduces weird behavior where a snap installed from different places (such as npm and http) with the same code is different while different snaps from one location (such as multiple deployments from http://localhost:8080) are treated as single continuously updated snap and share persistent state. 18 | 19 | This SIP is part of separating Snap IDs into location and a unique identifier. 20 | 21 | ## Specification 22 | 23 | > Such sections are considered non-normative. 24 | 25 | ### Language 26 | 27 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 28 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 29 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 30 | 31 | ### Snap Location 32 | 33 | The location of a snap MUST be an URI as defined in [RFC-3986](https://datatracker.ietf.org/doc/html/rfc3986). 34 | 35 | > URI consists of a `scheme`, `authority` and a `path`, which we use throughout this SIP. 36 | 37 | ### Supported schemes 38 | 39 | - `npm` - The snap SHALL be searched for using [registry.npmjs.com](https://registry.npmjs.com) protocol. `https` SHALL be used as the underlying transport protocol. 40 | - The `authority` part of the URI SHALL indicate the npmjs-like registry to use. The `authority` MAY be omitted and the default [https://registry.npmjs.com](https://registry.npmjs.com) SHALL be used instead. 41 | - The `path` represents the package's id namespaced to the registry. The wallet SHALL search for `package.json` in the root directory of the package. All files referenced in `package.json` SHALL be searched for relative to the root directory of the package. 42 | - `http` / `https` - The `package.json` MUST be under that URL. All files referenced in `package.json` SHALL be searched for relative to the path of `package.json` using relative URL resolution as described by [RFC 1808](https://www.ietf.org/rfc/rfc1808.txt). 43 | - `ipfs` - The `authority` MUST be an IPFS CID that is also a directory. The wallet SHALL search for `package.json` in root of that directory. All files referenced in `package.json` SHALL be looked relative to the root directory. 44 | 45 | ## Test vectors 46 | 47 | ### NPM 48 | 49 | - `npm:my-snap` 50 | - `scheme` - `npm` 51 | - `authority` - `https://registry.npmjs.com` 52 | - `path` - `my-snap` 53 | - `npm://root@my-registry.com:8080/my-snap` 54 | - `scheme` - `npm` 55 | - `authority` - `https://root@my-registry.com:8080` 56 | - `path` - `my-snap` 57 | 58 | ### HTTP / HTTPS 59 | 60 | > Test vectors for HTTP are considered the same except the differing scheme 61 | 62 | - `https://localhost:8080` 63 | - `scheme` - `https` 64 | - `authority` - `localhost:8080` 65 | - `path` - _(zero-length)_ 66 | - `package.json:main: "dist/index.js"` - `https://localhost:8080/dist/index.js` 67 | - `https://my-host.com/my-snap` 68 | - `scheme` - `https` 69 | - `authority` - `my-host.com` 70 | - `path` - `my-snap` 71 | - `package.json:main: "dist/index.js"` - `https://my-host.com/dist/index.js` 72 | - `https://my-host.com/my-snap/` 73 | - `scheme` - `https` 74 | - `authority` - `my-host.com` 75 | - `path` - `my-snap/` 76 | - `package.json:main: "dist/index.js"` - `https://my-host.com/my-snap/dist/index.js` 77 | 78 | ### IPFS 79 | 80 | - `ipfs://bafybeifpaez32hlrz5tmr7scndxtjgw3auuloyuyxblynqmjw5saapewmu` 81 | - `scheme` - `ipfs` 82 | - `authority` - `bafybeifpaez32hlrz5tmr7scndxtjgw3auuloyuyxblynqmjw5saapewmu` 83 | - `path` - _(zero-length)_ 84 | - `package.json:main: "dist/index.js"` - `ipfs://bafybeifpaez32hlrz5tmr7scndxtjgw3auuloyuyxblynqmjw5saapewmu/dist/index.js` 85 | 86 | ## Copyright 87 | 88 | Copyright and related rights waived via [CC0](../LICENSE). 89 | -------------------------------------------------------------------------------- /SIPS/sip-14.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 14 3 | title: Dynamic Permissions 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/114 6 | author: Frederik Bolding (@frederikbolding) 7 | created: 2023-10-19 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes changes to the Snap manifest and new RPC methods that allows Snap developers to request additional permissions dynamically at runtime. This proposal outlines some of the details around this feature. 13 | 14 | ## Motivation 15 | 16 | Snaps currently have to request all permissions that they plan to use at install-time. This becomes a problem when a Snap wants to use many permissions as the installation experience suffers and the user has to either accept all permissions requested, or deny the installation. This proposal provides an improvement to the experience by letting Snaps request permissions at runtime as long as those permissions are statically defined in the manifest at build-time. 17 | 18 | ## Specification 19 | 20 | > Formal specifications are written in Typescript. 21 | 22 | ### Language 23 | 24 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 25 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 26 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 27 | 28 | ### Snap Manifest 29 | 30 | This SIP adds a new field to the Snap manifest called `dynamicPermissions`. 31 | This field can be used in tandem with the existing `initialPermissions`, but permissions in this field are not granted by installation: They MUST be requested when needed. The field follows the same format as `initialPermissions`. 32 | 33 | The new field can be specified as follows in a `snap.manifest.json` file: 34 | 35 | ```json 36 | { 37 | "initialPermissions": { 38 | "endowment:transaction-insight": {} 39 | }, 40 | "dynamicPermissions": { 41 | "snap_dialog": {}, 42 | "snap_getBip44Entropy": [ 43 | { 44 | "coinType": 1 45 | }, 46 | { 47 | "coinType": 3 48 | } 49 | ] 50 | } 51 | } 52 | ``` 53 | 54 | ### Permission caveats 55 | 56 | Duplicated permissions in `initialPermissions` and `dynamicPermissions` MUST NOT be allowed. A permission MUST only be able to exist in one of the manifest fields. 57 | 58 | Furthermore, permissions specified in `dynamicPermissions` MUST contain the caveats that will be requested at runtime and the permission request MUST fully match the caveats specified in the manifest. 59 | 60 | This MAY change in a future SIP. 61 | 62 | ### RPC Methods 63 | 64 | This SIP proposes the following RPC methods to manage the dynamic permissions: 65 | 66 | #### snap_requestPermissions 67 | 68 | This RPC method SHOULD function as a subset of the existing `wallet_requestPermissions` RPC method (as defined in [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255)) and take the same parameters and have the same return value. 69 | 70 | This RPC method MUST prompt the user to get consent for any requested permissions and MUST validate that the requested permissions are specified in the manifest before continuing its execution (including matching caveats). 71 | 72 | 73 | #### snap_getPermissions 74 | 75 | This RPC method SHOULD be an alias for `wallet_getPermissions`, and MAY be used by the Snap for verifying whether it already has the permissions needed for operating. The return value and parameters SHOULD match the existing specification defined in [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255). 76 | 77 | #### snap_revokePermissions 78 | 79 | This RPC method SHOULD take a similar input to `wallet_requestPermissions`, an object keyed with permission names, where the values may contain caveats if applicable. 80 | 81 | For example: 82 | 83 | ```json 84 | { 85 | "method": "snap_revokePermissions", 86 | "params": { 87 | "snap_getBip32Entropy": { 88 | "caveats": [ 89 | { 90 | "type": "permittedDerivationPaths", 91 | "value": [ 92 | { "path": ["m", "44'", "60'"], "curve": "secp256k1" }, 93 | { "path": ["m", "0'", "0'"], "curve": "ed25519" } 94 | ] 95 | } 96 | ] 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | The caveat information passed SHOULD be ignored in the initial implementation of this. Instead of processing the caveats, the implementation SHOULD revoke the entire permission key. We will revisit this at a later time to make it more granular. 103 | 104 | This RPC method SHOULD return `null` if the permissions are revoked successfully, or return an error otherwise. 105 | 106 | This RPC method MUST validate that the permissions to be revoked do not contain any permissions specified in `initialPermissions`. Only `dynamicPermissions` can be revoked. 107 | 108 | ## Copyright 109 | 110 | Copyright and related rights waived via [CC0](../LICENSE). 111 | -------------------------------------------------------------------------------- /assets/sip-9/snap.manifest.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | 4 | "title": "Manifest describing capabilities of a MetaMask snap", 5 | 6 | "type": "object", 7 | "required": [ 8 | "version", 9 | "proposedName", 10 | "description", 11 | "source", 12 | "initialPermissions", 13 | "manifestVersion" 14 | ], 15 | "properties": { 16 | "version": { 17 | "title": "Snap's version in SemVer format", 18 | "description": "Must be the same as in package.json", 19 | 20 | "type": "string" 21 | }, 22 | "proposedName": { 23 | "title": "Snap author's proposed name for the snap", 24 | "description": "The wallet may display this name in its user interface", 25 | 26 | "type": "string", 27 | "minLength": 1, 28 | "maxLength": 214 29 | }, 30 | "description": { 31 | "title": "A short description of snap", 32 | "description": "The wallet may display this description in its user interface", 33 | 34 | "type": "string", 35 | "minLength": 1, 36 | "maxLength": 280 37 | }, 38 | "repository": { 39 | "$comment": "Taken from https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/package.json", 40 | 41 | "title": "Git repository with snap's source code", 42 | "description": "Specify the place where your code lives. This is helpful for people who want to contribute.", 43 | 44 | "type": ["object", "string"], 45 | "properties": { 46 | "type": { 47 | "type": "string" 48 | }, 49 | "url": { 50 | "type": "string" 51 | }, 52 | "directory": { 53 | "type": "string" 54 | } 55 | } 56 | }, 57 | "source": { 58 | "title": "Snap's runtime files", 59 | "description": "Information needed to load and execute the snaps' code", 60 | 61 | "type": "object", 62 | "required": ["shasum", "location"], 63 | "properties": { 64 | "shasum": { 65 | "title": "Checksum of runtime files", 66 | "description": "Checksum composed from all the files that are loaded during runtime", 67 | 68 | "type": "string", 69 | "minLength": 44, 70 | "maxLength": 44, 71 | "pattern": "^[A-Za-z0-9+\\/]{43}=$" 72 | }, 73 | "location": { 74 | "title": "Location of the snap runtime code", 75 | "description": "Points to a single JavaScript file which will be run under Snaps sandbox", 76 | 77 | "type": "object", 78 | "additionalProperties": false, 79 | "required": ["npm"], 80 | "properties": { 81 | "npm": { 82 | "title": "Runtime file location in Npm", 83 | "description": "Information on how to locate the runtime file in an Npm package", 84 | 85 | "type": "object", 86 | "required": ["filePath", "packageName"], 87 | "properties": { 88 | "filePath": { 89 | "title": "Runtime code filepath", 90 | "description": "Unix-style location of the JavaScript source file relative to root of the Npm package", 91 | "type": "string" 92 | }, 93 | "packageName": { 94 | "$comment": "Taken from https://github.com/SchemaStore/schemastore/blob/master/src/schemas/json/package.json", 95 | 96 | "title": "Npm package name", 97 | "description": "The name of the npm package. Currently required to be the same one as the one containing the manifest.", 98 | "type": "string", 99 | "maxLength": 214, 100 | "minLength": 1, 101 | "pattern": "^(?:@[a-z0-9-*~][a-z0-9-*._~]*/)?[a-z0-9-~][a-z0-9-._~]*$" 102 | }, 103 | "iconPath": { 104 | "title": "Snap icon filepath", 105 | "description": "Optional path of .svg icon that will be shown to the user in the UI", 106 | 107 | "type": "string" 108 | }, 109 | "registry": { 110 | "title": "Npm registry", 111 | "description": "URL pointing to Npm registry used to load the package", 112 | 113 | "enum": [ 114 | "https://registry.npmjs.org", 115 | "https://registry.npmjs.org/" 116 | ] 117 | } 118 | } 119 | } 120 | } 121 | } 122 | } 123 | }, 124 | "initialPermissions": { 125 | "title": "Snap permissions", 126 | "description": "Permissions requested by the snap from the user", 127 | 128 | "type": "object" 129 | }, 130 | "manifestVersion": { 131 | "title": "snap.manifest.json version", 132 | "description": "The version of this file used to detect compatibility", 133 | "enum": ["0.1"] 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /SIPS/sip-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 1 3 | title: SIP Process 4 | status: Living 5 | author: Olaf Tomalka (@ritave), Erik Marks (@rekmarks) 6 | created: 2022-06-08 7 | updated: 2022-09-29 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP specified the process of working with proposals inside this repository. 13 | The goals of the process is participate with the community, but in a structured, engineering-focused, way. 14 | 15 | ### Language 16 | 17 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 18 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 19 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 20 | 21 | ## Workflow 22 | 23 | > A good starting point for a new SIP is the [SIP template](../sip-template.md). 24 | 25 | ### Stages 26 | 27 | ```mermaid 28 | stateDiagram-v2 29 | direction LR 30 | [*] --> Draft 31 | Draft --> Review 32 | Review --> Living 33 | Review --> Implementation 34 | Implementation --> Final 35 | Final --> [*] 36 | 37 | Draft --> Withdrawn 38 | Review --> Withdrawn 39 | Implementation --> Withdrawn 40 | Withdrawn --> [*] 41 | ``` 42 | 43 | - **Draft** - The initial SIP status, indicating that it is in development. The SIP will be merged into the repository after being properly formatted. Major changes to the contents of the SIP are expected. 44 | - **Review** - The SIP author(s) marked this SIP as ready for peer review. All members of community are encouraged to participate. Incremental changes to the contents of the SIP are expected. 45 | - **Implementation** - This SIP is being implemented. Only critical changes based on implementation experience are expected. 46 | - **Final** - The SIP is considered a final standard. No further updates except errata and clarifications will be considered. The SIP MUST be fully implemented before being considered for this status. 47 | - **Withdrawn** - The proposed SIP has been withdrawn by the SIP author(s) or will not be considered for inclusion. This status is final. If the idea is to be pursued again, a new proposal MUST be created. 48 | - **Living** - SIPs with this special status are considered continually updated and never final. Such SIPs MUST include the `updated` property in their front-matter. Specifically this includes [SIP-1](./sip-1.md). 49 | 50 | ## SIP Header Preamble 51 | 52 | Each SIP MUST begin with a header preamble in YAML format, preceded and followed by `---`. Such header is termed as [Front Matter by Jekyll](https://jekyllrb.com/docs/front-matter/). The headers MUST appear in following order: 53 | 54 | - `sip` - A unique number identifying the SIP, assigned by the SIP editors. 55 | - `title` - A descriptive name for the SIP. 56 | - `status` - The current status of the SIP. One of stages as described [above](#stages). 57 | - `discussions-to` - (Optional) A URL where discussions and review of the specified SIP can be found. 58 | - `author` - The list of authors in format described [below](#author-header). 59 | - `created` - The date the SIP was created on. 60 | - `updated` - (Optional) The date the SIP was updated on. SIPs with status `Living` MUST have this header. 61 | 62 | All dates in the preamble MUST be in [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format. 63 | 64 | #### `author` header 65 | 66 | A comma-separated list of the SIP authors. Each author MUST be written in the following format: `Name Surname (@github-username)`. If the author wants to maintain anonymity, they MAY provide only the username instead of their name and surname. The e-mail and github username are optional and MAY be provided. At least one of the authors MUST have a Github username provided in order to be notified of the changes concerning their SIP. 67 | 68 | Example `author` header: 69 | 70 | `John F. Kowalsky (@kowalsky), Gregory House, Anon (@anon)` 71 | 72 | ## Additional files 73 | 74 | The SIP MAY include additional files such as images and diagrams. Such files MUST be placed in `assets` folder in the following location `assets/SIP/sip-N/` where `N` is the SIP's number. When linking to files from SIPs, relative links MUST be used, such as `[schema](../assets/sip/sip-1/schema.json)`. 75 | 76 | ## Validation 77 | 78 | Every SIP MUST pass automatic validation when added in a pull request. You can manually use the validation tool as follows: 79 | 80 | ```bash 81 | cd tools/validate 82 | yarn install 83 | yarn build 84 | yarn validate '../../SIPS/**' 85 | ``` 86 | 87 | ## SIP Editors 88 | 89 | The role of SIP repository editors is to enforce the inclusion process. They do not judge the proposals on it's merits, it is up to the community to discuss. 90 | 91 | The current SIP editors, sorted alphabetically, are: 92 | 93 | - Christian Montoya ([@Montoya](https://github.com/Montoya)) 94 | - Hassan Malik ([@hmalik88](https://github.com/hmalik88)) 95 | - Olaf Tomalka ([@ritave](https://github.com/ritave)) 96 | - Ziad Saab ([@ziad-saab](https://github.com/ziad-saab)) 97 | 98 | At least one of the editors has to approve any incoming pull requests that update files in the [SIPs folder](./). 99 | 100 | ## History 101 | 102 | The Snaps Improvement Proposals have been inspired by [EIPs](https://github.com/ethereum/EIPs), [CAIPs](https://github.com/ChainAgnostic/CAIPs) and [TC39 Stages](https://tc39.es/process-document/). 103 | 104 | ## Copyright 105 | 106 | Copyright and related rights waived via [CC0](../LICENSE). 107 | -------------------------------------------------------------------------------- /SIPS/sip-23.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 23 3 | title: JSX for Snap interfaces 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/137 6 | author: Maarten Zuidhoorn (@mrtenz) 7 | created: 2024-03-22 8 | --- 9 | 10 | ## Abstract 11 | 12 | This proposal presents a specification for the use of JSX (JavaScript XML) in 13 | Snap interfaces. It includes a set of definitions, components, and rules for 14 | usage. The document delves into the details of various components such as 15 | `SnapElement`, `SnapComponent`, and `SnapNode`. It also elaborates on specific 16 | elements like , `Address`, `Bold`, `Button`, and `Text` among others, explaining 17 | their structure and purpose. 18 | 19 | Moreover, the proposal takes into account backward compatibility considerations. 20 | It outlines a systematic approach to translate the components from the previous 21 | SIP-7 format into the newly proposed format. 22 | 23 | The proposal aims to create a more robust, flexible, and versatile system for 24 | Snap interfaces. It strives to enhance the user experience and improve the 25 | efficiency of the system by offering a structured and standardised component 26 | framework. 27 | 28 | ## Motivation 29 | 30 | The motivation behind this proposal is to leverage JSX, a popular syntax 31 | extension for JavaScript, for designing and implementing Snap interfaces. JSX 32 | offers several advantages that make it a preferred choice among developers. 33 | Primarily, it allows for writing HTML-like syntax directly in the JavaScript 34 | code, which makes it more readable and intuitive. This facilitates easier 35 | development and maintenance of complex UI structures. 36 | 37 | Furthermore, JSX is universally recognised and widely adopted in the JavaScript 38 | community, especially within the React ecosystem. By using JSX for Snap 39 | interfaces, we enable a vast number of developers familiar with this syntax to 40 | contribute effectively in a shorter time frame. 41 | 42 | Adopting JSX also ensures better integration with modern development tools and 43 | practices. It allows for integration with linters, formatters, and type 44 | checkers, thus improving the development workflow. 45 | 46 | In summary, the use of JSX in Snap interfaces aims to improve developer 47 | experience, enhance code maintainability, and ultimately, lead to the creation 48 | of more robust and efficient Snap interfaces. 49 | 50 | ## Specification 51 | 52 | > Formal specifications are written in TypeScript. 53 | 54 | ### Language 55 | 56 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 57 | "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" written in 58 | uppercase in this document are to be interpreted as described in 59 | [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). 60 | 61 | ### Definitions 62 | 63 | #### Key 64 | 65 | A JSX key-like value, i.e., a `string`, `number`, or `null`. 66 | 67 | ```typescript 68 | type Key = string | number | null; 69 | ``` 70 | 71 | #### SnapElement 72 | 73 | A rendered JSX element, i.e., an object with a `type`, `props`, and `key`. 74 | 75 | ```typescript 76 | type SnapElement> = { 77 | type: string; 78 | props: Props; 79 | key: Key | null; 80 | }; 81 | ``` 82 | 83 | #### SnapComponent 84 | 85 | A JSX component, i.e., a function which accepts `props`, and returns a 86 | `SnapElement`. All components MUST accept a `key` property in addition to the 87 | regular props. 88 | 89 | ```typescript 90 | type SnapComponent> = 91 | (props: Props & { key: Key }) => SnapElement; 92 | ``` 93 | 94 | #### SnapNode 95 | 96 | A rendered JSX node, i.e., a `SnapElement`, `string`, `null`, or an array of 97 | `SnapNode`s. Note that HTML elements are not supported in Snap nodes. 98 | 99 | ```typescript 100 | type SnapNode = 101 | | SnapElement 102 | | string 103 | | null 104 | | SnapNode[]; 105 | ``` 106 | 107 | ### Interface structure 108 | 109 | The Snap interface structure is defined using JSX components, and consists of 110 | either: 111 | 112 | - A container element, `Container`, which wraps the entire interface. 113 | - A box element, `Box`, which contains the main content of the interface. 114 | - An optional footer element, `Footer`, which appears at the bottom of the 115 | interface. 116 | 117 | ![Snap interface structure](../assets/sip-23/interface-structure.png) 118 | 119 | Or: 120 | 121 | - A box element, `Box`, which contains the main content of the interface. 122 | 123 | #### Example 124 | 125 | Below is an example of a simple Snap interface structure using JSX components: 126 | 127 | ```typescript jsx 128 | 129 | 130 | My Snap 131 | Hello, world! 132 | 133 |
134 | 135 |
136 |
137 | ``` 138 | 139 | ### JSX runtime 140 | 141 | The JSX runtime is a set of functions that are used to render JSX elements, 142 | typically provided by a library like React. Since Snap interfaces are rendered 143 | in a custom environment, the JSX runtime MUST be provided by the Snaps platform. 144 | 145 | The Snaps JSX runtime only supports the modern JSX factory functions, i.e., 146 | `jsx` and `jsxs`. The runtime MUST NOT support the legacy `createElement` and 147 | `Fragment` functions. 148 | 149 | Both the `jsx` and `jsxs` functions MUST return a `SnapElement`. 150 | 151 | ## Backward compatibility 152 | 153 | To ensure backward compatibility with the previous SIP-7 format, the legacy 154 | components MUST be translated into the new JSX format. 155 | 156 | This SIP does not cover the translation process in detail, but simply outlines 157 | the components and rules for usage in the new format. All features and 158 | functionalities of the previous format are supported in the new format, so 159 | existing Snap interfaces can be easily translated into the new format. 160 | 161 | Most components in the new format have a one-to-one correspondence with the 162 | components in the previous format. The `Text` component in the new format 163 | replaces the Markdown syntax in the previous format, and the `Bold`, `Italic`, 164 | and `Link` components can be used to achieve similar effects. The `Box` 165 | component in the new format replaces the `panel` component in the previous 166 | format. 167 | 168 | ## Copyright 169 | 170 | Copyright and related rights waived via [CC0](../LICENSE). 171 | -------------------------------------------------------------------------------- /SIPS/sip-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 3 3 | title: Transaction Insights 4 | status: Final 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/31 6 | author: Hassan Malik (@hmalik88), Frederik Bolding (@frederikbolding) 7 | created: 2022-08-23 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes a way for snaps to provide "insights" into transactions that users are signing. These insights can then be displayed in the MetaMask confirmation UI, helping the user to make informed decisions about signing transactions. 13 | 14 | Example use cases for transaction insights are phishing detection, malicious contract detection and transaction simulation. 15 | 16 | ## Motivation 17 | 18 | One of the most difficult problems blockchain wallets solve for their users is "signature comprehension", i.e. making cryptographic signature inputs intelligible to the user. 19 | Blockchain transactions are signed before being submitted to a node, and constitute an important subset of this problem space. 20 | A single wallet may not be able to provide all relevant information to any given user for any given transaction. 21 | To alleviate this problem, this SIP aims to expand the kinds of information MetaMask provides to a user before signing a transaction. 22 | 23 | The current MetaMask extension already has a "transaction insights" feature that decodes transactions and displays the result to the user. 24 | To expand on this feature, this SIP allows the community to build snaps that provide arbitrary "insights" into transactions. 25 | These insights can then be displayed in the MetaMask UI alongside any information provided by MetaMask itself. 26 | 27 | ## Specification 28 | 29 | > Formal specifications are written in Typescript. Usage of `CAIP-N` specifications, where `N` is a number, are references to [Chain Agnostic Improvement Proposals](https://github.com/ChainAgnostic/CAIPs). 30 | 31 | ### Language 32 | 33 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 34 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 35 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 36 | 37 | ### Definitions 38 | 39 | > This section is non-normative, and merely recapitulates some definitions from [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md). 40 | 41 | - `ChainId` - a [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) string. 42 | It identifies a specific chain among all blockchains recognized by the CAIP standards. 43 | - `ChainId` consists of a `Namespace` and a `Reference` 44 | - `Namespace` - a class of similar blockchains. For example EVM-based blockchains. 45 | - `Reference` - a way to identify a concrete chain inside a `Namespace`. For example Ethereum Mainnet or one of its test networks. 46 | 47 | ### Snap Manifest 48 | 49 | This SIP specifies a permission named `endowment:transaction-insight`. 50 | The permission grants a snap read-only access to raw transaction payloads, before they are accepted for signing by the user. 51 | 52 | This permission is specified as follows in `snap.manifest.json` files: 53 | 54 | ```json 55 | { 56 | "initialPermissions": { 57 | "endowment:transaction-insight": {} 58 | } 59 | } 60 | ``` 61 | 62 | ### Snap Implementation 63 | 64 | Any snap that wishes to provide transaction insight features **MUST** implement the following API: 65 | 66 | ```typescript 67 | import { OnTransactionHandler } from "@metamask/snap-types"; 68 | 69 | export const onTransaction: OnTransactionHandler = async ({ 70 | transaction, 71 | chainId, 72 | }) => { 73 | const insights = /* Get insights */; 74 | return { insights }; 75 | }; 76 | ``` 77 | 78 | The interface for an `onTransaction` handler function’s arguments is: 79 | 80 | ```typescript 81 | interface OnTransactionArgs { 82 | transaction: Record; 83 | chainId: string; 84 | } 85 | ``` 86 | 87 | `transaction` - The transaction object is intentionally not defined in this SIP because different chains may specify different transaction formats. 88 | It is beyond the scope of the SIP standards to define interfaces for every chain. 89 | Instead, it is the Snap developer's responsibility to be cognizant of the shape of transaction objects for relevant chains. 90 | Nevertheless, you can refer to [Appendix I](#appendix-i-ethereum-transaction-objects) for the interfaces of the Ethereum transaction objects available in MetaMask at the time of this SIP's creation. 91 | 92 | `chainId` - This is a [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) `chainId` string. 93 | The snap is expected to parse and utilize this string as needed. 94 | 95 | The interface for the return value of an `onTransaction` export is: 96 | 97 | ```typescript 98 | interface OnTransactionReturn { 99 | insights: Record; 100 | } 101 | ``` 102 | 103 | ### MetaMask Integration 104 | 105 | The `insights` object returned by the snap will be displayed alongside the confirmation for the `transaction` that `onTransaction` was called with. 106 | Keys and values will be displayed in the order received, with each key rendered as a title and each value rendered as follows: 107 | 108 | - If the value is an array or an object, it will be rendered as text after being converted to a string. 109 | - If the value is neither an array nor an object, it will be rendered directly as text. 110 | 111 | ## Appendix I: Ethereum Transaction Objects 112 | 113 | The following transaction objects may appear for any `chainId` of `eip155:*` where `*` is some positive integer. 114 | This includes all Ethereum or "EVM-compatible" chains. 115 | As of the time of creation of this SIP, they are the only possible transaction objects for Ethereum chains. 116 | 117 | ### EIP-1559 118 | 119 | ```typescript 120 | interface TransactionObject { 121 | from: string; 122 | to: string; 123 | nonce: string; 124 | value: string; 125 | data: string; 126 | gas: string; 127 | maxFeePerGas: string; 128 | maxPriorityFeePerGas: string; 129 | type: string; 130 | estimateSuggested: string; 131 | estimateUsed: string; 132 | } 133 | ``` 134 | 135 | ### Legacy (non-EIP-1559) 136 | 137 | ```typescript 138 | interface LegacyTransactionObject { 139 | from: string; 140 | to: string; 141 | nonce: string; 142 | value: string; 143 | data: string; 144 | gas: string; 145 | gasPrice: string; 146 | type: string; 147 | estimateSuggested: string; 148 | estimateUsed: string; 149 | } 150 | ``` 151 | 152 | ## Copyright 153 | 154 | Copyright and related rights waived via [CC0](../LICENSE). 155 | -------------------------------------------------------------------------------- /SIPS/sip-9.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 9 3 | title: snap.manifest.json v0.1 4 | status: Final 5 | author: Erik Marks (@rekmarks), Olaf Tomalka (@ritave) 6 | created: 2022-11-07 7 | --- 8 | 9 | ## Abstract 10 | 11 | This document specifies version `0.1` of the Snaps manifest file, `snap.manifest.json`. 12 | 13 | ## Motivation 14 | 15 | The goal of this SIP is to supersede [Snaps Publishing Specification v0.1](https://github.com/MetaMask/specifications/blob/c226cbaca1deb83d3e85941d06fc7534ff972336/snaps/publishing.md), and move Snaps specifications into one place - Snaps Improvement Proposals. 16 | 17 | ## Specification 18 | 19 | > Indented sections like this are considered non-normative. 20 | 21 | Paths that traverse JSON objects are using [jq syntax](https://stedolan.github.io/jq/manual/#Basicfilters). 22 | 23 | ### Language 24 | 25 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 26 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 27 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 28 | 29 | ## Folder Structure 30 | 31 | > The published files of an example Snap published to npm under the package name `@metamask/example-snap` may look like this: 32 | > 33 | > ``` 34 | > example-snap/ 35 | > ├─ dist/ 36 | > │ ├─ bundle.js 37 | > ├─ package.json 38 | > ├─ snap.manifest.json 39 | > ``` 40 | 41 | The snap MUST contain both [`package.json`](#packagejson) and [`snap.manifest.json`](#snapmanifestjson) files in the root directory of the snap package. 42 | 43 | ### `package.json` 44 | 45 | The `package.json` file MUST adhere to [the requirements of npm](https://docs.npmjs.com/cli/v7/configuring-npm/package-json). 46 | 47 | ### `snap.manifest.json` 48 | 49 | > Note that the manifest intentionally does not contain any information explicitly identifying its author. 50 | > Author information should be verifiable out-of-band at the point of Snap installation, and is beyond the scope of this specification. 51 | 52 | - `snap.manifest.json` - The contents of the file MUST be a JSON object. 53 | 54 | - `.version` - MUST be a valid [SemVer][] version string and equal to the [corresponding `package.json` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#version). 55 | - `.proposedName` - MUST be a string less than or equal to 214 characters. 56 | The proposed name SHOULD be human-readable. 57 | 58 | > The snap's author proposed name for the snap. 59 | > 60 | > The Snap host application may display this name unmodified in its user interface. 61 | 62 | - `.description` - MUST be a non-empty string less than or equal to 280 characters. 63 | MAY differ from the [corresponding `package.json:.description` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#description-1) 64 | > A short description of the Snap. 65 | > 66 | > The Snap host application may display this description unmodified in its user interface. 67 | - `.repository` - MAY be omitted. If present, MUST be equal to the [corresponding `package.json:.repository` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#repository). 68 | - `.source` - MUST be a JSON object. 69 | - `.shasum` - MUST hash of the snap source file as specified in [Checksum](#checksum) paragraph. 70 | - `.location` - MUST be a JSON object. 71 | - `.npm` - MUST be a JSON object. 72 | - `.filePath` - MUST be a [Unix-style][unix filesystem] path relative to the package root directory pointing to the Snap source file. 73 | - `.packageName` - MUST be equal to the [`package.json:.name` field](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#name). 74 | - `.iconPath` - MAY be omitted. If present, MUST be a [Unix-style][unix filesystem] path relative to the package root directory pointing to an `.svg` file. 75 | - `.registry` - MUST be string `https://registry.npmjs.org`. 76 | - `.initialPermissions` - MUST be a valid [EIP-2255][] `wallet_requestPermissions` parameter object. 77 | > Specifies the initial permissions that will be requested when the Snap is added to the host application. 78 | - `.manifestVersion` - MUST be the string `0.1`. 79 | 80 | ### Checksum 81 | 82 | The checksum MUST be calculated using SHA-256 algorithm as specified in NIST's [FIPS PUB 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf). 83 | 84 | The checksum MUST be calculated over the file located under `snap.manifest.json:.source.location.npm.filePath` path and saved under `snap.manifest.json:.source.shasum` as Base64 field with exactly 44 characters. The Base64 character set MUST be `A-Z`, `a-z`, `0-9`, `+`, `/` with `=` used for padding. The padding MUST NOT be optional. 85 | 86 | ### Snap Source File 87 | 88 | > Represented in the [example](../assets/sip-9/example-snap/) as `dist/bundle.js`. The Snap "source" or "bundle" file can be named anything and kept anywhere in the package file hierarchy. 89 | 90 | The snap source file, located under `snap.manifest.json:.source.location.npm.filePath` path MUST: 91 | 92 | - have the `.js` file extension. 93 | - contain the entire source of the Snap program, including all dependencies. 94 | - execute under [SES][]. 95 | 96 | ## Test vectors 97 | 98 | ### Snap package 99 | 100 | > A full example snap package can be found in the [assets](../assets/sip-9/example-snap/). 101 | 102 | ### Manifest 103 | 104 | > A complete JSON Schema can be [found in the assets](../assets/sip-9/snap.manifest.schema.json). 105 | 106 | ### Checksum 107 | 108 | > The shashum was generated using `shasum -a 256 assets/sip-9/source.js | cut -d ' ' -f 1 | xxd -r -p | base64` command 109 | 110 | - [`assets/sip-9/source.js`](../assets/sip-9/source.js) - `x3coXGvZxPMsVCqPA1zr9SG/bw8SzrCPncClIClCfwA=` 111 | - `` - `47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=` 112 | 113 | ## Errata 114 | 115 | 2023-05-08: 116 | 117 | - Fix [JSON Schema](../assets/sip-9/snap.manifest.schema.json) having some properties at the wrong nesting level. 118 | - Fix [JSON Schema](../assets/sip-9/snap.manifest.schema.json) not supporting NPM registry with `/` suffix. 119 | - Added titles and descriptions to all properties in [JSON Schema](../assets/sip-9/snap.manifest.schema.json) which are required for suggestions in Visual Studio Code. 120 | 121 | ## Copyright 122 | 123 | Copyright and related rights waived via [CC0](../LICENSE). 124 | 125 | [eip-2255]: https://eips.ethereum.org/EIPS/eip-2255 126 | [semver]: https://semver.org/ 127 | [ses]: https://www.npmjs.com/package/ses 128 | [unix filesystem]: https://en.wikipedia.org/wiki/Unix_filesystem 129 | -------------------------------------------------------------------------------- /SIPS/sip-26.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 26 3 | title: Non-EVM protocol support 4 | status: Draft 5 | author: Daniel Rocha (@danroc), Frederik Bolding (@FrederikBolding), Alex Donesky (@adonesky1) 6 | created: 2024-08-28 7 | --- 8 | 9 | ## Abstract 10 | 11 | This SIP presents an architecture to enable Snaps to expose blockchain-specific 12 | methods to dapps, extending MetaMask's functionality to support a multichain 13 | ecosystem. 14 | 15 | ## Motivation 16 | 17 | Currently, MetaMask is limited to EVM-compatible networks. This proposal aims 18 | to empower developers, both first- and third-party, to use Snaps to add native 19 | support for non-EVM-compatible chains within MetaMask. 20 | 21 | ## Specification 22 | 23 | > Formal specifications are written in TypeScript. 24 | 25 | ### Language 26 | 27 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", 28 | "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" written 29 | in uppercase in this document are to be interpreted as described in [RFC 30 | 2119](https://www.ietf.org/rfc/rfc2119.txt). 31 | 32 | ### High-Level architecture 33 | 34 | The diagram below represents a high-level architecture of how the RPC Router 35 | integrates inside MetaMask to allow Snaps to expose protocol methods to dapps 36 | and the MetaMask clients. 37 | 38 | ![High-level architecture](../assets/sip-26/components-diagram.png) 39 | 40 | - **Account Snaps**: Snaps that implement the [Keyring API][keyring-api] and are responsible 41 | for signing requests and managing accounts. 42 | 43 | - **Protocol Snaps**: Snaps that implement protocol methods that do not require 44 | an account to be executed. 45 | 46 | - **RPC Router**: Native component that forwards RPC requests to the 47 | appropriate Protocol Snap or Account Snap. 48 | 49 | - **Keyring Controller**: Native component responsible for forwarding signing 50 | requests to the appropriate keyring implementation. 51 | 52 | - **Accounts Controller**: Native component responsible for managing accounts 53 | inside MetaMask. It stores all non-sensitive account information. 54 | 55 | - **Snaps Keyring**: Native component that acts as a bridge between the 56 | Keyring Controller and the Account Snaps. 57 | 58 | ### Components 59 | 60 | Here is a brief description of the components involved in this architecture 61 | which will require to be implemented or modified. 62 | 63 | #### RPC Router 64 | 65 | The RPC Router will be a new native component responsible for routing JSON-RPC 66 | requests to the appropriate Snap or keyring. 67 | 68 | To route a request, the RPC Router MUST extract the method name and [CAIP-2 chainId][caip-2] 69 | from the request object. It then determines whether the method is supported by 70 | a Protocol Snap or an Account Snap, with Account Snaps taking precedence over 71 | Protocol Snaps. 72 | 73 | If the method is supported by an Account Snap, the RPC Router forwards the 74 | request to the Keyring Controller; otherwise, it forwards the request to the 75 | appropriate Protocol Snap. 76 | 77 | #### Snaps Keyring 78 | 79 | The Snaps Keyring is an existing native component that exposes the 80 | [snap_manageAccounts][snap-manage-accs] method, allowing Account Snaps to 81 | register, remove, and update accounts. 82 | 83 | For example, this code can be used by an Account Snap to register a new account 84 | with the Account Router: 85 | 86 | ```typescript 87 | // This will notify the Account Router that a new account was created, and the 88 | // Account Router will register this account as available for signing requests 89 | // using the `eth_signTypedData_v4` method. 90 | await snap.request({ 91 | method: "snap_manageAccounts", 92 | params: { 93 | method: "notify:accountCreated", 94 | params: { 95 | account: { 96 | id: "74bb3393-f267-48ee-855a-2ba575291ab0", 97 | type: "eip155:eoa", 98 | address: "0x1234567890123456789012345678901234567890", 99 | methods: ["eth_signTypedData_v4"], 100 | options: {}, 101 | }, 102 | }, 103 | }, 104 | }); 105 | ``` 106 | 107 | Similar events are available to notify about the removal and update of 108 | accounts: `notify:accountRemoved` and `notify:accountUpdated`. 109 | 110 | Additionally, the Snaps Keyring expects the Account Snap to implement the 111 | Keyring API so it can forward signing requests to it through the 112 | [`keyring_submitRequest`][submit-request] method. 113 | 114 | #### Account Snaps 115 | 116 | As part of the Keyring API, non-EVM Account Snaps MUST also implement support 117 | for the `keyring_resolveAccountAddress` RPC method defined below. It is used 118 | by the RPC Router to extract the address of the account that should handle 119 | the signing request from the request object. 120 | 121 | ```typescript 122 | type ResolveAccountAddressRequest = { 123 | method: "keyring_resolveAccountAddress"; 124 | params: { 125 | scope: CaipChainId; 126 | request: JsonRpcRequest; 127 | }; 128 | }; 129 | ``` 130 | `scope` - The [CAIP-2][caip-2] chainId the request is targeting 131 | 132 | `request` - A `JsonRpcRequest` containing strictly JSON-serializable values. 133 | 134 | The implementation MUST return a value of the type `{ address: CaipAccountId }` or `null` (where `CaipAccountId` refers to a [CAIP-10][caip-10] identifier). 135 | 136 | #### Protocol Snaps 137 | 138 | Protocol Snaps implement and expose methods that do not require an account to 139 | execute and MUST list their supported methods and notifications in their manifest file: 140 | 141 | ```json5 142 | "initialPermissions": { 143 | "endowment:protocol": { 144 | "scopes": { 145 | "": { 146 | "methods": [ 147 | // List of supported methods 148 | ], 149 | "notifications": [ 150 | // List of supported notifications 151 | ] 152 | } 153 | } 154 | } 155 | } 156 | ``` 157 | 158 | Additionally protocol Snaps MUST implement the `onProtocolRequest` handler: 159 | 160 | ```typescript 161 | import { OnProtocolRequestHandler } from "@metamask/snap-sdk"; 162 | 163 | export const onProtocolRequest: OnProtocolRequestHandler = async ({ 164 | origin, 165 | scope, 166 | request, 167 | }) => { 168 | // Return protocol responses 169 | }; 170 | ``` 171 | 172 | The interface for an `onProtocolRequest` handler function’s arguments is: 173 | 174 | ```typescript 175 | interface OnProtocolRequestArguments { 176 | origin: string; 177 | scope: CaipChainId; 178 | request: JsonRpcRequest; 179 | } 180 | ``` 181 | 182 | `origin` - The origin making the protocol request (i.e. a dapp). 183 | 184 | `scope` - The [CAIP-2][caip-2] chainId the request is targeting. 185 | 186 | `request` - A `JsonRpcRequest` containing strictly JSON-serializable values. 187 | 188 | Any JSON-serializable value is allowed as the return value for `onProtocolRequest`. 189 | 190 | ## Copyright 191 | 192 | Copyright and related rights waived via [CC0](../LICENSE). 193 | 194 | [keyring-api]: https://github.com/MetaMask/accounts/tree/main/packages/keyring-api 195 | [snap-manage-accs]: https://docs.metamask.io/snaps/reference/snaps-api/#snap_manageaccounts 196 | [submit-request]: https://docs.metamask.io/snaps/reference/keyring-api/account-management/#keyring_submitrequest 197 | [caip-2]: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md 198 | [caip-10]: https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md 199 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /SIPS/sip-5.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 5 3 | title: Creating JSON Web Tokens (JWTs) 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/64 6 | author: Vid Kersic (@Vid201), Andraz Vrecko (@andyv09) 7 | created: 2022-10-20 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes a functionality of signing the data in the form of JSON Web Tokens (JWTs), exposed as an RPC method. 13 | 14 | JSON Web Tokens (JWTs) are an internet standard for creating data whose payload holds claims in the JSON. The standard is widely used in many applications on the web, often for authentication and authorization purposes. Other use cases are Verifiable Credentials (VCs) and Verifiable Presentations (VPs), also part of Decentralized Identity and Self-Sovereign Identity (SSI). 15 | 16 | ## Motivation 17 | 18 | Snaps were introduced to extend the functionalities of MetaMask to support more use cases and paradigms. MetaMask currently provides several RPC methods for digitally signing data (described [here](https://docs.metamask.io/guide/signing-data.html)), such as personal_sign and signTypedData. All methods add the prefix "\x19Ethereum Signed Message:\n" to the data, as described in [EIP 191](https://eips.ethereum.org/EIPS/eip-191) and [EIP 712](https://eips.ethereum.org/EIPS/eip-712), which prevents several attack vectors, most notably impersonating transactions. But this also prevents the ability to produce pure signatures over the data, which is needed for other internet data standards (such as JWTs). Actually, a pure signature is possible only with eth_sign, but that method was deprecated and advised not to use. Therefore, there is no safe way to create JWTs containing signatures signed by MetaMask accounts. This SIP proposes a new RPC method for Snaps that enables the creation of signed JWTs. 19 | 20 | ## Specification 21 | 22 | > Formal specifications are written in Typescript. 23 | 24 | ### Language 25 | 26 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 27 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 28 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 29 | 30 | ### Definitions 31 | 32 | JWTs are composed of three parts: header, payload, and signature. For more information, please check [JSON Web Token (JWT) Request for Comments (RFC)](https://www.rfc-editor.org/rfc/rfc7519). 33 | 34 | ### Snap Manifest 35 | 36 | This SIP specifies permission named `snap_signJwt`. This permission grants a snap the ability to create signed JWTs by introducing an additional signing method. 37 | 38 | This specification is specified as follows in `snap.manifest.json` files: 39 | ```typescript 40 | { 41 | "initialPermissions": { 42 | "snap_signJwt": {}, 43 | } 44 | } 45 | ``` 46 | 47 | ### Common Types 48 | 49 | The below common types are used throughout the specification. 50 | 51 | ```typescript 52 | interface JWTHeader { 53 | typ: string; 54 | alg: string; 55 | [params: string]: any; 56 | } 57 | 58 | interface JWTPayload { 59 | iss?: string; 60 | sub?: string; 61 | aud?: string | string[]; 62 | jti?: string; 63 | nbf?: number; 64 | exp?: number; 65 | iat?: number; 66 | [claims: string]: any; 67 | } 68 | ``` 69 | 70 | ### JWT Signing Implementation 71 | 72 | This implementation is modeled after implementations from libraries [did-jwt](https://github.com/decentralized-identity/did-jwt) and [jose](https://github.com/panva/jose). 73 | 74 | All hash functions that are implemented in the library [ethereum-cryptography](https://github.com/ethereum/js-ethereum-cryptography) are supported, e.g., SHA256, SHA512, and keccak256. 75 | 76 | ```typescript 77 | import * as u8a from "uint8arrays"; 78 | const { sha256 } = require("ethereum-cryptography/sha256"); 79 | const { keccak256 } = require("ethereum-cryptography/keccak"); 80 | // import other hash functions 81 | const { utf8ToBytes } = require("ethereum-cryptography/utils"); 82 | 83 | function hexToBytes(s: string): Uint8Array { 84 | const input = s.startsWith("0x") ? s.substring(2) : s; 85 | return u8a.fromString(input.toLowerCase(), "base16"); 86 | } 87 | 88 | function bytesToBase64url(b: Uint8Array): string { 89 | return u8a.toString(b, "base64url"); 90 | } 91 | 92 | function encodeSection(data: any): string { 93 | return encodeBase64url(JSON.stringify(data)); 94 | } 95 | 96 | export function encodeBase64url(s: string): string { 97 | return bytesToBase64url(u8a.fromString(s)); 98 | } 99 | 100 | export async function createJWT( 101 | header: Partial = {}, 102 | payload: Partial = {}, 103 | address: string, 104 | hashFunction: string 105 | ): Promise { 106 | if (!header.typ) header.typ = "JWT"; 107 | 108 | const encodedPayload = (typeof payload === "string") ? payload : encodeSection(payload); 109 | const signingInput: string = [encodeSection(header), encodedPayload].join( 110 | "." 111 | ); 112 | const bytes: Uint8Array = utf8ToBytes(signingInput); 113 | 114 | let hash: Uint8Array; 115 | 116 | switch(hashFunction) { 117 | case 'sha256': 118 | hash = sha256(bytes); 119 | break; 120 | case 'keccak256': 121 | hash = keccak256(bytes); 122 | break; 123 | ... // other hash functions 124 | } 125 | 126 | let signature = sign(hash); // Function sign can be the same as the MetaMask RPC method eth_sign. The header and payload that will be signed MUST be shown to the user. 127 | signature = hexToBytes(signature.slice(0, -2)); // remove byte appended by MetaMask 128 | const encodedSignature = bytesToBase64url(signature); 129 | 130 | return [signingInput, encodedSignature].join("."); 131 | } 132 | ``` 133 | 134 | ### Snap Implementation 135 | 136 | Any snap that needs the capability of creating JWTs can do that by calling the JSON-RPC method in the following way: 137 | 138 | ```typescript 139 | const jwt = await wallet.request({ 140 | method: 'snap_signJwt', 141 | params: [ 142 | { 143 | header: 144 | { 145 | "alg": "ES256K", 146 | "typ": "JWT" 147 | }, 148 | payload: 149 | { 150 | "iss": "Company", 151 | "sub": "Alice", 152 | "role": "employee" 153 | }, 154 | address: '0x12345...', 155 | hashFunction: 'sha256' 156 | }, 157 | ], 158 | }); 159 | ``` 160 | 161 | ## Appendix I: JSON Web Tokens (JWTs) 162 | 163 | Example of signed JWT: 164 | 165 | ```eyJhbGciOiJFUzI1NksiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJDb21wYW55Iiwic3ViIjoiQWxpY2UiLCJyb2xlIjoiZW1wbG95ZWUifQ.E_QJXlLHIgO6xifadQRcsPty2LounknXq_O7HK3c1kZ0jGAG0pXgyAmkjqvpBtLsLNLonj3ilrrUEe5I_n9Clw``` 166 | 167 | In the example above, the header contains fields ``alg`` and ``typ``. Algorithm ``ES256K`` uses the elliptic curve ``secp256k1`` (used in Ethereum) and hash function SHA256. 168 | 169 | ```json 170 | { 171 | "alg": "ES256K", 172 | "typ": "JWT" 173 | } 174 | ``` 175 | 176 | Payload is a simple JSON object: 177 | 178 | ```json 179 | { 180 | "iss": "Company", 181 | "sub": "Alice", 182 | "role": "employee" 183 | } 184 | ``` 185 | 186 | ## Appendix II: Suggested Alternative Approach 187 | 188 | There is another alternative approach for creating JWTs, which does not require the RPC signing method provided by MetaMask. Any Snap can already retrieve a private key for any account using the RPC method ``snap_getBip44Entropy``, which can be used to create signed JWTs (and for any other custom signing solution) - but this requires Snap to ask the user for special permission. 189 | 190 | ## Copyright 191 | 192 | Copyright and related rights waived via [CC0](../LICENSE). -------------------------------------------------------------------------------- /SIPS/sip-4.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 4 3 | title: Merging of snap.manifest.json and package.json 4 | status: Withdrawn 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/51 6 | author: Olaf Tomalka (@ritave) 7 | created: 2022-09-23 8 | --- 9 | 10 | ## Abstract 11 | 12 | A proposed specification of a new `package.json` file that includes all the data needed to execute the snap, removing `snap.manifest.json` altogether. 13 | 14 | This SIP intends to supersede [Snaps Publishing Specification v0.1](https://github.com/MetaMask/specifications/blob/c226cbaca1deb83d3e85941d06fc7534ff972336/snaps/publishing.md). 15 | 16 | ## Motivation 17 | 18 | There are multiple fields that are the same in `package.json` files and `snap.manifest.json` files that routinely become desynchronized. 19 | Visual Studio Code adds it's own properties to the package.json successfully. Merging those two files will be beneficial for the developers while still allowing other snap sources outside of NPM. 20 | 21 | Because this SIP intends to supersede the previous specification, all behavior has been re-specified to serve as a singular source of truth. 22 | 23 | ## Specification 24 | 25 | > Such sections are non-normative 26 | 27 | ### Language 28 | 29 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 30 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 31 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 32 | 33 | ### package.json 34 | 35 | > The full JSON Schema for snap's package.json [can be found in the assets](../assets/sip-4/package.schema.json). 36 | 37 | The snap's `package.json` MUST adhere to `package.json` schema as [defined by the NPM organization](https://docs.npmjs.com/cli/v8/configuring-npm/package-json). That schema is extended with the following behavior: 38 | 39 | - `package.json` 40 | - `.description` - The wallet MAY use this field to display information about the snap to the user. 41 | - `.version` - MUST be a valid [SemVer](https://semver.org/spec/v2.0.0.html) string. DApps MAY request specific version ranges. If a mismatch occurs between requested version range and the version inside the fetched snap, that snap MUST NOT be installed. 42 | > Additionally, wallet MAY allow updating snaps to the requested version in some schemes. 43 | > For example, if the installed snap is `npm:my-snap@1.0.0` while the dapp requests `npm:my-snap@^2.0.0`, the wallet will try to get the newest version from [npm](https://npmjs.com) and will update the snap a version that satisfies `^2.0.0`. 44 | - `.main` - Filepath relative to `package.json` with location of the snaps bundled source code to be executed. 45 | - `.engines` - The wallet SHALL introduce `snaps` engine which will follow semver versioning. If the requested SemVer is not satisfied by the extension run by the end-user, the snap MUST NOT be executed. 46 | - The first version of `snaps` engine after implementing this SIP SHALL be `1.0.0`. 47 | - The MetaMask team SHALL adhere to the following social contract: 48 | - Any breaking changes to the API or changes to the `package.json` that require all snaps to update SHALL update the `major` part (`1.0.0` -> `2.0.0`). 49 | - Any new backwards-compatible new features SHALL update the `minor` part (`1.0.0` -> `1.1.0`). 50 | - Any bug fixes SHALL update the `patch` part (`1.0.0` -> `1.0.1`). 51 | - `.snap` - MUST exist and MUST be a JSON object with snap specific metadata. 52 | - `.snap.proposedName` - MUST exist. User readable name that the wallet MAY show in the UI. The name MUST be shorter or equal to 214 characters. 53 | - `.snap.permissions` - MUST exist. Permissions that the snap is requesting. 54 | - `.snap.checksum` - MUST exist and MUST be a JSON object specified below. The checksum in package.json MUST match the checksum of the code executed by the wallet. 55 | - `.snap.checksum.algorithm` - The algorithm field MUST be `sha-256`. 56 | - `.snap.checksum.hash` - The resulting checksum hash as described in [Checksum](#checksum) paragraph. 57 | - `.snap.icon` - MAY exist. The location of the icon that the wallet MAY use to identify the snap to the user in the UI. The icon MUST be in SVG file format. 58 | 59 | ### Checksum 60 | 61 | The checksum SHALL be calculated using SHA-256 algorithm as specified in NIST's [FIPS PUB 180-4](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf). 62 | 63 | The checksum SHALL be calculated over the file located under `package.json:.main` path and saved under `package.json:.snap.checksum.hash` as Base64 field with 44 characters. The Base64 character set MUST be `A-Z`, `a-z`, `0-9`, `+`, `/` with `=` used for padding. 64 | 65 | ## Test vectors 66 | 67 | ## `package.json` 68 | 69 | > You can find an example [`package.json` in the assets](../assets/sip-4/package.json). 70 | 71 | ## Checksum 72 | 73 | > The shashum was generated using `shasum -a 256 assets/sip-4/source.js | cut -d ' ' -f 1 | xxd -r -p | base64` command 74 | 75 | - [`assets/sip-4/source.js`](../assets/sip-4/source.js) - `x3coXGvZxPMsVCqPA1zr9SG/bw8SzrCPncClIClCfwA=` 76 | - `` - `47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=` 77 | 78 | ## Backwards compatibility 79 | 80 | This SIP intends to break backwards compatibility. We propose that MetaMask Flask follows our standard practice of informing the community of deprecation for one version of Flask and remove the old way altogether in the following one. 81 | 82 | List of breaking changes: 83 | 84 | - Removal of `snap.manifest.json` and reliance on `package.json` 85 | 86 | ### Potential incompatibilities 87 | 88 | While most of the properties of the files are interchangeable in the data they can represent, aside from the data structure, we've singled out `package.json:main` / `snap.manifest.json:source.location` as a potentially incompatible. `package.json` assumes that the bundle is included along with the `package.json` in one directory, while `snap.manifest.json` does not make that assumption. This means that `snap.manifest.json` can live in one location while source files in the second. 89 | 90 | We haven't identified any cases where that separation would be useful. NPM, IPFS, http and local all have concepts of directories that can be leveraged, and thus this SIP proposes that we remove the distinction of different locations for the manifest and the source. 91 | 92 | ## Appendix I: Identifying required data 93 | 94 | This is the minimal set of information we need to know about a snap to manage it properly and location of where it currently lives at the time of writing this SIP. 95 | 96 | - _Duplicate_ 97 | - `snap.manifest.json:description` / `package.json:description` - User readable description of the snap. 98 | - `snap.manifest.json:version` / `package.json:version` - Version of the snap. 99 | - `snap.manifest.json:repository` / `package.json:repository` - The location of the source code. 100 | - `snap.manifest.json:manifestVersion` / `package.json:engines` - The version of the metamask for which the snap was written for. 101 | - `snap.manifest.json:source.location` / `package.json:main` - The location of the bundle file. 102 | - _`snap.manifest.json`_ 103 | - `snap.manifest.json:proposedName` - The user readable name shown in the UI of the wallet. 104 | - `snap.manifest.json:initialPermissions` - Permissions that the snap is requesting from the user. 105 | - `snap.manifest.json:shasum` - The shashum of the bundled source required for security purposes. 106 | - `snap.manifest.json:source.location.npm.iconPath` - _(optional)_ The location of the icon file that represents the snap in the UI of the wallet. 107 | 108 | ## Copyright 109 | 110 | Copyright and related rights waived via [CC0](../LICENSE). 111 | -------------------------------------------------------------------------------- /SIPS/sip-12.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 12 3 | title: Domain Resolution 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/103 6 | author: Hassan Malik (@hmalik88) 7 | created: 2023-07-18 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes a new endowment, `endowment:name-lookup`, that enables a way for snaps to resolve a `domain` and `address` to their respective counterparts. 13 | 14 | ## Motivation 15 | 16 | Currently, the MetaMask wallet allows for ENS domain resolution. 17 | The implementation is hardcoded and limited to just the ENS protocol. 18 | In an effort to increasingly modularize the wallet and allow for resolution beyond ENS, we decided to open up domain/address resolution to snaps. 19 | A snap would be able to provide resolutions based on a domain or address provided with a chain ID. 20 | The address resolution is in essence "reverse resolution". 21 | The functionality provided by this API is also beneficial as a base layer for a petname system (**see definition**). With plans to bring petnames to MetaMask, resolutions would be fed into the petname system and used as a means for cache invalidation. 22 | 23 | ## Specification 24 | 25 | > Formal specifications are written in Typescript. Usage of `CAIP-N` specifications, where `N` is a number, are references to [Chain Agnostic Improvement Proposals](https://github.com/ChainAgnostic/CAIPs). 26 | 27 | ### Language 28 | 29 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 30 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 31 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 32 | 33 | ### Definitions 34 | 35 | > This section is non-normative, and merely recapitulates some definitions from [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md). 36 | 37 | - `ChainId` - a [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) string. 38 | It identifies a specific chain among all blockchains recognized by the CAIP standards. 39 | - `ChainId` consists of a `Namespace` and a `Reference` 40 | - `Namespace` - a class of similar blockchains. For example EVM-based blockchains. 41 | - `Reference` - a way to identify a concrete chain inside a `Namespace`. For example Ethereum Mainnet or one of its test networks. 42 | 43 | - `AccountAddress` - The account address portion of a [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-10.md) ID. 44 | 45 | - `Petname system` - A naming system that finds balance between global, collision-free and memorable names. Please see this [article](http://www.skyhunter.com/marcs/petnames/IntroPetNames.html) on this topic. 46 | 47 | ### Snap Manifest 48 | 49 | This SIP specifies a permission named `endowment:name-lookup`. 50 | The permission grants a snap the ability to expose an `onNameLookup` export that receives an object with `chainId` and `domain` OR `address` fields. 51 | 52 | This permission is specified as follows in `snap.manifest.json` files: 53 | 54 | ```json 55 | { 56 | "initialPermissions": { 57 | "endowment:name-lookup": { 58 | "chains": ["eip155:1", "bip122:000000000019d6689c085ae165831e93"], 59 | "matchers": { "tlds": ["lens"], "schemes": ["farcaster"] } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | `chains` - An optional non-empty array of CAIP-2 chain IDs that the snap supports. This field is useful for a client in order to avoid unnecessary overhead. 66 | 67 | `matchers` - An optional non-empty object that MUST contain 1 or both of the below properties. These matchers are useful for a client for validating input for domain resolution, also helpful in reducing overhead. 68 | 1. `tlds` - An optional non-empty array of top level domains that the snap will provide resolution for. 69 | 70 | 2. `schemes` - An optional non-empty array of prefixes that the snap expects for non-tld domain lookup. 71 | 72 | **Note:** TLD domains are presumed to end with "." and one of the `tlds`. Non-tld domains are presumed to start with one of the `schemes` followed by ":" then the domain. Respectively, an example of each would be `hassan.lens` and `farcaster:hbm88`. 73 | 74 | ### Snap Implementation 75 | 76 | Please see below for an example implementation of the API: 77 | 78 | ```typescript 79 | import { OnNameLookupHandler } from "@metamask/snap-types"; 80 | 81 | export const onNameLookup: OnNameLookupHandler = async ({ 82 | chainId, 83 | domain, 84 | address 85 | }) => { 86 | let resolution; 87 | 88 | if (domain) { 89 | 90 | resolution = { protocol: /* Domain protocol */ , resolvedAddress: /* Get domain resolution */, domainName: /* Domain name that the resolved address matches against */ }; 91 | return { resolvedAddresses: [resolution] }; 92 | } 93 | 94 | if (address) { 95 | resolution = { protocol: /* Domain protocol */, resolvedDomain: /* Get address resolution */ }; 96 | return { resolvedDomains: [resolution] }; 97 | } 98 | 99 | return null; 100 | }; 101 | ``` 102 | 103 | The type for an `onNameLookup` handler function's arguments is: 104 | 105 | ```typescript 106 | type OnNameLookupBaseArgs = { 107 | chainId: ChainId 108 | } 109 | 110 | type DomainLookupArgs = OnNameLookUpBaseArgs & { domain: string; address?: never }; 111 | type AddressLookupArgs = OnNameLookUpBaseArgs & { address: string; domain?: never }; 112 | 113 | type OnNameLookupArgs = DomainLookupArgs | AddressLookupArgs; 114 | 115 | ``` 116 | 117 | `chainId` - This is a [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) `chainId` string. 118 | The snap is expected to parse and utilize this string as needed. 119 | 120 | `domain` - This is a human-readable address. If the `domain` property is defined, the request is looking for resolution to an address. 121 | 122 | `address` - This is a non-readable address, this should be the native address format of the currently selected chain/protocol. If the `address` property is defined, 123 | the request is looking for resolution to a domain. 124 | 125 | The interface for the return value of an `onNameLookup` export is: 126 | 127 | ```typescript 128 | type AddressResolution = { 129 | protocol: string; 130 | resolvedAddress: AccountAddress; 131 | domainName: string; 132 | }; 133 | 134 | type DomainResolution = { 135 | protocol: string; 136 | resolvedDomain: string; 137 | }; 138 | 139 | type OnNameLookupResponse = 140 | | { 141 | resolvedAddresses: NonEmptyArray; 142 | resolvedDomains?: never; 143 | } 144 | | { resolvedDomains: NonEmptyArray; resolvedAddresses?: never } 145 | | null; 146 | ``` 147 | 148 | **Note:** 149 | 1. The `resolvedDomain` or `resolvedAddress` in a resolution object MUST be the key that the address or domain being queried is indexed by in the protocol that the snap is resolving for. These returned values are un-opinionated at the API layer to allow the client to use them as they see fit. 150 | 2. There MUST NOT be duplicate resolutions for the same protocol in either `resolvedAddresses` or `resolvedDomains`. 151 | 3. `protocol` refers to the name of the protocol providing resolution for said `resolvedAddress`/`resolvedDomain`. 152 | 4. For address resolutions, the `domainName` property pertains to the domain that the address was resolved for. In most instances this will be just the `domain` passed into the lookup handler. However, the requirement of this property allows a snap to make fuzzy matches and indicate the domain name that was fuzzy matched in the response. 153 | 5. The returned resolution(s) MUST exist on the chain specificed by the `chainId` passed into the handler. 154 | 155 | ## Copyright 156 | 157 | Copyright and related rights waived via [CC0](../LICENSE). 158 | -------------------------------------------------------------------------------- /SIPS/sip-30.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 30 3 | title: Entropy Source Identifiers 4 | status: Draft 5 | author: Shane T Odlum (@shane-t) 6 | created: 2024-12-17 7 | --- 8 | 9 | ## Abstract 10 | 11 | This SIP proposes additions to entropy retrieval APIs that allows snaps to request entropy from a specific source. 12 | 13 | ## Motivation 14 | 15 | Interoperability snaps and account management snaps use the methods `snap_getEntropy`, `snap_getBip44Entropy`, `snap_getBip32Entropy`, and `snap_getBip32PublicKey` to generate addresses and other key material. 16 | 17 | These methods assume the client contains a single entropy source (the user's primary keyring mnemonic). The proposed API changes will allow snaps to request entropy from a specific source such as a secondary mnemonic. A new method `snap_listEntropySources` will be added to allow snaps to request a list of available entropy sources. 18 | 19 | ## Specification 20 | 21 | > Formal specifications are written in TypeScript. 22 | 23 | ### Language 24 | 25 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", 26 | "NOT RECOMMENDED", "MAY", and "OPTIONAL" written in uppercase in this document are to be interpreted as described in 27 | [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 28 | 29 | ### Common Types 30 | 31 | ```typescript 32 | type SLIP10Node = { 33 | depth: number; 34 | parentFingerprint: number; 35 | index: number; 36 | privateKey: string; 37 | publicKey: string; 38 | chainCode: string; 39 | curve: "ed25519" | "ed25519Bip32" | "secp256k1"; 40 | }; 41 | 42 | export type BIP44Node = { 43 | coin_type: number; 44 | depth: number; 45 | privateKey: string; 46 | publicKey: string; 47 | chainCode: string; 48 | path: string[]; 49 | }; 50 | 51 | export type EntropySource = { 52 | name: string; 53 | id: string; 54 | type: "mnemonic"; 55 | primary: boolean; 56 | } 57 | ``` 58 | 59 | ### Scope 60 | 61 | This SIP applies to snaps that implement the [Keyring API][keyring-api] and any others which use the `snap_getEntropy`, `snap_getBip44Entropy`, `snap_getBip32Entropy`, and `snap_getBip32PublicKey` methods. 62 | 63 | ### Snap Manifest 64 | 65 | No changes are required to the snap manifest. 66 | 67 | ### Client Implementation 68 | 69 | #### Entropy Sources 70 | 71 | Permission to request `snap_listEntropySources` is endowed on any snap that has the permissions `snap_getEntropy`, `snap_getBip44Entropy`, `snap_getBip32Entropy` and/or `snap_getBip32PublicKey`. 72 | 73 | If a snap requests a list of available entropy sources, the client MUST return a list of `EntropySource` objects. 74 | 75 | The client MUST have a primary entropy source, which is used when no source is specified. In the list of available entropy sources, the primary source MUST be marked as `primary: true`. 76 | 77 | #### Handling Entropy Requests 78 | 79 | If a snap requests entropy and includes the `source` parameter for an entropy source of type `mnemonic`, the client MUST return entropy corresponding to that source, if it exists. 80 | 81 | If the source does not exist, the client MUST respond with an error. 82 | 83 | If the request does not include the `source` parameter, the client MUST return entropy from the primary source. 84 | 85 | #### Creating Accounts 86 | 87 | A client MAY invoke the `keyring.createAccount` method with an `entropySource` parameter in the `options` object. 88 | 89 | The `entropySource` parameter MUST be a string which uniquely identifies the entropy source to use. It is not guaranteed to be the same string visible to any other snap, but should always refer to the same source in the context of interactions between the snap and the client. 90 | 91 | ### Snap Implementation 92 | 93 | If a snap is asked to create an account via `keyring.createAccount`, and the `entropySource` parameter is provided, and the snap requires entropy to create an account, the snap SHOULD request the entropy from the specified source. 94 | 95 | ### New RPC Methods 96 | 97 | #### `snap_listEntropySources` 98 | 99 | The method returns an array of `EntropySource` objects, each representing an available entropy source (including the primary source). The Snap MAY choose to display this list to the user. 100 | 101 | ```typescript 102 | const entropySources = await snap.request({ 103 | method: "snap_listEntropySources", 104 | }); 105 | // [ 106 | // { name: "Phrase 1", id: "phrase-1" }, 107 | // { name: "Phrase 2", id: "phrase-2" }, 108 | // ] 109 | ``` 110 | 111 | ### Existing RPC Methods 112 | 113 | #### `snap_getEntropy` 114 | 115 | ##### Parameters 116 | 117 | An object containing: 118 | 119 | - `version` - The number 1. 120 | - `salt` (optional) - An arbitrary string to be used as a salt for the entropy. This can be used to generate different entropy for different purposes. 121 | - `source` (optional) - The ID of the entropy source to use. If not specified, the primary entropy source will be used. 122 | 123 | #### Returns 124 | 125 | The entropy as a hexadecimal string. 126 | 127 | #### Example 128 | 129 | ```typescript 130 | const entropy = await snap.request({ 131 | method: "snap_getEntropy", 132 | params: { 133 | version: 1, 134 | salt: "my-salt", 135 | source: "1234-5678-9012-3456-7890", 136 | }, 137 | }); 138 | // '0x1234567890abcdef' 139 | ``` 140 | 141 | #### `snap_getBip32Entropy` 142 | 143 | ##### Parameters 144 | 145 | - `path` - An array starting with `m` containing the BIP-32 derivation path of the key to retrieve. 146 | - `curve` - The curve to use - `ed25519`, `ed25519Bip32` or `secp256k1`. 147 | - `source` (optional) - The ID of the entropy source to use. If not specified, the primary entropy source will be used. 148 | 149 | ##### Returns 150 | 151 | A `SLIP10Node` object representing the BIP-32 HD tree node and containing its corresponding key material. 152 | 153 | ##### Example 154 | 155 | ```typescript 156 | const node = await snap.request({ 157 | method: "snap_getBip32Entropy", 158 | params: { 159 | path: ["m", "44", "0", "0", "0"], 160 | source: "1234-5678-9012-3456-7890", 161 | curve: "secp256k1", 162 | }, 163 | }); 164 | // { 165 | // depth: 5, 166 | // parentFingerprint: 1234567890, 167 | // index: 0, 168 | // privateKey: '0x1234567890abcdef', 169 | // publicKey: '0x1234567890abcdef', 170 | // chainCode: '0x1234567890abcdef', 171 | // curve: 'secp256k1', 172 | // } 173 | ``` 174 | 175 | #### `snap_getBip32PublicKey` 176 | 177 | ##### Parameters 178 | 179 | - `path` An array starting with `m` containing the BIP-32 derivation path of the key to retrieve. 180 | - `curve` - The curve to use - `ed25519` or `ed25519Bip32`, `secp256k1`. 181 | - `compressed` (optional) - Whether to return the public key in compressed format. (defaults to `false`) 182 | - `source` (optional) - The ID of the entropy source to use. If not specified, the primary entropy source will be used. 183 | 184 | ##### Returns 185 | 186 | The public key as a hexadecimal string. 187 | 188 | ##### Example 189 | 190 | ```typescript 191 | const publicKey = await snap.request({ 192 | method: "snap_getBip32PublicKey", 193 | params: { 194 | path: ["m", "44", "0", "0", "0"], 195 | source: "1234-5678-9012-3456-7890", 196 | curve: "secp256k1", 197 | compressed: true, 198 | }, 199 | }); 200 | // '0x1234567890abcdef' 201 | ``` 202 | 203 | #### `snap_getBip44Entropy` 204 | 205 | ##### Parameters 206 | 207 | An object containing: 208 | 209 | - `coin_type` - The BIP-44 coin type value of the node. 210 | - `source` (optional) - The ID of the entropy source to use. If not specified, the primary entropy source will be used. 211 | 212 | ##### Returns 213 | 214 | A `BIP44Node` object representing the BIP-44 `coin_type` HD tree node and containing its corresponding key material. 215 | 216 | ##### Example 217 | 218 | ```typescript 219 | const node = await snap.request({ 220 | method: "snap_getBip44Entropy", 221 | params: { 222 | coin_type: 1, 223 | source: "1234-5678-9012-3456-7890", 224 | }, 225 | }); 226 | // { 227 | // coin_type: 1, 228 | // depth: 5, 229 | // privateKey: '0x1234567890abcdef', 230 | // publicKey: '0x1234567890abcdef', 231 | // chainCode: '0x1234567890abcdef', 232 | // path: ['m', '44', '0', '0', '0'], 233 | // } 234 | ``` 235 | 236 | ## Copyright 237 | 238 | Copyright and related rights waived via [CC0](../LICENSE). 239 | 240 | [keyring-api]: https://github.com/MetaMask/accounts/tree/main/packages/keyring-api -------------------------------------------------------------------------------- /SIPS/sip-16.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 16 3 | title: Signature Insights 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/118 6 | author: Christian Montoya (@Montoya), Hassan Malik (@hmalik88) 7 | created: 2023-11-01 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP proposes a way for Snaps to provide "insights" into signatures requested by dapps. These insights can then be displayed in the MetaMask confirmation UI, helping the user to make informed decisions before signing messages. 13 | 14 | Example use cases for signature insights are phishing detection, scam prevention and signature simulation. 15 | 16 | This SIP closely follows [SIP-3: Transaction Insights](sip-3.md) and [SIP-11: Transaction insight severity levels](sip-11.md). 17 | 18 | ## Motivation 19 | 20 | One of the most difficult problems blockchain wallets solve for their users is "signature comprehension," i.e. making cryptographic signature inputs intelligible to the user. 21 | A signature in the wrong hands can give an attacker the ability to steal user assets. 22 | A single wallet may not be able to provide all relevant information to any given user for any given signature request. 23 | To alleviate this problem, this SIP aims to expand the kinds of information MetaMask provides to a user before signing a message. 24 | 25 | The current Snaps API in the MetaMask extension already has a "transaction insights" feature that allows Snaps to decode transactions and provide insights to users. 26 | These transaction insights can also specify a transaction severity level to provide extra friction in the transaction confirmation. 27 | To expand on this feature, this SIP allows the community to build Snaps that provide arbitrary "insights" into signatures in a similar manner. 28 | These insights can then be displayed in the MetaMask UI alongside any information provided by MetaMask itself. 29 | Signature insight Snaps can also return a `severity` key to indicate the severity level of the content being returned. 30 | This key uses the same `SeverityLevel` enum used by transaction insight Snaps. 31 | 32 | ## Specification 33 | 34 | > Formal specifications are written in Typescript. Usage of `CAIP-N` specifications, where `N` is a number, are references to [Chain Agnostic Improvement Proposals](https://github.com/ChainAgnostic/CAIPs). 35 | 36 | ### Language 37 | 38 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 39 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 40 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 41 | 42 | ### Definitions 43 | 44 | > This section is non-normative, and merely recapitulates some definitions from [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md). 45 | 46 | - `ChainId` - a [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-2.md) string. 47 | It identifies a specific chain among all blockchains recognized by the CAIP standards. The ChainId refers to the network that the signing account is connected to. It is not necessarily the network where the signed message will be broadcast. 48 | - `ChainId` consists of a `Namespace` and a `Reference` 49 | - `Namespace` - a class of similar blockchains. For example EVM-based blockchains. 50 | - `Reference` - a way to identify a concrete chain inside a `Namespace`. For example Ethereum Mainnet or one of its test networks. 51 | - `Method` - a string which can be any of the following 4 [signing methods](https://docs.metamask.io/wallet/concepts/signing-methods/) supported by the MetaMask wallet API. The set of possible signature types may expand in the future as new signing methods are supported. The signing method is required by MetaMask for the dapp to make a signature request. It is not part of the signature payload. 52 | 1. `eth_signTypedData_v4` 53 | 2. `eth_signTypedData_v3` 54 | 3. `eth_signTypedData_v1` 55 | 4. `personal_sign` 56 | 57 | ### Snap Manifest 58 | 59 | This SIP specifies a permission named `endowment:signature-insight`. 60 | The permission grants a Snap read-only access to raw signature payloads, before they are accepted for signing by the user. 61 | 62 | The permission is specified as follows in `snap.manifest.json` files: 63 | 64 | ```json 65 | { 66 | "initialPermissions": { 67 | "endowment:signature-insight": {} 68 | } 69 | } 70 | ``` 71 | 72 | The permission includes an OPTIONAL caveat `allowSignatureOrigin`. 73 | The caveat grants a Snap read-only access to the URL requesting the signature. 74 | It can be specified as follows: 75 | 76 | ```json 77 | { 78 | "initialPermissions": { 79 | "endowment:signature-insight": { 80 | "allowSignatureOrigin": true 81 | } 82 | }, 83 | } 84 | ``` 85 | 86 | ### Snap Implementation 87 | 88 | The following is an example implementation of the API: 89 | 90 | ```typescript 91 | import { OnSignatureHandler, SeverityLevel } from "@metamask/snaps-sdk"; 92 | 93 | export const onSignature: OnSignatureHandler = async ({ 94 | signature: Record, 95 | signatureOrigin: string, /* If allowSignatureOrigin is set to true */ 96 | }) => { 97 | const content = /* Get UI component with insights */; 98 | const isContentCritical = /* Boolean checking if content is critical */ 99 | return isContentCritical ? { content, severity: SeverityLevel.Critical } : { content }; 100 | }; 101 | ``` 102 | The interface for an `onSignature` handler function’s arguments is: 103 | 104 | ```typescript 105 | interface OnSignatureArgs { 106 | signature: Record; 107 | signatureOrigin?: string; 108 | } 109 | ``` 110 | 111 | `signature` - The signature object is intentionally not defined in this SIP because different chains may specify different signature formats. 112 | It is beyond the scope of the SIP standards to define interfaces for every chain. 113 | Instead, it is the Snap developer's responsibility to be cognizant of the shape of signature objects for relevant chains. 114 | Nevertheless, you can refer to [Appendix I](#appendix-i-ethereum-signature-objects) for the interfaces of the Ethereum signature objects available in MetaMask at the time of this SIP's creation. 115 | 116 | `signatureOrigin` - The URL origin of the signature request. The existence of this property is dependent on the `allowSignatureOrigin` caveat. 117 | 118 | 119 | The interface for the return value of an `onSignature` export is: 120 | 121 | ```typescript 122 | interface OnSignatureResponse { 123 | content: Component | null; 124 | severity?: SeverityLevel; 125 | } 126 | ``` 127 | 128 | **Note:** `severity` is an OPTIONAL field and the omission of such means that there is no escalation of the content being returned. 129 | 130 | ## Specification 131 | 132 | Please see [SIP-3](sip-3.md) for more information on the original transaction insights API. 133 | 134 | Please see [SIP-7](sip-7.md) for more information on the `Component` type returned in the `OnTransactionResponse`. 135 | 136 | Please see [SIP-11](sip-3.md) for more information on Transaction insight severity levels. 137 | 138 | ## Appendix I: Ethereum Signature Objects 139 | 140 | The following signature objects may appear for any `chainId` of `eip155:*` where `*` is some positive integer. This includes all Ethereum or "EVM-compatible" chains. As of the time of the creation of this SIP, they are the only possible signature objects for Ethereum chains. 141 | 142 | ### personal_sign 143 | 144 | ```typescript 145 | interface PersonalSignature { 146 | from: string; 147 | data: string; 148 | signatureMethod: 'personal_sign'; 149 | } 150 | ``` 151 | 152 | ### eth_signTypedData 153 | 154 | ```typescript 155 | interface SignTypedDataSignature { 156 | from: string; 157 | data: Record[]; 158 | signatureMethod: 'eth_signTypedData'; 159 | } 160 | ``` 161 | 162 | ### eth_signTypedData_v3 163 | 164 | ```typescript 165 | interface SignTypedDataV3Signature { 166 | from: string; 167 | data: Record; 168 | signatureMethod: 'eth_signTypedData_v3'; 169 | } 170 | ``` 171 | 172 | ### eth_signTypedData_v4 173 | 174 | ```typescript 175 | interface SignTypedDataV4Signature { 176 | from: string; 177 | data: Record; 178 | signatureMethod: 'eth_signTypedData_v4'; 179 | } 180 | ``` 181 | 182 | **Note**: The `signatureMethod` property is MetaMask specific and not reflective of the standards defining the underlying signature methods. `signatureMethod` SHOULD be used by the signature insight snap as the source of the truth to identify the signature scheme it is providing insights for. 183 | 184 | ## Copyright 185 | 186 | Copyright and related rights waived via [CC0](../LICENSE). 187 | -------------------------------------------------------------------------------- /_includes/anchor_headings.html: -------------------------------------------------------------------------------- 1 | {% capture headingsWorkspace %} 2 | {% comment %} 3 | Copyright (c) 2018 Vladimir "allejo" Jimenez 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | {% endcomment %} 26 | {% comment %} 27 | Version 1.0.12 28 | https://github.com/allejo/jekyll-anchor-headings 29 | 30 | "Be the pull request you wish to see in the world." ~Ben Balter 31 | 32 | Usage: 33 | {% include anchor_headings.html html=content anchorBody="#" %} 34 | 35 | Parameters: 36 | * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll 37 | 38 | Optional Parameters: 39 | * beforeHeading (bool) : false - Set to true if the anchor should be placed _before_ the heading's content 40 | * headerAttrs (string) : '' - Any custom HTML attributes that will be added to the heading tag; you may NOT use `id`; 41 | the `%heading%` and `%html_id%` placeholders are available 42 | * anchorAttrs (string) : '' - Any custom HTML attributes that will be added to the `` tag; you may NOT use `href`, `class` or `title`; 43 | the `%heading%` and `%html_id%` placeholders are available 44 | * anchorBody (string) : '' - The content that will be placed inside the anchor; the `%heading%` placeholder is available 45 | * anchorClass (string) : '' - The class(es) that will be used for each anchor. Separate multiple classes with a space 46 | * anchorTitle (string) : '' - The `title` attribute that will be used for anchors 47 | * h_min (int) : 1 - The minimum header level to build an anchor for; any header lower than this value will be ignored 48 | * h_max (int) : 6 - The maximum header level to build an anchor for; any header greater than this value will be ignored 49 | * bodyPrefix (string) : '' - Anything that should be inserted inside of the heading tag _before_ its anchor and content 50 | * bodySuffix (string) : '' - Anything that should be inserted inside of the heading tag _after_ its anchor and content 51 | * generateId (true) : false - Set to true if a header without id should generate an id to use. 52 | 53 | Output: 54 | The original HTML with the addition of anchors inside of all of the h1-h6 headings. 55 | {% endcomment %} 56 | 57 | {% assign minHeader = include.h_min | default: 1 %} 58 | {% assign maxHeader = include.h_max | default: 6 %} 59 | {% assign beforeHeading = include.beforeHeading %} 60 | {% assign headerAttrs = include.headerAttrs %} 61 | {% assign nodes = include.html | split: ' 76 | {% if headerLevel == 0 %} 77 | 78 | {% assign firstChunk = node | split: '>' | first %} 79 | 80 | 81 | {% unless firstChunk contains '<' %} 82 | {% capture node %}{% endcapture %} 90 | {% assign _workspace = node | split: _closingTag %} 91 | {% capture _hAttrToStrip %}{{ _workspace[0] | split: '>' | first }}>{% endcapture %} 92 | {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} 93 | {% assign escaped_header = header | strip_html | strip %} 94 | 95 | {% assign _classWorkspace = _workspace[0] | split: 'class="' %} 96 | {% assign _classWorkspace = _classWorkspace[1] | split: '"' %} 97 | {% assign _html_class = _classWorkspace[0] %} 98 | 99 | {% if _html_class contains "no_anchor" %} 100 | {% assign skip_anchor = true %} 101 | {% else %} 102 | {% assign skip_anchor = false %} 103 | {% endif %} 104 | 105 | {% assign _idWorkspace = _workspace[0] | split: 'id="' %} 106 | {% if _idWorkspace[1] %} 107 | {% assign _idWorkspace = _idWorkspace[1] | split: '"' %} 108 | {% assign html_id = _idWorkspace[0] %} 109 | {% elsif include.generateId %} 110 | 111 | {% assign html_id = escaped_header | slugify %} 112 | {% if html_id == "" %} 113 | {% assign html_id = false %} 114 | {% endif %} 115 | {% capture headerAttrs %}{{ headerAttrs }} id="%html_id%"{% endcapture %} 116 | {% endif %} 117 | 118 | 119 | {% capture anchor %}{% endcapture %} 120 | 121 | {% if skip_anchor == false and html_id and headerLevel >= minHeader and headerLevel <= maxHeader %} 122 | {% if headerAttrs %} 123 | {% capture _hAttrToStrip %}{{ _hAttrToStrip | split: '>' | first }} {{ headerAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}>{% endcapture %} 124 | {% endif %} 125 | 126 | {% capture anchor %}href="#{{ html_id }}"{% endcapture %} 127 | 128 | {% if include.anchorClass %} 129 | {% capture anchor %}{{ anchor }} class="{{ include.anchorClass }}"{% endcapture %} 130 | {% endif %} 131 | 132 | {% if include.anchorTitle %} 133 | {% capture anchor %}{{ anchor }} title="{{ include.anchorTitle | replace: '%heading%', escaped_header }}"{% endcapture %} 134 | {% endif %} 135 | 136 | {% if include.anchorAttrs %} 137 | {% capture anchor %}{{ anchor }} {{ include.anchorAttrs | replace: '%heading%', escaped_header | replace: '%html_id%', html_id }}{% endcapture %} 138 | {% endif %} 139 | 140 | {% capture anchor %}{{ include.anchorBody | replace: '%heading%', escaped_header | default: '' }}{% endcapture %} 141 | 142 | 143 | {% if beforeHeading %} 144 | {% capture anchor %}{{ anchor }} {% endcapture %} 145 | {% else %} 146 | {% capture anchor %} {{ anchor }}{% endcapture %} 147 | {% endif %} 148 | {% endif %} 149 | 150 | {% capture new_heading %} 151 | 160 | {% endcapture %} 161 | 162 | 165 | {% assign chunkCount = _workspace | size %} 166 | {% if chunkCount > 1 %} 167 | {% capture new_heading %}{{ new_heading }}{{ _workspace | last }}{% endcapture %} 168 | {% endif %} 169 | 170 | {% capture edited_headings %}{{ edited_headings }}{{ new_heading }}{% endcapture %} 171 | {% endfor %} 172 | {% endcapture %}{% assign headingsWorkspace = '' %}{{ edited_headings | strip }} 173 | -------------------------------------------------------------------------------- /SIPS/sip-6.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 6 3 | title: Deterministic Snap-specific entropy 4 | status: Draft 5 | discussions-to: https://github.com/MetaMask/SIPs/discussions/69 6 | author: Maarten Zuidhoorn (@Mrtenz) 7 | created: 2022-10-27 8 | --- 9 | 10 | ## Abstract 11 | 12 | This SIP describes a way for Snaps to get some deterministic Snap-specific entropy, based on the secret recovery phrase 13 | of the user and the snap ID. Since Snaps do not have access to the secret recovery phrase directly, other Snaps are 14 | unable to get the same entropy. 15 | 16 | Snaps can optionally specify a salt in order to generate different entropy for different use cases, for example, some 17 | entropy to derive new private keys from, and some other entropy to encrypt some data. 18 | 19 | The entropy can be accessed from within a Snap, using the `snap_getEntropy` JSON-RPC method. 20 | 21 | ## Motivation 22 | 23 | Before this SIP, Snaps did not have a way to get some kind of deterministic entropy, without storing it on the disk. If 24 | the user deletes the Snap, or deletes the data of MetaMask, this entropy is lost. This SIP proposes a new way to get 25 | deterministic entropy, which is tied to the secret recovery phrase of the user, combined with the Snap ID. In this case, 26 | the entropy can always be re-created, as long as the user has a copy of their secret recovery phrase. 27 | 28 | ## Specification 29 | 30 | > Formal specifications are written in TypeScript. 31 | 32 | ### Language 33 | 34 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", 35 | "NOT RECOMMENDED", "MAY", and "OPTIONAL" written in uppercase in this document are to be interpreted as described in 36 | [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 37 | 38 | ### Deriving the entropy 39 | 40 | Entropy is derived using a 41 | [BIP 32](https://github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0032.mediawiki) derivation 42 | path. This derivation path MUST start with the magic value `0xd36e6170` (`1399742832'` in BIP 32 notation). The 43 | following eight indices are based on a hash of the Snap ID, with an optional salt. The hashing algorithm of choice is 44 | `keccak256`. 45 | 46 | To prevent other Snaps from getting the same entropy, Snaps MUST NOT be able to manually derive using the magic value 47 | `0xd36e6170`, i.e., through the `snap_getBip32Entropy` JSON-RPC method. 48 | 49 | The hash of the Snap ID is calculated as follows: 50 | 51 | ```typescript 52 | const hash = keccak256(snapId + keccak256(salt)); 53 | ``` 54 | 55 | If the salt is not provided, an empty string MUST be used instead: 56 | 57 | ```typescript 58 | const hash = keccak256(snapId + keccak256('')); 59 | ``` 60 | 61 | The hash is then split into eight big endian `uint32 | 0x80000000` integers. The resulting derivation path is a 62 | combination of the magic value, and the eight integers: 63 | 64 | ```typescript 65 | const computedDerivationPath = getUin32Array(hash).map((index) => (index | 0x80000000) >>> 0); 66 | const derivationPath = [0xd36e6170, ...computedDerivationPath]; 67 | ``` 68 | 69 | The entropy is then derived using the secret recovery phrase of the user, and the derivation path. The derivation 70 | algorithm of choice is `secp256k1`. The entropy is the private key of the derived key pair. 71 | 72 | ```typescript 73 | const { privateKey: entropy } = bip32Derive(secretRecoveryPhrase, derivationPath); 74 | ``` 75 | 76 | `bip32Derive` is defined as the `CKDpriv` function in 77 | [BIP 32](https://github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0032.mediawiki), using a root 78 | node created as per 79 | [BIP 39](https://github.com/bitcoin/bips/blob/6545b81022212a9f1c814f6ce1673e84bc02c910/bip-0039.mediawiki). 80 | 81 | ### `snap_getEntropy` JSON-RPC method 82 | 83 | The `snap_getEntropy` JSON-RPC method is used to get the entropy for a Snap. It takes an object as parameters, which has 84 | the following properties: 85 | 86 | - `version` (required, `number`): A version number, which MUST be the number `1`. 87 | - `salt` (optional, `string`): A salt to use when deriving the entropy. If provided, this MUST be interpreted as a UTF-8 88 | string value. If not provided, an empty string MUST be used instead. 89 | 90 | ```json 91 | { 92 | "method": "snap_getEntropy", 93 | "params": { 94 | "version": 1, 95 | "salt": "foo" 96 | } 97 | } 98 | ``` 99 | 100 | The method returns a `string` containing the entropy, encoded as a hexadecimal string. 101 | 102 | ## Reference implementation 103 | 104 | ```typescript 105 | import { SLIP10Node } from '@metamask/key-tree'; 106 | import { concatBytes, stringToBytes } from '@metamask/utils'; 107 | import { keccak_256 as keccak256 } from '@noble/hashes/sha3'; 108 | 109 | const MAGIC_VALUE = 0xd36e6170; 110 | const HARDENED_VALUE = 0x80000000; 111 | 112 | /** 113 | * Get an array of `uint32 | 0x80000000` values from a hash. The hash is assumed 114 | * to be 32 bytes long. 115 | * 116 | * @param hash - The hash to derive indices from. 117 | * @returns The derived indices. 118 | */ 119 | const getUint32Array = (hash: Uint8Array) => { 120 | const array = []; 121 | const view = new DataView(hash.buffer, hash.byteOffset, hash.byteLength); 122 | 123 | for (let index = 0; index < 8; index++) { 124 | const uint32 = view.getUint32(index * 4); 125 | array.push((uint32 | HARDENED_VALUE) >>> 0); 126 | } 127 | 128 | return array; 129 | }; 130 | 131 | /** 132 | * Get a BIP-32 derivation path, compatible with `@metamask/key-tree`, from an 133 | * array of indices. The indices are assumed to be a `uint32 | 0x80000000`. 134 | * 135 | * @param indices - The indices to get the derivation path for. 136 | * @returns The derivation path. 137 | */ 138 | const getDerivationPath = (indices: number[]) => { 139 | return indices.map((index) => `bip32:${index - HARDENED_VALUE}'` as const); 140 | }; 141 | 142 | /** 143 | * Derive deterministic Snap-specific entropy from a mnemonic phrase. The 144 | * snap ID and salt are used to derive a BIP-32 derivation path, which is then 145 | * used to derive a private key from the mnemonic phrase. 146 | * 147 | * The derived private key is returned as entropy. 148 | * 149 | * @param mnemonicPhrase - The mnemonic phrase to derive entropy from. 150 | * @param snapId - The ID of the Snap. 151 | * @param salt - An optional salt to use in the derivation. If not provided, an 152 | * empty string is used. 153 | * @returns The derived entropy. 154 | */ 155 | const getEntropy = async ( 156 | mnemonicPhrase: string, 157 | snapId: string, 158 | salt = '' 159 | ): Promise => { 160 | const snapIdBytes = stringToBytes(snapId); 161 | const saltBytes = stringToBytes(salt); 162 | 163 | // Get the derivation path from the snap ID. 164 | const hash = keccak256(concatBytes([snapIdBytes, keccak256(saltBytes)])); 165 | const computedDerivationPath = getUint32Array(hash); 166 | 167 | // Derive the private key using BIP-32. 168 | const { privateKey } = await SLIP10Node.fromDerivationPath({ 169 | derivationPath: [ 170 | `bip39:${mnemonicPhrase}`, 171 | ...getDerivationPath([MAGIC_VALUE, ...computedDerivationPath]), 172 | ], 173 | curve: 'secp256k1', 174 | }); 175 | 176 | if (!privateKey) { 177 | throw new Error('Failed to derive private key.'); 178 | } 179 | 180 | return privateKey; 181 | }; 182 | ``` 183 | 184 | ## Test vectors 185 | 186 | These test vectors are generated using the reference implementation, and the following mnemonic phrase: 187 | 188 | ``` 189 | test test test test test test test test test test test ball 190 | ``` 191 | 192 | ### Test vector 1 193 | 194 | ```json 195 | { 196 | "snapId": "foo", 197 | "derivationPath": "m/1399742832'/1323571613'/1848851859'/458888073'/1339050117'/513522582'/1371866341'/2121938770'/1014285256'", 198 | "entropy": "0x8bbb59ec55a4a8dd5429268e367ebbbe54eee7467c0090ca835c64d45c33a155" 199 | } 200 | ``` 201 | 202 | ### Test vector 2 203 | 204 | ```json 205 | { 206 | "snapId": "bar", 207 | "derivationPath": "m/1399742832'/767024459'/1206550137'/1427647479'/1048031962'/1656784813'/1860822351'/1362389435'/2133253878'", 208 | "entropy": "0xbdae5c0790d9189d8ae27fd4860b3b57bab420b6594c420ae9ae3a9f87c1ea14" 209 | } 210 | ``` 211 | 212 | ### Test vector 3 213 | 214 | ```json 215 | { 216 | "snapId": "foo", 217 | "salt": "bar", 218 | "derivationPath": "m/1399742832'/2002032866'/301374032'/1159533269'/453247377'/187127851'/1859522268'/152471137'/187531423'", 219 | "entropy": "0x59cbec1fa877ecb38d88c3a2326b23bff374954b39ad9482c9b082306ac4b3ad" 220 | } 221 | ``` 222 | 223 | ### Test vector 4 224 | 225 | ```json 226 | { 227 | "snapId": "bar", 228 | "salt": "baz", 229 | "derivationPath": "m/1399742832'/734358031'/701613791'/1618075622'/1535938847'/1610213550'/18831365'/356906080'/2095933563'", 230 | "entropy": "0x814c1f121eb4067d1e1d177246461e8a1cc6a1b1152756737aba7fa9c2161ba2" 231 | } 232 | ``` 233 | 234 | ## Copyright 235 | 236 | Copyright and related rights waived via [CC0](../LICENSE). 237 | -------------------------------------------------------------------------------- /_includes/toc.html: -------------------------------------------------------------------------------- 1 | {% capture tocWorkspace %} 2 | {% comment %} 3 | Copyright (c) 2017 Vladimir "allejo" Jimenez 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | {% endcomment %} 26 | {% comment %} 27 | Version 1.2.0 28 | https://github.com/allejo/jekyll-toc 29 | 30 | "...like all things liquid - where there's a will, and ~36 hours to spare, there's usually a/some way" ~jaybe 31 | 32 | Usage: 33 | {% include toc.html html=content sanitize=true class="inline_toc" id="my_toc" h_min=2 h_max=3 %} 34 | 35 | Parameters: 36 | * html (string) - the HTML of compiled markdown generated by kramdown in Jekyll 37 | 38 | Optional Parameters: 39 | * sanitize (bool) : false - when set to true, the headers will be stripped of any HTML in the TOC 40 | * class (string) : '' - a CSS class assigned to the TOC 41 | * id (string) : '' - an ID to assigned to the TOC 42 | * h_min (int) : 1 - the minimum TOC header level to use; any header lower than this value will be ignored 43 | * h_max (int) : 6 - the maximum TOC header level to use; any header greater than this value will be ignored 44 | * ordered (bool) : false - when set to true, an ordered list will be outputted instead of an unordered list 45 | * item_class (string) : '' - add custom class(es) for each list item; has support for '%level%' placeholder, which is the current heading level 46 | * submenu_class (string) : '' - add custom class(es) for each child group of headings; has support for '%level%' placeholder which is the current "submenu" heading level 47 | * base_url (string) : '' - add a base url to the TOC links for when your TOC is on another page than the actual content 48 | * anchor_class (string) : '' - add custom class(es) for each anchor element 49 | * skip_no_ids (bool) : false - skip headers that do not have an `id` attribute 50 | 51 | Output: 52 | An ordered or unordered list representing the table of contents of a markdown block. This snippet will only 53 | generate the table of contents and will NOT output the markdown given to it 54 | {% endcomment %} 55 | 56 | {% capture newline %} 57 | {% endcapture %} 58 | {% assign newline = newline | rstrip %} 59 | 60 | {% capture deprecation_warnings %}{% endcapture %} 61 | 62 | {% if include.baseurl %} 63 | {% capture deprecation_warnings %}{{ deprecation_warnings }}{{ newline }}{% endcapture %} 64 | {% endif %} 65 | 66 | {% if include.skipNoIDs %} 67 | {% capture deprecation_warnings %}{{ deprecation_warnings }}{{ newline }}{% endcapture %} 68 | {% endif %} 69 | 70 | {% capture jekyll_toc %}{% endcapture %} 71 | {% assign orderedList = include.ordered | default: false %} 72 | {% assign baseURL = include.base_url | default: include.baseurl | default: '' %} 73 | {% assign skipNoIDs = include.skip_no_ids | default: include.skipNoIDs | default: false %} 74 | {% assign minHeader = include.h_min | default: 1 %} 75 | {% assign maxHeader = include.h_max | default: 6 %} 76 | {% assign nodes = include.html | strip | split: ' maxHeader %} 92 | {% continue %} 93 | {% endif %} 94 | 95 | {% assign _workspace = node | split: '' | first }}>{% endcapture %} 114 | {% assign header = _workspace[0] | replace: _hAttrToStrip, '' %} 115 | 116 | {% if include.item_class and include.item_class != blank %} 117 | {% capture listItemClass %} class="{{ include.item_class | replace: '%level%', currLevel | split: '.' | join: ' ' }}"{% endcapture %} 118 | {% endif %} 119 | 120 | {% if include.submenu_class and include.submenu_class != blank %} 121 | {% assign subMenuLevel = currLevel | minus: 1 %} 122 | {% capture subMenuClass %} class="{{ include.submenu_class | replace: '%level%', subMenuLevel | split: '.' | join: ' ' }}"{% endcapture %} 123 | {% endif %} 124 | 125 | {% capture anchorBody %}{% if include.sanitize %}{{ header | strip_html }}{% else %}{{ header }}{% endif %}{% endcapture %} 126 | 127 | {% if htmlID %} 128 | {% capture anchorAttributes %} href="{% if baseURL %}{{ baseURL }}{% endif %}#{{ htmlID }}"{% endcapture %} 129 | 130 | {% if include.anchor_class %} 131 | {% capture anchorAttributes %}{{ anchorAttributes }} class="{{ include.anchor_class | split: '.' | join: ' ' }}"{% endcapture %} 132 | {% endif %} 133 | 134 | {% capture listItem %}{{ anchorBody }}{% endcapture %} 135 | {% elsif skipNoIDs == true %} 136 | {% continue %} 137 | {% else %} 138 | {% capture listItem %}{{ anchorBody }}{% endcapture %} 139 | {% endif %} 140 | 141 | {% if currLevel > lastLevel %} 142 | {% capture jekyll_toc %}{{ jekyll_toc }}<{{ listModifier }}{{ subMenuClass }}>{% endcapture %} 143 | {% elsif currLevel < lastLevel %} 144 | {% assign repeatCount = lastLevel | minus: currLevel %} 145 | 146 | {% for i in (1..repeatCount) %} 147 | {% capture jekyll_toc %}{{ jekyll_toc }}{% endcapture %} 148 | {% endfor %} 149 | 150 | {% capture jekyll_toc %}{{ jekyll_toc }}{% endcapture %} 151 | {% else %} 152 | {% capture jekyll_toc %}{{ jekyll_toc }}{% endcapture %} 153 | {% endif %} 154 | 155 | {% capture jekyll_toc %}{{ jekyll_toc }}{{ listItem }}{% endcapture %} 156 | 157 | {% assign lastLevel = currLevel %} 158 | {% assign firstHeader = false %} 159 | {% endfor %} 160 | 161 | {% assign repeatCount = minHeader | minus: 1 %} 162 | {% assign repeatCount = lastLevel | minus: repeatCount %} 163 | {% for i in (1..repeatCount) %} 164 | {% capture jekyll_toc %}{{ jekyll_toc }}{% endcapture %} 165 | {% endfor %} 166 | 167 | {% if jekyll_toc != '' %} 168 | {% assign rootAttributes = '' %} 169 | {% if include.class and include.class != blank %} 170 | {% capture rootAttributes %} class="{{ include.class | split: '.' | join: ' ' }}"{% endcapture %} 171 | {% endif %} 172 | 173 | {% if include.id and include.id != blank %} 174 | {% capture rootAttributes %}{{ rootAttributes }} id="{{ include.id }}"{% endcapture %} 175 | {% endif %} 176 | 177 | {% if rootAttributes %} 178 | {% assign nodes = jekyll_toc | split: '>' %} 179 | {% capture jekyll_toc %}<{{ listModifier }}{{ rootAttributes }}>{{ nodes | shift | join: '>' }}>{% endcapture %} 180 | {% endif %} 181 | {% endif %} 182 | {% endcapture %}{% assign tocWorkspace = '' %}{{ deprecation_warnings }}{{ jekyll_toc -}} 183 | -------------------------------------------------------------------------------- /SIPS/sip-20.md: -------------------------------------------------------------------------------- 1 | --- 2 | sip: 20 3 | title: WebSockets 4 | status: Draft 5 | author: David Drazic (@david0xd), Frederik Bolding (@frederikbolding), Guillaume Roux (@guillaumerx) 6 | created: 2023-12-15 7 | updated: 2025-06-05 8 | --- 9 | 10 | ## Abstract 11 | This SIP proposes to expose a new communication protocol to `endowment:network-access`, that enables Snaps to communicate with external services via WebSockets. This will allow Snaps to receive real-time data updates from external sources, such as price feeds or event notifications. 12 | 13 | ## Motivation 14 | Currently, Snaps can only communicate with external services via HTTP requests. This limits their ability to receive real-time data updates, which is essential for many use cases, such as price feeds or event notifications. By exposing the WebSocket protocol, Snaps can establish persistent connections with external services and receive real-time updates. 15 | 16 | ## Specification 17 | 18 | > Formal specifications are written in TypeScript. 19 | 20 | ### Language 21 | 22 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", 23 | "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and 24 | "OPTIONAL" written in uppercase in this document are to be interpreted as described in [RFC 2119](https://www.ietf.org/rfc/rfc2119.txt) 25 | 26 | ### Top-level architecture 27 | 28 | This architecture defers WebSocket management to the client, allowing Snaps to open, close, and send messages over WebSockets. The client will handle the underlying WebSocket connections and notify the Snap of any events that occur on those connections. 29 | 30 | The client has multiple responsibilities in this architecture: 31 | 32 | - All WebSocket connections opened by Snaps MUST be monitored by the client. Each connection is identified by a unique identifier, which is returned to the Snap when the connection is opened. 33 | 34 | - Any incoming message from WebSocket connections MUST be handled by the client and forwarded to the appropriate Snap through `onWebSocketEvent`. 35 | 36 | - When the Snap requests to open a WebSocket connection via `snap_openWebSocket`, the client MUST ensure that the connection is established and return a unique identifier for that connection. 37 | 38 | - Any open connection MUST be kept alive by the client until the Snap explicitly closes it, the connection is lost or the client is shut down, even while the Snap isn't running. 39 | 40 | - When the Snap requests to close a WebSocket connection via `snap_closeWebSocket`, the client MUST close the connection and remove it from its list of open connections. 41 | 42 | - Any errors that occur when the WebSocket connection is open (e.g., connection failure, message send failure) MUST be handled by the client and reported to the Snap via `onWebSocketEvent`. 43 | 44 | 45 | ### Snap Manifest 46 | 47 | This SIP doesn't introduce any new permissions, but rather extends `endowment:network-access` with new capabilities. 48 | 49 | ### RPC Methods 50 | 51 | #### `snap_openWebSocket` 52 | 53 | This method allows a Snap to open a WebSocket connection to an external service. The method takes a URL as a parameter and returns a unique identifier for the connection. 54 | 55 | ```typescript 56 | 57 | type OpenWebSocketParams = { 58 | url: string; 59 | protocols?: string[]; 60 | }; 61 | ``` 62 | The RPC method takes two parameters: 63 | 64 | - `url` - The URL of the WebSocket service to connect to. 65 | - The URL MUST be a valid WebSocket URL, starting with `wss://` or `ws://`. 66 | - Only one WebSocket connection can be opened per URL at a time. If a Snap tries to open a new connection to the same URL while an existing connection is still open, the client MUST throw an error. 67 | - `protocols` - An optional array of subprotocols to use for the WebSocket connection. 68 | 69 | An example of usage is given below. 70 | 71 | ```typescript 72 | snap.request({ 73 | method: "snap_openWebSocket", 74 | params: { 75 | url: "wss://example.com/websocket", 76 | protocols: ["soap", "wamp"], 77 | }, 78 | }); 79 | 80 | ``` 81 | 82 | #### `snap_closeWebSocket` 83 | This method allows a Snap to close an existing WebSocket connection. The method takes the unique identifier of the connection as a parameter. 84 | 85 | ```typescript 86 | type CloseWebSocketParams = { 87 | id: string; 88 | }; 89 | ``` 90 | The RPC method takes one parameter: 91 | - `id` - The unique identifier of the WebSocket connection to close. This identifier is returned by the `snap_openWebSocket` method. 92 | 93 | An example of usage is given below. 94 | 95 | ```typescript 96 | snap.request({ 97 | method: "snap_closeWebSocket", 98 | params: { 99 | id: "unique-connection-id", 100 | }, 101 | }); 102 | ``` 103 | #### `snap_sendWebSocketMessage` 104 | This method allows a Snap to send a message over an existing WebSocket connection. The method takes the unique identifier of the connection and the message to send as parameters. 105 | 106 | ```typescript 107 | type SendWebSocketMessageParams = { 108 | id: string; 109 | message: string | number[]; 110 | }; 111 | ``` 112 | 113 | The RPC method takes two parameters: 114 | - `id` - The unique identifier of the WebSocket connection to send the message over. This identifier is returned by the `snap_openWebSocket` method. 115 | - `message` - The message to send over the WebSocket connection. It can be either a string or an array of bytes. 116 | 117 | An example of usage is given below. 118 | 119 | ```typescript 120 | snap.request({ 121 | method: "snap_sendWebSocketMessage", 122 | params: { 123 | id: "unique-connection-id", 124 | message: "Hello, WebSocket!", 125 | }, 126 | }); 127 | ``` 128 | #### `snap_getWebSockets` 129 | This method allows a Snap to retrieve a list of all currently open WebSocket connections. It returns an array of objects, each containing the unique identifier, the optional protocols and the URL of the connection. 130 | 131 | - `id` - The unique identifier of the WebSocket connection. 132 | - `url` - The URL of the WebSocket connection. 133 | - `protocols` - The optional protocols of the WebSocket connection. 134 | 135 | ```typescript 136 | type WebSocketConnection = { 137 | id: string; 138 | url: string; 139 | protocols?: string[]; 140 | }; 141 | 142 | type GetWebSocketsResult = WebSocketConnection[]; 143 | ``` 144 | 145 | An example of usage is given below. 146 | 147 | ```typescript 148 | snap.request({ 149 | method: "snap_getWebSockets", 150 | }) 151 | ``` 152 | 153 | 154 | ### Handling WebSocket Events 155 | 156 | Snaps can handle WebSocket events by implementing the `onWebSocketEvent` handler. This handler will be called whenever a WebSocket event occurs, such as receiving a message, opening a connection or closing a connection. 157 | 158 | ```typescript 159 | import { OnWebSocketEventHandler } from "@metamask/snap-sdk"; 160 | 161 | export const onWebSocketEvent: OnWebSocketEventHandler = async ({ event }) => { 162 | switch (event.type) { 163 | case "message": 164 | // Handle incoming message 165 | console.log(`Message received from ${event.origin}:`, event.data); 166 | break; 167 | case "open": 168 | // Handle connection opened 169 | console.log(`WebSocket connection opened with ID ${event.id} from ${event.origin}`); 170 | break; 171 | case "close": 172 | // Handle connection closed 173 | console.log(`WebSocket connection closed with ID ${event.id} code ${event.code} and reason ${event.reason} from ${event.origin}`); 174 | break; 175 | } 176 | }; 177 | ``` 178 | 179 | ```typescript 180 | type OnWebSocketEventArgs = { 181 | event: WebSocketEvent; 182 | }; 183 | ``` 184 | 185 | ```typescript 186 | type OnWebSocketEventResponse = void; 187 | ``` 188 | 189 | #### Event Types 190 | 191 | The type for the `onWebSocketEvent` handler function's arguments. 192 | 193 | ```typescript 194 | export type WebSocketEvent = 195 | | WebSocketMessage 196 | | WebSocketOpenEvent 197 | | WebSocketCloseEvent; 198 | ``` 199 | 200 | ##### Message Event 201 | 202 | This event is triggered when a message is received over a WebSocket connection. The message can be either text or binary data. 203 | 204 | ```typescript 205 | export type WebSocketTextMessage = { 206 | type: "text"; 207 | message: string; 208 | }; 209 | 210 | export type WebSocketBinaryMessage = { 211 | type: "binary"; 212 | message: number[]; 213 | }; 214 | 215 | export type WebSocketMessageData = 216 | | WebSocketTextMessage 217 | | WebSocketBinaryMessage; 218 | 219 | export type WebSocketMessage = { 220 | type: "message"; 221 | id: string; 222 | origin: string; 223 | data: WebSocketMessageData; 224 | }; 225 | ``` 226 | 227 | `type` - The type of the WebSocket event, which is `"message"` for this event type. 228 | 229 | `id` - The unique identifier of the WebSocket connection associated with the event. 230 | 231 | `origin` - The origin of the WebSocket event. 232 | 233 | `data` - The data received in the event. This can be either a text message (as a string) or binary data (as an array of numbers). The `type` property indicates whether the data is text or binary. 234 | 235 | ##### Connection opened event 236 | 237 | This event is triggered when a WebSocket connection is successfully opened. It provides the unique identifier of the connection and the origin of the event. 238 | 239 | ```typescript 240 | export type WebSocketOpenEvent = { 241 | type: "open"; 242 | id: string; 243 | origin: string; 244 | }; 245 | ``` 246 | `type` - The type of the WebSocket event, which is `"open"` for this event type. 247 | 248 | `id` - The unique identifier of the WebSocket connection associated with the event. 249 | 250 | `origin` - The origin of the WebSocket connection. 251 | 252 | ##### Connection closed event 253 | 254 | This event is triggered when a WebSocket connection is closed. It provides the unique identifier of the connection and the origin of the event. 255 | 256 | ```typescript 257 | export type WebSocketCloseEvent = { 258 | type: "close"; 259 | id: string; 260 | origin: string; 261 | code: number; 262 | reason: string | null; 263 | wasClean: boolean | null; 264 | }; 265 | ``` 266 | 267 | `type` - The type of the WebSocket event, which is `"close"` for this event type. 268 | 269 | `id` - The unique identifier of the WebSocket connection associated with the event. 270 | 271 | `origin` - The origin of the WebSocket connection. 272 | 273 | `code` - The numeric code indicating the reason for the closure of the WebSocket connection. This is a standard WebSocket close code. 274 | 275 | `reason` - A string providing a human-readable explanation of why the WebSocket connection was closed. 276 | 277 | `wasClean` - A boolean indicating whether the connection was closed cleanly (i.e., without any errors). 278 | 279 | ## Copyright 280 | 281 | Copyright and related rights waived via [CC0](../LICENSE). 282 | --------------------------------------------------------------------------------