From 169b12cb6a4cb4f1cd9be88b3b0511bd2df25934 Mon Sep 17 00:00:00 2001 From: Kris <605420+krzysu@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:05:27 +0100 Subject: [PATCH] feat: add get/request/revoke permissions methods (#9) --- .gitignore | 1 + .prettierrc.json | 3 + README.md | 87 ++++++--- .../example-react-app/public/manifest.json | 4 +- packages/web3-plugin-wallet-rpc/.gitignore | 3 +- .../src/WalletRpcPlugin.ts | 115 ++++++++++-- packages/web3-plugin-wallet-rpc/src/types.ts | 175 +++++++++++++++--- .../test/wallet_getPermissions.test.ts | 26 +++ .../test/wallet_requestPermissions.test.ts | 30 +++ .../test/wallet_revokePermissions.test.ts | 30 +++ 10 files changed, 405 insertions(+), 69 deletions(-) create mode 100644 .prettierrc.json create mode 100644 packages/web3-plugin-wallet-rpc/test/wallet_getPermissions.test.ts create mode 100644 packages/web3-plugin-wallet-rpc/test/wallet_requestPermissions.test.ts create mode 100644 packages/web3-plugin-wallet-rpc/test/wallet_revokePermissions.test.ts diff --git a/.gitignore b/.gitignore index 3c3629e..55371e5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +.vscode \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..bf357fb --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "trailingComma": "all" +} diff --git a/README.md b/README.md index a93ec4e..b7a19c4 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,16 @@ This Web3.js plugin adds support for the following wallet-related RPC methods: - [wallet_addEthereumChain (EIP-3085)](https://eips.ethereum.org/EIPS/eip-3085) -- [wallet_updateEthereumChain (EIP-2015)](https://eips.ethereum.org/EIPS/eip-2015) - [wallet_switchEthereumChain (EIP-3326)](https://eips.ethereum.org/EIPS/eip-3326) -- [wallet_getOwnedAssets (EIP-2256)](https://eips.ethereum.org/EIPS/eip-2256) - [wallet_watchAsset (EIP-747)](https://eips.ethereum.org/EIPS/eip-747) - -Not implemented yet: - - [wallet_requestPermissions (EIP-2255)](https://eips.ethereum.org/EIPS/eip-2255) - [wallet_getPermissions (EIP-2255)](https://eips.ethereum.org/EIPS/eip-2255) +- [wallet_revokePermissions](https://docs.metamask.io/wallet/reference/json-rpc-methods/wallet_revokepermissions/) + +Experimental - These methods require further investigation, as other libraries don’t implement them and wallets appear not to support them: + +- [wallet_updateEthereumChain (EIP-2015)](https://eips.ethereum.org/EIPS/eip-2015) +- [wallet_getOwnedAssets (EIP-2256)](https://eips.ethereum.org/EIPS/eip-2256) ## Installation @@ -45,7 +46,7 @@ web3.registerPlugin(new WalletRpcPlugin()); #### addEthereumChain -Invokes the `wallet_addEthereumChain` method as defined in [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085). +Invokes the `wallet_addEthereumChain` method as defined in [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085#wallet_addethereumchain). ```typescript await web3.walletRpc.addEthereumChain({ @@ -62,6 +63,58 @@ await web3.walletRpc.addEthereumChain({ }); ``` +#### switchEthereumChain + +Invokes the `wallet_switchEthereumChain` method as defined in [EIP-3326](https://eips.ethereum.org/EIPS/eip-3326#wallet_switchethereumchain). + +```typescript +await web3.walletRpc.switchEthereumChain(5000); +``` + +#### watchAsset + +Invokes the `wallet_watchAsset` method as defined in [EIP-747](https://eips.ethereum.org/EIPS/eip-747#specification). + +```typescript +await web3.walletRpc.watchAsset({ + type: "ERC20", + options: { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + symbol: "USDC", + }, +}); +``` + +#### requestPermissions + +Invokes the `wallet_requestPermissions` method as defined in [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255#specification). + +```typescript +const permissions = await web3.walletRpc.requestPermissions({ + eth_accounts: {}, +}); +``` + +#### getPermissions + +Invokes the `wallet_getPermissions` method as defined in [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255#specification). + +```typescript +const permissions = await web3.walletRpc.getPermissions(); +``` + +#### revokePermissions + +Invokes the `wallet_revokePermissions` method as defined in [MetaMask docs](https://docs.metamask.io/wallet/reference/json-rpc-methods/wallet_revokepermissions/). + +```typescript +const permissions = await web3.walletRpc.revokePermissions({ + eth_accounts: {}, +}); +``` + +### Experimental methods + #### updateEthereumChain Invokes the `wallet_updateEthereumChain` method as defined in [EIP-2015](https://eips.ethereum.org/EIPS/eip-2015). @@ -80,14 +133,6 @@ await web3.walletRpc.updateEthereumChain({ }); ``` -#### switchEthereumChain - -Invokes the `wallet_switchEthereumChain` method as defined in [EIP-3326](https://eips.ethereum.org/EIPS/eip-3326). - -```typescript -await web3.walletRpc.switchEthereumChain({ chainId: 5000 }); -``` - #### getOwnedAssets Invokes the `wallet_getOwnedAssets` method as defined in [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256). @@ -98,20 +143,6 @@ const ownedAssets = await web3.walletRpc.getOwnedAssets({ }); ``` -#### watchAsset - -Invokes the `wallet_watchAsset` method as defined in [EIP-747](https://eips.ethereum.org/EIPS/eip-747). - -```typescript -await web3.walletRpc.watchAsset({ - type: "ERC20", - options: { - address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - symbol: "USDC", - }, -}); -``` - ## Contributing We welcome pull requests! For major changes, please open an issue first to discuss the proposed modifications. diff --git a/packages/example-react-app/public/manifest.json b/packages/example-react-app/public/manifest.json index decaf0e..c7b92bc 100644 --- a/packages/example-react-app/public/manifest.json +++ b/packages/example-react-app/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "Web3.js + React dApp", - "name": "Web3.js + React Demonstration dApp", + "short_name": "Web3.js Wallet RPC Demo", + "name": "Web3.js Wallet RPC Methods Demonstration dApp", "icons": [ { "src": "favicon.ico", diff --git a/packages/web3-plugin-wallet-rpc/.gitignore b/packages/web3-plugin-wallet-rpc/.gitignore index 9b26ed0..7b52c4b 100644 --- a/packages/web3-plugin-wallet-rpc/.gitignore +++ b/packages/web3-plugin-wallet-rpc/.gitignore @@ -1,2 +1,3 @@ node_modules -lib \ No newline at end of file +lib +docs \ No newline at end of file diff --git a/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts b/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts index 0430bf0..d31ba1f 100644 --- a/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts +++ b/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts @@ -5,6 +5,8 @@ import type { AddEthereumChainRequest, GetOwnedAssetsRequest, OwnedAsset, + Permission, + PermissionRequest, UpdateEthereumChainRequest, WatchAssetRequest, } from "./types"; @@ -16,6 +18,9 @@ type WalletRpcApi = { wallet_switchEthereumChain: (chainId: Numbers) => void; wallet_getOwnedAssets: (param: GetOwnedAssetsRequest) => OwnedAsset[]; wallet_watchAsset: (param: WatchAssetRequest) => boolean; + wallet_requestPermissions: (param: PermissionRequest) => Permission[]; + wallet_getPermissions: () => Permission[]; + wallet_revokePermissions: (param: PermissionRequest) => void; }; /** @@ -44,8 +49,22 @@ export class WalletRpcPlugin extends Web3PluginBase { * * See [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085) for more details. * - * @param param - Details of the chain to add - * @returns a Promise that resolves if the request is successful + * @param param - Details of the chain to add. + * @returns A Promise that resolves if the request is successful. + * + * @example + * await web3.walletRpc.addEthereumChain({ + * chainId: 5000, + * blockExplorerUrls: ["https://mantlescan.xyz"], + * chainName: "Mantle", + * iconUrls: ["https://icons.llamao.fi/icons/chains/rsz_mantle.jpg"], + * nativeCurrency: { + * name: "Mantle", + * symbol: "MNT", + * decimals: 18, + * }, + * rpcUrls: ["https://rpc.mantle.xyz"], + * }); */ public async addEthereumChain(param: AddEthereumChainRequest): Promise { return this.requestManager.send({ @@ -64,8 +83,9 @@ export class WalletRpcPlugin extends Web3PluginBase { * * See [EIP-2015](https://eips.ethereum.org/EIPS/eip-2015) for more details. * - * @param param - Details of the chain to switch to and possibly add - * @returns a Promise that resolves if the request is successful + * @param param - Details of the chain to switch to and possibly add. + * @returns A Promise that resolves if the request is successful. + * @experimental */ public async updateEthereumChain( param: UpdateEthereumChainRequest, @@ -82,12 +102,17 @@ export class WalletRpcPlugin extends Web3PluginBase { } /** - * Switch the wallet’s currently active chain. + * Switch the wallet's currently active chain. + * If the specified chain does not exist in the wallet, an error will be thrown. + * To prevent errors, ensure the chain has been added first or handle the call within a try/catch block. * * See [EIP-3326](https://eips.ethereum.org/EIPS/eip-3326) for more details. * - * @param param - Chain ID of the chain to switch to - * @returns a Promise that resolves if the request is successful + * @param chainId - The ID of the chain to switch to. + * @returns A Promise that resolves if the chain switch is successful. + * + * @example + * await web3.walletRpc.switchEthereumChain(5000); */ public async switchEthereumChain(chainId: Numbers): Promise { return this.requestManager.send({ @@ -105,8 +130,9 @@ export class WalletRpcPlugin extends Web3PluginBase { * * See [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256) for more details. * - * @param param - Details of the request for owned assets - * @returns a Promise that resolves to a list of owned assets + * @param param - Details of the request for owned assets. + * @returns A Promise that resolves to a list of owned assets. + * @experimental */ public async getOwnedAssets( param: GetOwnedAssetsRequest, @@ -131,8 +157,17 @@ export class WalletRpcPlugin extends Web3PluginBase { * * See [EIP-747](https://eips.ethereum.org/EIPS/eip-747) for more details. * - * @param param - Details of the asset to watch - * @returns a Promise that resolves to `true` if the request is successful + * @param param - Details of the asset to watch. + * @returns A Promise that resolves to `true` if the request is successful. + * + * @example + * await web3.walletRpc.watchAsset({ + * type: "ERC20", + * options: { + * address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + * symbol: "USDC", + * }, + * }); */ public async watchAsset(param: WatchAssetRequest): Promise { return this.requestManager.send({ @@ -140,9 +175,65 @@ export class WalletRpcPlugin extends Web3PluginBase { params: [param], }); } + + /** + * Request permissions for a dApp. + * + * See [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255) for more details. + * + * @param param - Details of the permission request. + * @returns A Promise that resolves to an array of granted permissions. + * + * @example + * const permissions = await web3.walletRpc.requestPermissions({ + * eth_accounts: {} + * }); + */ + public async requestPermissions( + param: PermissionRequest, + ): Promise { + return this.requestManager.send({ + method: "wallet_requestPermissions", + params: [param], + }); + } + + /** + * Retrieve the list of permissions granted to the dApp. + * + * See [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255) for more details. + * + * @returns A Promise that resolves to an array of granted permissions. + * + * @example + * const permissions = await web3.walletRpc.getPermissions(); + */ + public async getPermissions(): Promise { + return this.requestManager.send({ + method: "wallet_getPermissions", + params: [], + }); + } + + /** + * Revoke permissions granted to the dApp. + * + * @param param - Details of the permissions to revoke. + * @returns A Promise that resolves if the request is successful. + * + * @example + * await web3.walletRpc.revokePermissions({ + * eth_accounts: {} + * }); + */ + public async revokePermissions(param: PermissionRequest): Promise { + return this.requestManager.send({ + method: "wallet_revokePermissions", + params: [param], + }); + } } -// Module Augmentation declare module "web3" { interface Web3Context { walletRpc: WalletRpcPlugin; diff --git a/packages/web3-plugin-wallet-rpc/src/types.ts b/packages/web3-plugin-wallet-rpc/src/types.ts index 5ab36b9..d20dc73 100644 --- a/packages/web3-plugin-wallet-rpc/src/types.ts +++ b/packages/web3-plugin-wallet-rpc/src/types.ts @@ -1,143 +1,266 @@ import type { Address, Numbers } from "web3"; export type NativeCurrencyData = { + /** + * The name of the native currency (e.g., "Ether" for Ethereum). + */ name: string; + + /** + * The currency symbol (e.g., "ETH" for Ethereum). + */ symbol: string; + + /** + * The number of decimal places used by the currency. + */ decimals: number; }; /** - * Request to add a new chain to the user's wallet. + * Request to add a new blockchain network to the user's wallet. * * See [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085) for more details. */ export type AddEthereumChainRequest = { + /** + * Unique identifier for the chain, as per EIP-155. + */ chainId: Numbers; + + /** + * URLs for block explorers associated with this chain. + */ blockExplorerUrls?: string[]; + + /** + * The name of the chain (e.g., "Ethereum Mainnet"). + */ chainName?: string; + + /** + * URLs to icons representing the chain. + */ iconUrls?: string[]; + + /** + * Details of the native currency used on this chain. + */ nativeCurrency?: NativeCurrencyData; + + /** + * URLs of the RPC nodes for this chain. + */ rpcUrls?: string[]; }; /** - * Request to switch to a new chain and register it with the user’s wallet if it isn’t already recognized. + * Request to switch to a specified blockchain network. Registers the network if it’s not recognized by the wallet. * * See [EIP-2015](https://eips.ethereum.org/EIPS/eip-2015) for more details. */ export type UpdateEthereumChainRequest = { + /** + * Unique identifier for the chain, as per EIP-155. + */ chainId: Numbers; + + /** + * URL for the primary block explorer of this chain. + */ blockExplorerUrl?: string; + + /** + * The name of the chain. + */ chainName?: string; + + /** + * Details of the native currency used on this chain. + */ nativeCurrency?: NativeCurrencyData; + + /** + * URLs of the RPC nodes for this chain. + */ rpcUrls?: string[]; }; /** - * Request to return a list of owned assets for the given address. + * Request to retrieve a list of assets owned by a specified Ethereum address. * * See [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256) for more details. */ export type GetOwnedAssetsRequest = { /** - * Ethereum address that owns the assets. + * Ethereum address of the asset owner. */ address: Address; + options?: { /** - * Chain ID respecting EIP-155. + * Chain ID as per EIP-155, specifying the blockchain. */ chainId?: Numbers; + /** - * Maximum number of owned assets expected by the dApp to be returned. + * Maximum number of assets to return. */ limit?: number; + /** - * Array of asset interface identifiers such as ['ERC20', 'ERC721']. + * List of asset types (e.g., ['ERC20', 'ERC721']) to filter the response. */ types?: string[]; + /** - * Human-readable text provided by the dApp, explaining the intended purpose of this request. + * Description of the purpose of this request, provided by the dApp. */ justification?: string; }; }; /** - * A single asset owned by the wallet user. + * Representation of a single asset owned by the user’s wallet. * * See [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256) for more details. */ export type OwnedAsset = { /** - * Ethereum checksummed address of the asset. + * Checksummed Ethereum address of the asset contract. */ address: Address; + /** - * Identifier for the chain on which the assets are deployed. + * Identifier for the chain where this asset is deployed. */ chainId: Numbers; + /** - * The token interface identifier, e.g., ERC20 + * Identifier of the asset type (e.g., "ERC20"). */ type?: string; + /** - * Asset-specific fields. + * Asset-specific metadata. */ options: { /** - * Token name. Optional if the token does not implement it. + * Name of the token. Optional if the token contract doesn’t implement it. */ name?: string; + /** - * Token symbol. Optional if the token does not implement it. + * Symbol of the token. Optional if the token contract doesn’t implement it. */ symbol?: string; + /** - * Token icon in base64 format. Optional. + * Token icon in base64 format or URL. Optional. */ icon?: string; + /** - * The number of tokens that the user owns, in the smallest token denomination. + * Number of tokens owned, in the smallest denomination. */ balance: Numbers; + /** - * The number of decimals implemented by the token. Optional. + * Number of decimal places supported by the token. Optional. */ decimals?: number; }; }; /** - * Request to add a new asset to the user’s wallet. + * Request to add a specified asset to the user’s wallet interface. * * See [EIP-747](https://eips.ethereum.org/EIPS/eip-747) for more details. */ export type WatchAssetRequest = { /** - * The token interface identifier, e.g., ERC20, ERC721, or ERC1155. - * This depends on the types of tokens supported by the wallet. + * Identifier for the token type (e.g., ERC20, ERC721, or ERC1155). */ type: string; + /** - * Configuration options for the specified token type. + * Asset-specific configuration details. */ options: { /** - * The Ethereum address of the token contract. + * Ethereum address of the token contract. */ address: Address; + /** - * The token symbol, such as "ETH" for Ethereum or "USDC" for USD Coin. + * Symbol of the token (e.g., "ETH", "USDC"). */ symbol?: string; + /** - * The number of decimals the token uses. For example, 18 for ETH and most ERC20 tokens. + * Number of decimals used by the token. */ decimals?: number; + /** - * A URL to an image representing the token (often in base64 or hosted format). + * URL or base64-encoded image of the token. */ image?: string; }; }; + +/** + * Defines a request to grant or revoke permissions. + * + * See [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255) for more details. + */ +export type PermissionRequest = Record; + +/** + * Specifies restrictions applied to a permitted method. + * + * See [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255) for more details. + */ +export type Caveat = { + /** + * Type of restriction. + */ + type: string; + + /** + * Value associated with this restriction, as JSON. Its meaning is context-dependent based on the caveat type. + */ + value: any; +}; + +/** + * Defines a permission granted to a dApp by the user. + * + * See [EIP-2255](https://eips.ethereum.org/EIPS/eip-2255) for more details. + */ +export type Permission = { + /** + * Unique identifier for the permission. + */ + id: string; + + /** + * URI of the dApp granted this permission. + */ + invoker: string; + + /** + * Method permitted by this permission (e.g., "eth_accounts"). + */ + parentCapability: string; + + /** + * List of restrictions applied to this permission. + */ + caveats: Caveat[]; + + /** + * Date of the permission request, in Unix timestamp format. + */ + date?: number; +}; diff --git a/packages/web3-plugin-wallet-rpc/test/wallet_getPermissions.test.ts b/packages/web3-plugin-wallet-rpc/test/wallet_getPermissions.test.ts new file mode 100644 index 0000000..f681cad --- /dev/null +++ b/packages/web3-plugin-wallet-rpc/test/wallet_getPermissions.test.ts @@ -0,0 +1,26 @@ +import { Web3 } from "web3"; + +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_getPermissions", () => { + const web3 = new Web3("http://127.0.0.1:8545"); + web3.registerPlugin(new WalletRpcPlugin()); + + const requestManagerSendSpy = jest.fn(); + web3.requestManager.send = requestManagerSendSpy; + + afterEach(() => { + requestManagerSendSpy.mockClear(); + }); + + it("should call the method with expected params", async () => { + await web3.walletRpc.getPermissions(); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_getPermissions", + params: [], + }); + }); + }); +}); diff --git a/packages/web3-plugin-wallet-rpc/test/wallet_requestPermissions.test.ts b/packages/web3-plugin-wallet-rpc/test/wallet_requestPermissions.test.ts new file mode 100644 index 0000000..c8f8d94 --- /dev/null +++ b/packages/web3-plugin-wallet-rpc/test/wallet_requestPermissions.test.ts @@ -0,0 +1,30 @@ +import { Web3 } from "web3"; + +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_requestPermissions", () => { + const web3 = new Web3("http://127.0.0.1:8545"); + web3.registerPlugin(new WalletRpcPlugin()); + + const requestManagerSendSpy = jest.fn(); + web3.requestManager.send = requestManagerSendSpy; + + afterEach(() => { + requestManagerSendSpy.mockClear(); + }); + + it("should call the method with expected params", async () => { + const request = { + eth_accounts: {}, + }; + + await web3.walletRpc.requestPermissions(request); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_requestPermissions", + params: [request], + }); + }); + }); +}); diff --git a/packages/web3-plugin-wallet-rpc/test/wallet_revokePermissions.test.ts b/packages/web3-plugin-wallet-rpc/test/wallet_revokePermissions.test.ts new file mode 100644 index 0000000..2d23506 --- /dev/null +++ b/packages/web3-plugin-wallet-rpc/test/wallet_revokePermissions.test.ts @@ -0,0 +1,30 @@ +import { Web3 } from "web3"; + +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_revokePermissions", () => { + const web3 = new Web3("http://127.0.0.1:8545"); + web3.registerPlugin(new WalletRpcPlugin()); + + const requestManagerSendSpy = jest.fn(); + web3.requestManager.send = requestManagerSendSpy; + + afterEach(() => { + requestManagerSendSpy.mockClear(); + }); + + it("should call the method with expected params", async () => { + const request = { + eth_accounts: {}, + }; + + await web3.walletRpc.revokePermissions(request); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_revokePermissions", + params: [request], + }); + }); + }); +});