diff --git a/README.md b/README.md index 3f8c277..1e58dcd 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ This Web3.js plugin adds support for the following wallet-related RPC methods: -- wallet_addEthereumChain (EIP-3085) -- wallet_updateEthereumChain (EIP-2015) -- wallet_switchEthereumChain (EIP-3326) -- wallet_getOwnedAssets (EIP-2256) -- wallet_watchAsset (EIP-747) -- wallet_requestPermissions (EIP-2255) -- wallet_getPermissions (EIP-2255) +- [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) +- [wallet_requestPermissions (EIP-2255)](https://eips.ethereum.org/EIPS/eip-2255) +- [wallet_getPermissions (EIP-2255)](https://eips.ethereum.org/EIPS/eip-2255) ## Installation @@ -31,8 +31,10 @@ pnpm add web3-plugin-wallet-rpc ### Register plugin ```typescript +import { Web3 } from "web3"; import { WalletRpcPlugin } from "web3-plugin-wallet-rpc"; -web3 = new Web3(/* provider here */); + +const web3 = new Web3("https://eth.llamarpc.com"); web3.registerPlugin(new WalletRpcPlugin()); ``` @@ -40,8 +42,71 @@ web3.registerPlugin(new WalletRpcPlugin()); #### addEthereumChain +Invokes the `wallet_addEthereumChain` method as defined in [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085). + +```typescript +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"], +}); +``` + +#### updateEthereumChain + +Invokes the `wallet_updateEthereumChain` method as defined in [EIP-2015](https://eips.ethereum.org/EIPS/eip-2015). + +```typescript +await web3.walletRpc.updateEthereumChain({ + chainId: 5000, + blockExplorerUrls: ["https://mantlescan.xyz"], + chainName: "Mantle", + nativeCurrency: { + name: "Mantle", + symbol: "MNT", + decimals: 18, + }, + rpcUrls: ["https://rpc.mantle.xyz"], +}); +``` + +#### 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). + +```typescript +const ownedAssets = await web3.walletRpc.getOwnedAssets({ + address: "0xa5653e88D9c352387deDdC79bcf99f0ada62e9c6", +}); +``` + +#### watchAsset + +Invokes the `wallet_watchAsset` method as defined in [EIP-747](https://eips.ethereum.org/EIPS/eip-747). + ```typescript -await web3.walletRpc.addEthereumChain({ chainId: "0x1388" }); // chainId 5000 is Mantle Mainnet +await web3.walletRpc.watchAsset({ + type: "ERC20", + options: { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + symbol: "USDC", + }, +}); ``` ## Contributing diff --git a/src/WalletRpcPlugin.ts b/src/WalletRpcPlugin.ts new file mode 100644 index 0000000..0b7aee3 --- /dev/null +++ b/src/WalletRpcPlugin.ts @@ -0,0 +1,152 @@ +import { Web3PluginBase, utils, validator } from "web3"; +import { + AddEthereumChainRequest, + GetOwnedAssetsRequest, + GetOwnedAssetsResult, + SwitchEthereumChainRequest, + UpdateEthereumChainRequest, + WatchAssetRequest, +} from "./types"; +import { parseToGetOwnedAssetsResult } from "./utils"; + +type WalletRpcApi = { + wallet_addEthereumChain: (param: AddEthereumChainRequest) => void; + wallet_updateEthereumChain: (param: UpdateEthereumChainRequest) => void; + wallet_switchEthereumChain: (param: SwitchEthereumChainRequest) => void; + wallet_getOwnedAssets: (param: GetOwnedAssetsRequest) => GetOwnedAssetsResult; + wallet_watchAsset: (param: WatchAssetRequest) => boolean; +}; + +/** + * This Web3.js plugin adds support for various wallet-related RPC methods. + * + * @example + * Initialize the plugin + * + * ```typescript + * import { Web3 } from "web3"; + * import { WalletRpcPlugin } from "web3-plugin-wallet-rpc"; + * + * const web3 = new Web3("https://eth.llamarpc.com"); + * web3.registerPlugin(new WalletRpcPlugin()); + * ``` + */ +export class WalletRpcPlugin extends Web3PluginBase { + public pluginNamespace = "walletRpc"; + + public constructor() { + super(); + } + + /** + * Request to add a new chain to the user's wallet. + * + * 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 + */ + public async addEthereumChain(param: AddEthereumChainRequest): Promise { + return this.requestManager.send({ + method: "wallet_addEthereumChain", + params: [ + { + ...param, + chainId: utils.toHex(param.chainId), + }, + ], + }); + } + + /** + * Switch to a new chain and register it with the user’s wallet if it isn’t already recognized. + * + * 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 + */ + public async updateEthereumChain( + param: UpdateEthereumChainRequest + ): Promise { + return this.requestManager.send({ + method: "wallet_updateEthereumChain", + params: [ + { + ...param, + chainId: utils.toHex(param.chainId), + }, + ], + }); + } + + /** + * Switch the wallet’s currently active chain. + * + * See [EIP-3326](https://eips.ethereum.org/EIPS/eip-3326) for more details. + * + * @param param - See {@link SwitchEthereumChainRequest} + * @returns a Promise that resolves if the request is successful + */ + public async switchEthereumChain( + param: SwitchEthereumChainRequest + ): Promise { + return this.requestManager.send({ + method: "wallet_switchEthereumChain", + params: [ + { + ...param, + chainId: utils.toHex(param.chainId), + }, + ], + }); + } + + /** + * Return a list of owned assets for the given address. + * + * 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, see {@link GetOwnedAssetsResult} + */ + public async getOwnedAssets( + param: GetOwnedAssetsRequest + ): Promise { + validator.validator.validate(["address"], [param.address]); + + const trueParam = { ...param }; + if (trueParam.options?.chainId) { + trueParam.options.chainId = utils.toHex(trueParam.options.chainId); + } + + const result = await this.requestManager.send({ + method: "wallet_getOwnedAssets", + params: [trueParam], + }); + + return parseToGetOwnedAssetsResult(result); + } + + /** + * Add an asset to the user's wallet. + * + * 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 + */ + public async watchAsset(param: WatchAssetRequest): Promise { + return this.requestManager.send({ + method: "wallet_watchAsset", + params: [param], + }); + } +} + +// Module Augmentation +declare module "web3" { + interface Web3Context { + walletRpc: WalletRpcPlugin; + } +} diff --git a/src/index.ts b/src/index.ts index 7941ca3..a715ade 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * from "./plugin"; +export * from "./WalletRpcPlugin"; export * from "./types"; diff --git a/src/plugin.ts b/src/plugin.ts deleted file mode 100644 index a156b07..0000000 --- a/src/plugin.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Web3PluginBase, utils } from "web3"; -import { AddEthereumChainRequest } from "./types"; - -type WalletRpcApi = { - wallet_addEthereumChain: (param: AddEthereumChainRequest) => null; -}; - -export class WalletRpcPlugin extends Web3PluginBase { - public pluginNamespace = "walletRpc"; - - public constructor() { - super(); - } - - public async addEthereumChain(param: AddEthereumChainRequest): Promise { - return this.requestManager.send({ - method: "wallet_addEthereumChain", - params: [ - { - ...param, - chainId: utils.toHex(param.chainId), - }, - ], - }); - } -} - -// Module Augmentation -declare module "web3" { - interface Web3Context { - walletRpc: WalletRpcPlugin; - } -} diff --git a/src/types.ts b/src/types.ts index 02bfebb..e2f5b35 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,160 @@ -import { Numbers } from "web3"; +import { Address, Numbers } from "web3"; -export interface NativeCurrencyData { +export type NativeCurrencyData = { name: string; symbol: string; decimals: number; -} +}; -export interface AddEthereumChainRequest { +/** + * Request to add a new chain to the user's wallet. + * + * See [EIP-3085](https://eips.ethereum.org/EIPS/eip-3085) for more details. + */ +export type AddEthereumChainRequest = { chainId: Numbers; blockExplorerUrls?: string[]; chainName?: string; iconUrls?: string[]; nativeCurrency?: NativeCurrencyData; rpcUrls?: string[]; -} +}; + +/** + * Request to switch to a new chain and register it with the user’s wallet if it isn’t already recognized. + * + * See [EIP-2015](https://eips.ethereum.org/EIPS/eip-2015) for more details. + */ +export type UpdateEthereumChainRequest = { + chainId: Numbers; + blockExplorerUrl?: string; + chainName?: string; + nativeCurrency?: NativeCurrencyData; + rpcUrls?: string[]; +}; + +/** + * Request to switch the wallet’s currently active chain. + * + * See [EIP-3326](https://eips.ethereum.org/EIPS/eip-3326) for more details. + */ +export type SwitchEthereumChainRequest = { + chainId: Numbers; +}; + +/** + * Request to return a list of owned assets for the given address. + * + * See [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256) for more details. + */ +export type GetOwnedAssetsRequest = { + /** + * Ethereum address that owns the assets. + */ + address: Address; + options?: { + /** + * Chain ID respecting EIP-155. + */ + chainId?: Numbers; + /** + * Maximum number of owned assets expected by the dApp to be returned. + */ + limit?: number; + /** + * Array of asset interface identifiers such as ['ERC20', 'ERC721']. + */ + types?: string[]; + /** + * Human-readable text provided by the dApp, explaining the intended purpose of this request. + */ + justification?: string; + }; +}; + +/** + * A single asset owned by the wallet user. + * + * See [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256) for more details. + */ +export type OwnedAsset = { + /** + * Ethereum checksummed address of the asset. + */ + address: Address; + /** + * Identifier for the chain on which the assets are deployed. + */ + chainId: Numbers; + /** + * Asset interface ERC identifier, e.g., ERC20. + * Optional - EIP-1820 could be used. + */ + type?: string; + /** + * Asset-specific fields. + */ + options: { + /** + * Token name. Optional if the token does not implement it. + */ + name?: string; + /** + * Token symbol. Optional if the token does not implement it. + */ + symbol?: string; + /** + * Token icon in base64 format. Optional. + */ + icon?: string; + /** + * The number of tokens that the user owns, in the smallest token denomination. + */ + balance: Numbers; + /** + * The number of decimals implemented by the token. Optional. + */ + decimals?: number; + }; +}; + +/** + * Response to a request to return a list of owned assets for the given address. + * + * See [EIP-2256](https://eips.ethereum.org/EIPS/eip-2256) for more details. + */ +export type GetOwnedAssetsResult = OwnedAsset[]; + +/** + * Request to add a new asset to the user’s wallet. + * + * 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. + */ + type: string; + /** + * Configuration options for the specified token type. + */ + options: { + /** + * The Ethereum address of the token contract. + */ + address: Address; + /** + * The token symbol, such as "ETH" for Ethereum or "USDC" for USD Coin. + */ + symbol?: string; + /** + * The number of decimals the token uses. For example, 18 for ETH and most ERC20 tokens. + */ + decimals?: number; + /** + * A URL to an image representing the token (often in base64 or hosted format). + */ + image?: string; + }; +}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..d86805e --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,7 @@ +import { GetOwnedAssetsResult } from "./types"; + +export function parseToGetOwnedAssetsResult( + result: unknown[] +): GetOwnedAssetsResult { + return result as GetOwnedAssetsResult; +} diff --git a/test/plugin.test.ts b/test/WalletRpcPlugin.test.ts similarity index 100% rename from test/plugin.test.ts rename to test/WalletRpcPlugin.test.ts diff --git a/test/wallet_addEthereumChain.test.ts b/test/wallet_addEthereumChain.test.ts index 7fced55..f4bbc66 100644 --- a/test/wallet_addEthereumChain.test.ts +++ b/test/wallet_addEthereumChain.test.ts @@ -3,18 +3,13 @@ import { WalletRpcPlugin } from "../src"; describe("WalletRpcPlugin", () => { describe("wallet_addEthereumChain", () => { - let requestManagerSendSpy: jest.Mock; - let web3: Web3; + const web3 = new Web3("http://127.0.0.1:8545"); + web3.registerPlugin(new WalletRpcPlugin()); - beforeAll(() => { - web3 = new Web3("http://127.0.0.1:8545"); - web3.registerPlugin(new WalletRpcPlugin()); + const requestManagerSendSpy = jest.fn(); + web3.requestManager.send = requestManagerSendSpy; - requestManagerSendSpy = jest.fn(); - web3.requestManager.send = requestManagerSendSpy; - }); - - afterAll(() => { + afterEach(() => { requestManagerSendSpy.mockClear(); }); @@ -26,5 +21,38 @@ describe("WalletRpcPlugin", () => { params: [{ chainId: "0x1388" }], }); }); + + it("should return correct result", async () => { + const result = await web3.walletRpc.addEthereumChain({ chainId: 5000 }); + + expect(result).toBeUndefined(); + }); + + it("should pass all possible fields as a param", async () => { + const request = { + 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"], + }; + + await web3.walletRpc.addEthereumChain(request); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_addEthereumChain", + params: [ + { + ...request, + chainId: "0x1388", + }, + ], + }); + }); }); }); diff --git a/test/wallet_getOwnedAssets.test.ts b/test/wallet_getOwnedAssets.test.ts new file mode 100644 index 0000000..2a1ac76 --- /dev/null +++ b/test/wallet_getOwnedAssets.test.ts @@ -0,0 +1,33 @@ +import { Web3 } from "web3"; +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_getOwnedAssets", () => { + 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.getOwnedAssets({ + address: "0xa5653e88D9c352387deDdC79bcf99f0ada62e9c6", + }); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_getOwnedAssets", + params: [{ address: "0xa5653e88D9c352387deDdC79bcf99f0ada62e9c6" }], + }); + }); + + it("should throw when called with invalid address", async () => { + await expect( + web3.walletRpc.getOwnedAssets({ address: "" }) + ).rejects.toThrow("validator found 1 error"); + }); + }); +}); diff --git a/test/wallet_switchEthereumChain.test.ts b/test/wallet_switchEthereumChain.test.ts new file mode 100644 index 0000000..3632395 --- /dev/null +++ b/test/wallet_switchEthereumChain.test.ts @@ -0,0 +1,33 @@ +import { Web3 } from "web3"; +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_switchEthereumChain", () => { + 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.switchEthereumChain({ chainId: 5000 }); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_switchEthereumChain", + params: [{ chainId: "0x1388" }], + }); + }); + + it("should return correct result", async () => { + const result = await web3.walletRpc.switchEthereumChain({ + chainId: 5000, + }); + + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/test/wallet_updateEthereumChain.test.ts b/test/wallet_updateEthereumChain.test.ts new file mode 100644 index 0000000..f6d3551 --- /dev/null +++ b/test/wallet_updateEthereumChain.test.ts @@ -0,0 +1,59 @@ +import { Web3 } from "web3"; +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_updateEthereumChain", () => { + 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.updateEthereumChain({ chainId: 5000 }); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_updateEthereumChain", + params: [{ chainId: "0x1388" }], + }); + }); + + it("should return correct result", async () => { + const result = await web3.walletRpc.updateEthereumChain({ + chainId: 5000, + }); + + expect(result).toBeUndefined(); + }); + + it("should pass all possible fields as a param", async () => { + const request = { + chainId: 5000, + blockExplorerUrls: ["https://mantlescan.xyz"], + chainName: "Mantle", + nativeCurrency: { + name: "Mantle", + symbol: "MNT", + decimals: 18, + }, + rpcUrls: ["https://rpc.mantle.xyz"], + }; + + await web3.walletRpc.updateEthereumChain(request); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_updateEthereumChain", + params: [ + { + ...request, + chainId: "0x1388", + }, + ], + }); + }); + }); +}); diff --git a/test/wallet_watchAsset.test.ts b/test/wallet_watchAsset.test.ts new file mode 100644 index 0000000..e767325 --- /dev/null +++ b/test/wallet_watchAsset.test.ts @@ -0,0 +1,44 @@ +import { Web3 } from "web3"; +import { WalletRpcPlugin } from "../src"; + +describe("WalletRpcPlugin", () => { + describe("wallet_watchAsset", () => { + 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 = { + type: "ERC20", + options: { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + symbol: "USDC", + }, + }; + + await web3.walletRpc.watchAsset(request); + + expect(requestManagerSendSpy).toHaveBeenCalledWith({ + method: "wallet_watchAsset", + params: [request], + }); + }); + + it("should return correct result", async () => { + const result = await web3.walletRpc.watchAsset({ + type: "ERC20", + options: { + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + }, + }); + + expect(result).toBeUndefined(); + }); + }); +});