From d4b9b0020fbfc4f3a00d4886e772e856897de995 Mon Sep 17 00:00:00 2001 From: Kris Urbas <605420+krzysu@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:25:45 +0100 Subject: [PATCH] feat: add wallet rpc examples --- package.json | 2 +- .../example-react-app/src/AccountDetail.tsx | 47 ------------ packages/example-react-app/src/App.tsx | 22 ++++-- .../example-react-app/src/ProviderButton.tsx | 18 ----- .../src/WalletRpcPlugComponent.tsx | 19 ----- .../src/components/AccountDetail.tsx | 44 +++++++++++ .../src/{ => components}/Accounts.tsx | 18 +++-- .../src/{ => components}/ProviderButton.css | 0 .../src/components/ProviderButton.tsx | 18 +++++ .../wallet-components/AddEthereumChain.tsx | 74 +++++++++++++++++++ .../src/wallet-components/GetPermissions.tsx | 38 ++++++++++ .../src/wallet-components/Permissions.tsx | 14 ++++ .../wallet-components/RequestPermissions.tsx | 40 ++++++++++ .../wallet-components/RevokePermissions.tsx | 35 +++++++++ .../wallet-components/SwitchEthereumChain.tsx | 45 +++++++++++ .../src/wallet-components/WatchAsset.tsx | 53 +++++++++++++ .../src/WalletRpcPlugin.ts | 4 +- packages/web3-plugin-wallet-rpc/src/types.ts | 2 +- 18 files changed, 391 insertions(+), 102 deletions(-) delete mode 100644 packages/example-react-app/src/AccountDetail.tsx delete mode 100644 packages/example-react-app/src/ProviderButton.tsx delete mode 100644 packages/example-react-app/src/WalletRpcPlugComponent.tsx create mode 100644 packages/example-react-app/src/components/AccountDetail.tsx rename packages/example-react-app/src/{ => components}/Accounts.tsx (70%) rename packages/example-react-app/src/{ => components}/ProviderButton.css (100%) create mode 100644 packages/example-react-app/src/components/ProviderButton.tsx create mode 100644 packages/example-react-app/src/wallet-components/AddEthereumChain.tsx create mode 100644 packages/example-react-app/src/wallet-components/GetPermissions.tsx create mode 100644 packages/example-react-app/src/wallet-components/Permissions.tsx create mode 100644 packages/example-react-app/src/wallet-components/RequestPermissions.tsx create mode 100644 packages/example-react-app/src/wallet-components/RevokePermissions.tsx create mode 100644 packages/example-react-app/src/wallet-components/SwitchEthereumChain.tsx create mode 100644 packages/example-react-app/src/wallet-components/WatchAsset.tsx diff --git a/package.json b/package.json index 19a8a1f..81210e4 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "lint:plugin": "cd packages/web3-plugin-wallet-rpc && yarn lint", "lint:example": "cd packages/example-react-app && yarn lint", "format": "prettier --write .", - "start:example": "cd packages/example-react-app && yarn start" + "start:example": "yarn build && cd packages/example-react-app && yarn start" }, "devDependencies": { "prettier": "^3.3.3" diff --git a/packages/example-react-app/src/AccountDetail.tsx b/packages/example-react-app/src/AccountDetail.tsx deleted file mode 100644 index 3e25fa3..0000000 --- a/packages/example-react-app/src/AccountDetail.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { type MutableRefObject, useContext, useEffect, useRef, useState } from 'react'; - -import { type IWeb3Context, Web3Context } from './web3/Web3Context'; - -function AccountDetail({ address }: { address: string }) { - const web3Context: IWeb3Context = useContext(Web3Context); - - const [balance, setBalance] = useState(NaN); - const subscriptionId: MutableRefObject = useRef(undefined); - - // update balance - useEffect(() => { - updateBalance(); - - if (!web3Context.web3.subscriptionManager.supportsSubscriptions()) { - return; - } - - web3Context.web3.eth.subscribe('newBlockHeaders').then((newBlockSubscription) => { - subscriptionId.current = newBlockSubscription.id; - newBlockSubscription.on('data', () => { - updateBalance(); - }); - }); - - return () => { - web3Context.web3.eth.subscriptionManager.unsubscribe( - ({ id }) => subscriptionId.current === id, - ); - }; - }); - - function updateBalance(): void { - web3Context.web3.eth.getBalance(address).then((balance: bigint) => { - setBalance(parseFloat(web3Context.web3.utils.fromWei(balance, 'ether'))); - }); - } - - return ( - <> -
{address}
-
Balance in native token: {`${balance}`}
- - ); -} - -export default AccountDetail; diff --git a/packages/example-react-app/src/App.tsx b/packages/example-react-app/src/App.tsx index 4408954..a4d8315 100644 --- a/packages/example-react-app/src/App.tsx +++ b/packages/example-react-app/src/App.tsx @@ -1,12 +1,14 @@ import { useContext, useEffect, useState } from 'react'; import type { ProviderChainId, providers } from 'web3'; +import { Accounts } from './components/Accounts'; +import { ProviderButton } from './components/ProviderButton'; import { AccountProvider } from './web3/AccountContext'; import { type IWeb3Context, Web3Context } from './web3/Web3Context'; - -import Accounts from './Accounts'; -import ProviderButton from './ProviderButton'; -import WalletRpcPlugComponent from './WalletRpcPlugComponent'; +import { SwitchEthereumChain } from './wallet-components/SwitchEthereumChain'; +import { AddEthereumChain } from './wallet-components/AddEthereumChain'; +import { WatchAsset } from './wallet-components/WatchAsset'; +import { Permissions } from './wallet-components/Permissions'; function App() { const web3Context: IWeb3Context = useContext(Web3Context); @@ -74,14 +76,22 @@ function App() { })} ) : null} +

Network Details

Chain ID: {`${chainId}`}
Network ID: {`${networkId}`}
+ - + - +

Wallet RPC Methods

+
+ + + + +
)}
diff --git a/packages/example-react-app/src/ProviderButton.tsx b/packages/example-react-app/src/ProviderButton.tsx deleted file mode 100644 index dd7b8d5..0000000 --- a/packages/example-react-app/src/ProviderButton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useContext } from 'react'; -import { EIP6963ProviderDetail } from 'web3'; - -import './ProviderButton.css'; -import { IWeb3Context, Web3Context } from './web3/Web3Context'; - -function ProviderButton({ provider }: { provider: EIP6963ProviderDetail }) { - const web3Context: IWeb3Context = useContext(Web3Context); - - return ( - - ); -} - -export default ProviderButton; diff --git a/packages/example-react-app/src/WalletRpcPlugComponent.tsx b/packages/example-react-app/src/WalletRpcPlugComponent.tsx deleted file mode 100644 index 6bfd0c4..0000000 --- a/packages/example-react-app/src/WalletRpcPlugComponent.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useContext } from 'react'; -import { Web3Context } from './web3/Web3Context'; - -function WalletRpcPlugComponent() { - const { web3 } = useContext(Web3Context); - - return ( -
- - -
- ); -} - -export default WalletRpcPlugComponent; diff --git a/packages/example-react-app/src/components/AccountDetail.tsx b/packages/example-react-app/src/components/AccountDetail.tsx new file mode 100644 index 0000000..2614e4b --- /dev/null +++ b/packages/example-react-app/src/components/AccountDetail.tsx @@ -0,0 +1,44 @@ +import { type MutableRefObject, useContext, useEffect, useRef, useState } from 'react'; + +import { type IWeb3Context, Web3Context } from '../web3/Web3Context'; + +export function AccountDetail({ address }: { address: string }) { + const web3Context: IWeb3Context = useContext(Web3Context); + + const [balance, setBalance] = useState(NaN); + const subscriptionId: MutableRefObject = useRef(undefined); + + async function updateBalance(): Promise { + const newBalance = await web3Context.web3.eth.getBalance(address); + + setBalance(parseFloat(web3Context.web3.utils.fromWei(newBalance, 'ether'))); + } + + useEffect(() => { + async function subscribeToNewBlockHeaders() { + const newBlockSubscription = + await web3Context.web3.eth.subscribe('newBlockHeaders'); + + subscriptionId.current = newBlockSubscription.id; + + newBlockSubscription.on('data', () => { + void updateBalance(); + }); + } + + void subscribeToNewBlockHeaders(); + + return () => { + void web3Context.web3.eth.subscriptionManager.unsubscribe( + ({ id }) => subscriptionId.current === id, + ); + }; + }); + + return ( + <> +
{address}
+
Balance in native token: {`${balance}`}
+ + ); +} diff --git a/packages/example-react-app/src/Accounts.tsx b/packages/example-react-app/src/components/Accounts.tsx similarity index 70% rename from packages/example-react-app/src/Accounts.tsx rename to packages/example-react-app/src/components/Accounts.tsx index 71d1f64..34fdfc6 100644 --- a/packages/example-react-app/src/Accounts.tsx +++ b/packages/example-react-app/src/components/Accounts.tsx @@ -1,8 +1,10 @@ import { useContext } from 'react'; -import { AccountContext, type IAccountContext } from './web3/AccountContext'; -import AccountDetail from './AccountDetail'; -function Accounts() { +import { AccountContext, type IAccountContext } from '../web3/AccountContext'; + +import { AccountDetail } from './AccountDetail'; + +export function Accounts() { const accountContext: IAccountContext = useContext(AccountContext); return ( @@ -10,16 +12,18 @@ function Accounts() {

Accounts

{accountContext.selectedAccount === undefined ? ( - + ) : ( <>

Selected Account

- + {accountContext.accounts.length > 1 ? ( <>

Other Accounts

{accountContext.accounts.slice(1).map((account: string) => ( - + ))} ) : null} @@ -29,5 +33,3 @@ function Accounts() { ); } - -export default Accounts; diff --git a/packages/example-react-app/src/ProviderButton.css b/packages/example-react-app/src/components/ProviderButton.css similarity index 100% rename from packages/example-react-app/src/ProviderButton.css rename to packages/example-react-app/src/components/ProviderButton.css diff --git a/packages/example-react-app/src/components/ProviderButton.tsx b/packages/example-react-app/src/components/ProviderButton.tsx new file mode 100644 index 0000000..5754738 --- /dev/null +++ b/packages/example-react-app/src/components/ProviderButton.tsx @@ -0,0 +1,18 @@ +import { useContext } from 'react'; +import type { EIP6963ProviderDetail } from 'web3'; + +import type { IWeb3Context } from '../web3/Web3Context'; +import { Web3Context } from '../web3/Web3Context'; + +import './ProviderButton.css'; + +export function ProviderButton({ provider }: { provider: EIP6963ProviderDetail }) { + const web3Context: IWeb3Context = useContext(Web3Context); + + return ( + + ); +} diff --git a/packages/example-react-app/src/wallet-components/AddEthereumChain.tsx b/packages/example-react-app/src/wallet-components/AddEthereumChain.tsx new file mode 100644 index 0000000..5edff3a --- /dev/null +++ b/packages/example-react-app/src/wallet-components/AddEthereumChain.tsx @@ -0,0 +1,74 @@ +import { useContext, useState } from 'react'; +import { AddEthereumChainRequest } from 'web3-plugin-wallet-rpc'; +import { Web3Context } from '../web3/Web3Context'; + +const chains: Record = { + mantle: { + 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'], + }, + scroll: { + chainId: 534352, + blockExplorerUrls: ['https://scrollscan.com'], + chainName: 'Scroll', + iconUrls: ['https://icons.llamao.fi/icons/chains/rsz_scroll.jpg'], + nativeCurrency: { + name: 'ETH', + symbol: 'ETH', + decimals: 18, + }, + rpcUrls: ['https://rpc.scroll.io'], + }, +}; + +function AddChainButton({ chainDetails }: { chainDetails: AddEthereumChainRequest }) { + const { web3 } = useContext(Web3Context); + const [error, setError] = useState(undefined); + + const handleClick = () => { + web3.walletRpc + .addEthereumChain(chainDetails) + .then((response) => { + // eslint-disable-next-line no-console + console.log( + `Successfully added chain ${chainDetails.chainId} with response`, + response, + ); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + if (e instanceof Error) { + setError(e); + } + }); + }; + + return ( + <> + + {error &&
{error.message}
} + + ); +} + +export function AddEthereumChain() { + return ( +
+

Add EVM Chain

+ + +
+ ); +} diff --git a/packages/example-react-app/src/wallet-components/GetPermissions.tsx b/packages/example-react-app/src/wallet-components/GetPermissions.tsx new file mode 100644 index 0000000..fa2fdc2 --- /dev/null +++ b/packages/example-react-app/src/wallet-components/GetPermissions.tsx @@ -0,0 +1,38 @@ +import { useContext, useState } from 'react'; +import { Permission } from 'web3-plugin-wallet-rpc'; +import { Web3Context } from '../web3/Web3Context'; + +export function GetPermissions() { + const { web3 } = useContext(Web3Context); + const [error, setError] = useState(undefined); + const [permissions, setPermissions] = useState(undefined); + + const handleClick = () => { + web3.walletRpc + .getPermissions() + .then((response) => { + // eslint-disable-next-line no-console + console.log(`Successfully got permissions with response`, response); + + setPermissions(response); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + if (e instanceof Error) { + setError(e); + } + }); + }; + + return ( + <> + + {permissions &&
{JSON.stringify(permissions, null, 2)}
} + {error &&
{error.message}
} + + ); +} diff --git a/packages/example-react-app/src/wallet-components/Permissions.tsx b/packages/example-react-app/src/wallet-components/Permissions.tsx new file mode 100644 index 0000000..81481dd --- /dev/null +++ b/packages/example-react-app/src/wallet-components/Permissions.tsx @@ -0,0 +1,14 @@ +import { GetPermissions } from './GetPermissions'; +import { RequestPermissions } from './RequestPermissions'; +import { RevokePermissions } from './RevokePermissions'; + +export function Permissions() { + return ( +
+

Request, get and revoke permissions

+ + + +
+ ); +} diff --git a/packages/example-react-app/src/wallet-components/RequestPermissions.tsx b/packages/example-react-app/src/wallet-components/RequestPermissions.tsx new file mode 100644 index 0000000..b1f70e8 --- /dev/null +++ b/packages/example-react-app/src/wallet-components/RequestPermissions.tsx @@ -0,0 +1,40 @@ +import { useContext, useState } from 'react'; +import { Permission } from 'web3-plugin-wallet-rpc'; +import { Web3Context } from '../web3/Web3Context'; + +export function RequestPermissions() { + const { web3 } = useContext(Web3Context); + const [error, setError] = useState(undefined); + const [permissions, setPermissions] = useState(undefined); + + const handleClick = () => { + web3.walletRpc + .requestPermissions({ + eth_accounts: {}, + }) + .then((response) => { + // eslint-disable-next-line no-console + console.log(`Successfully requested permissions with response`, response); + + setPermissions(response); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + if (e instanceof Error) { + setError(e); + } + }); + }; + + return ( + <> + + {permissions &&
{JSON.stringify(permissions, null, 2)}
} + {error &&
{error.message}
} + + ); +} diff --git a/packages/example-react-app/src/wallet-components/RevokePermissions.tsx b/packages/example-react-app/src/wallet-components/RevokePermissions.tsx new file mode 100644 index 0000000..2ac68fb --- /dev/null +++ b/packages/example-react-app/src/wallet-components/RevokePermissions.tsx @@ -0,0 +1,35 @@ +import { useContext, useState } from 'react'; +import { Web3Context } from '../web3/Web3Context'; + +export function RevokePermissions() { + const { web3 } = useContext(Web3Context); + const [error, setError] = useState(undefined); + + const handleClick = () => { + web3.walletRpc + .revokePermissions({ + eth_accounts: {}, + }) + .then((response) => { + // eslint-disable-next-line no-console + console.log(`Successfully revoked permissions with response`, response); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + if (e instanceof Error) { + setError(e); + } + }); + }; + + return ( + <> + + {error &&
{error.message}
} + + ); +} diff --git a/packages/example-react-app/src/wallet-components/SwitchEthereumChain.tsx b/packages/example-react-app/src/wallet-components/SwitchEthereumChain.tsx new file mode 100644 index 0000000..66202c0 --- /dev/null +++ b/packages/example-react-app/src/wallet-components/SwitchEthereumChain.tsx @@ -0,0 +1,45 @@ +import { useContext, useState } from 'react'; + +import { Web3Context } from '../web3/Web3Context'; + +function SwitchChainButton({ chainId }: { chainId: number }) { + const { web3 } = useContext(Web3Context); + const [error, setError] = useState(undefined); + + const handleClick = () => { + web3.walletRpc + .switchEthereumChain(chainId) + .then((response) => { + // eslint-disable-next-line no-console + console.log(`Successfully switched to chain ${chainId} with response`, response); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + if (e instanceof Error) { + setError(e); + } + }); + }; + + return ( + <> + + {error &&
{error.message}
} + + ); +} + +export function SwitchEthereumChain() { + return ( +
+

Switch Chain (must be known by the wallet)

+ + + +
+ ); +} diff --git a/packages/example-react-app/src/wallet-components/WatchAsset.tsx b/packages/example-react-app/src/wallet-components/WatchAsset.tsx new file mode 100644 index 0000000..a59d255 --- /dev/null +++ b/packages/example-react-app/src/wallet-components/WatchAsset.tsx @@ -0,0 +1,53 @@ +import { useContext, useState } from 'react'; +import { WatchAssetRequest } from 'web3-plugin-wallet-rpc'; +import { Web3Context } from '../web3/Web3Context'; + +const tokens: Record = { + usdc: { + type: 'ERC20', + options: { + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + symbol: 'USDC', + }, + }, +}; + +function WatchAssetButton({ asset }: { asset: WatchAssetRequest }) { + const { web3 } = useContext(Web3Context); + const [error, setError] = useState(undefined); + + const handleClick = () => { + web3.walletRpc + .watchAsset(asset) + .then((response) => { + // eslint-disable-next-line no-console + console.log(`Successfully added ${asset.options.symbol} with response`, response); + }) + .catch((e) => { + // eslint-disable-next-line no-console + console.error(e); + + if (e instanceof Error) { + setError(e); + } + }); + }; + + return ( + <> + + {error &&
{error.message}
} + + ); +} + +export function WatchAsset() { + return ( +
+

Add token to wallet's list

+ +
+ ); +} diff --git a/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts b/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts index 061d6a1..b6da5e8 100644 --- a/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts +++ b/packages/web3-plugin-wallet-rpc/src/WalletRpcPlugin.ts @@ -18,7 +18,7 @@ type WalletRpcApi = { wallet_watchAsset: (param: WatchAssetRequest) => boolean; wallet_requestPermissions: (param: PermissionRequest) => Permission[]; wallet_getPermissions: () => Permission[]; - wallet_revokePermissions: (param: PermissionRequest) => void; + wallet_revokePermissions: (param: PermissionRequest) => null; // experimental wallet_getOwnedAssets: (param: GetOwnedAssetsRequest) => OwnedAsset[]; wallet_updateEthereumChain: (param: UpdateEthereumChainRequest) => void; @@ -222,7 +222,7 @@ export class WalletRpcPlugin extends Web3PluginBase { * eth_accounts: {} * }); */ - public async revokePermissions(param: PermissionRequest): Promise { + public async revokePermissions(param: PermissionRequest): Promise { return this.requestManager.send({ method: 'wallet_revokePermissions', params: [param], diff --git a/packages/web3-plugin-wallet-rpc/src/types.ts b/packages/web3-plugin-wallet-rpc/src/types.ts index c3d951b..7aeabf7 100644 --- a/packages/web3-plugin-wallet-rpc/src/types.ts +++ b/packages/web3-plugin-wallet-rpc/src/types.ts @@ -230,7 +230,7 @@ export type Caveat = { /** * Value associated with this restriction, as JSON. Its meaning is context-dependent based on the caveat type. */ - value: any; + value: unknown; }; /**