Skip to content

Commit cfdae84

Browse files
committed
added ExtensionTools: zero-dependency module to work with Polkadot and Ethereum browser extensions
1 parent a447d05 commit cfdae84

5 files changed

Lines changed: 747 additions & 0 deletions

File tree

README.md

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,200 @@ Utf16.lengthInBytes('👨🏼‍👩🏼‍👧🏼‍👧🏼') // 19
6666
```
6767

6868
**[Full documentation for the UTF and hex string helpers](https://github.com/fend25/utf-helpers#readme)**
69+
70+
## Extension tools
71+
72+
### Ethereum
73+
74+
A tiny (0.5 Kb) and zero-dependency module to detect whether metamask extension presents,
75+
detect whether the access is granted and get account(s) or request access.
76+
77+
Node.js-safe, it will not throw unexpected errors like "cannot find window".
78+
Also, these methods are safe in a common way, they don't throw errors,
79+
but return wrapped result or error with additional info.
80+
81+
The `ExtensionsTools`, `Ethereum` and `Polkadot` modules should be imported directly from
82+
`@unique-nft/utils/extension` because they are not available at `@unique-nft/utils`.
83+
84+
For Ethereum browser extensions, like metamask:
85+
86+
```ts
87+
import {ExtensionTools} from '@unique-nft/utils/extension'
88+
89+
const Ethereum = ExtensionTools.Ethereum
90+
//or
91+
import {Ethereum} from '@unique-nft/utils/extension'
92+
93+
const getAccountsResult = await Ethereum.getAccounts()
94+
/*
95+
{
96+
accounts: ['0xf8cC75F76d46c3b1c5F270Fe06c8FFdeAB8E5eaB'],
97+
info: {extensionFound: true, chainId: '0x22b2', chainIdNumber: 8882},
98+
selectedAddress: 0xf8cC75F76d46c3b1c5F270Fe06c8FFdeAB8E5eaB
99+
}
100+
// or, when there is no granted account:
101+
{accounts: [], selectedAddress: null, info: {extensionFound: true, chainId: '0x22b2', chainIdNumber: 8882}}
102+
// or, when there is no extension:
103+
{accounts: [], selectedAddress: null, info: {extensionFound: false}}
104+
// in Node.js `info.extensionFound is` always false.
105+
*/
106+
```
107+
108+
Simple example which just checks extension and tries to get an address without prompting user:
109+
110+
```ts
111+
import {Ethereum} from '@unique-nft/utils/extension'
112+
113+
let result = await Ethereum.getAccounts()
114+
115+
if (result.info.extensionFound && result.selectedAddress) {
116+
//woohoo, let's create a Web3 Provider like that:
117+
const provider = new ethers.providers.Web3Provider(window.ethereum)
118+
console.log(ethers.utils.formatEther(await provider.getBalance(result.selectedAddress)))
119+
}
120+
```
121+
122+
More complex example, when we want to request user to grant access.
123+
_Note: If user has already granted access, it will work silently, just like_ `getAccounts`.
124+
125+
```ts
126+
import {Ethereum} from '@unique-nft/utils/extension'
127+
128+
const result = await Ethereum.requestAccounts()
129+
130+
if (result.selectedAddress) {
131+
//woohoo, let's create a Web3 Provider like that:
132+
const provider = new ethers.providers.Web3Provider(window.ethereum)
133+
console.log(ethers.utils.formatEther(await provider.getBalance(result.selectedAddress)))
134+
} else {
135+
if (result.info.userRejected) {
136+
console.log(`Oops, user doesn't want us. Let's show them some kawaii popup`)
137+
} else {
138+
console.error(result.info.error)
139+
}
140+
}
141+
```
142+
143+
### Polkadot
144+
145+
A tiny (1.5 Kb) and zero-dependency (no WASM, no anything) module to work with Polkadot extensions family -
146+
Polkadot.js, Subwallet, Talisman and other Polkadot.js-compatible wallets.
147+
Actually it's a neat and really useful replacement for the extension-dapp module.
148+
149+
This module works in browsers with multiple wallets, it can detect whether there is an extension installed
150+
and contains the boilerplate logic around detecting wallets, it's access and so on.
151+
152+
Node.js-safe, it will not throw unexpected errors like "cannot find window".
153+
Also, these methods are safe in a common way, they don't throw errors,
154+
but return wrapped result or error with additional info.
155+
156+
The `ExtensionsTools`, `Ethereum` and `Polkadot` modules should be imported directly
157+
from `@unique-nft/utils/extension` because they are not available at `@unique-nft/utils`.
158+
159+
```ts
160+
import {ExtensionTools} from '@unique-nft/utils/extension'
161+
//or
162+
import {Polkadot} from '@unique-nft/utils/extension'
163+
164+
const result = await Polkadot.enableAndLoadAllWallets()
165+
166+
result.info.extensionFound // boolean, in Node.js it's always false.
167+
168+
result.accounts[0].address // string
169+
```
170+
171+
Also, it contains ready signer object for the [Unique SDK](https://www.npmjs.com/package/@unique-nft/sdk):
172+
173+
```ts
174+
import {Polkadot} from '@unique-nft/utils/extension'
175+
import {Sdk} from '@unique-nft/sdk'
176+
177+
const {accounts} = await Polkadot.enableAndLoadAllWallets() // Some checks are omitted
178+
const account = accounts[0] // For the simplicity
179+
180+
const sdk = new Sdk({
181+
baseUrl: 'https://rest.opal.uniquenetwork.dev/v1',
182+
signer: account.uniqueSdkSigner
183+
})
184+
185+
// or provide it (or override default one) on demand with specific request:
186+
187+
const sdkWithoutSigner = new Sdk({baseUrl: 'https://rest.opal.uniquenetwork.dev/v1'})
188+
189+
const result = await sdkWithoutSigner.balance.transfer.submitWaitResult({
190+
amount: 1,
191+
address: account.address,
192+
destination: "5..." // some another address
193+
}, {
194+
signer: account.uniqueSdkSigner
195+
})
196+
```
197+
198+
##### Return types
199+
200+
`enableAndLoadAllWallets` and `loadEnabledWallets` return such result:
201+
202+
```ts
203+
export interface IPolkadotExtensionLoadWalletsResult {
204+
info: {
205+
extensionFound: boolean
206+
accountsFound: boolean
207+
userHasWalletsButHasNoAccounts: boolean
208+
userHasBlockedAllWallets: boolean
209+
}
210+
211+
accounts: IPolkadotExtensionAccount[]
212+
213+
wallets: IPolkadotExtensionWallet[]
214+
215+
rejectedWallets: Array<{
216+
name: string
217+
version: string
218+
isEnabled: boolean | undefined
219+
prettyName: string
220+
logo: {
221+
ipfsCid: string,
222+
url: string,
223+
}
224+
error: Error
225+
isBlockedByUser: boolean
226+
}>
227+
}
228+
```
229+
230+
where `IPolkadotExtensionAccount` is:
231+
232+
```ts
233+
export interface IPolkadotExtensionAccount extends Omit<Signer, 'signRaw'> {
234+
name: string
235+
id: string
236+
address: string
237+
addressShort: string
238+
239+
wallet: {
240+
name: string
241+
version: string
242+
isEnabled: boolean | undefined
243+
prettyName: string
244+
logo: {
245+
ipfsCid: string,
246+
url: string,
247+
}
248+
}
249+
250+
signRaw: (raw: SignerPayloadRawWithAddressAndTypeOptional | string) => Promise<SignerResult>
251+
signPayload: (payload: SignerPayloadJSON) => Promise<SignerResult>
252+
update?: (id: number, status: any) => void
253+
254+
uniqueSdkSigner: {
255+
sign: (unsignedTxPayload: SDK_UnsignedTxPayloadBody) => Promise<SDK_SignTxResultResponse>
256+
}
257+
258+
meta: {
259+
genesisHash: string | null
260+
name: string
261+
source: string
262+
}
263+
type: KeypairType // 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum'
264+
}
265+
```

configs/tsup.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export default defineConfig([
66
index: "src/index.ts",
77
string: "src/StringUtils/index.ts",
88
address: "src/Address/index.ts",
9+
extension: "src/ExtensionTools/index.ts",
910
},
1011
format: [
1112
"esm",

src/ExtensionTools/ethereum.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
export interface IEthereumRequestAccountsResult {
2+
accounts: string[]
3+
selectedAddress: string | null
4+
info: {
5+
extensionFound: boolean
6+
chainId?: string
7+
chainIdNumber?: number
8+
userRejected?: boolean
9+
error?: Error
10+
}
11+
}
12+
13+
export interface AddEthereumChainParameter {
14+
chainId: string // A 0x-prefixed hexadecimal string
15+
chainName: string
16+
nativeCurrency: {
17+
name: string
18+
symbol: string // 2-6 characters long
19+
decimals: 18
20+
}
21+
rpcUrls: string[]
22+
blockExplorerUrls?: string[]
23+
iconUrls?: string[] // Currently ignored
24+
}
25+
26+
27+
export const requestAccounts = async (): Promise<Promise<IEthereumRequestAccountsResult>> => {
28+
if (typeof window === 'undefined' || !(window as any).ethereum) {
29+
return {accounts: [], selectedAddress: null, info: {extensionFound: false}}
30+
}
31+
const ethereum = (window as any).ethereum
32+
let accounts: string[] = []
33+
try {
34+
accounts = await ethereum.request({method: 'eth_requestAccounts'})
35+
return {
36+
accounts,
37+
selectedAddress: ethereum.selectedAddress,
38+
info: {
39+
extensionFound: true,
40+
chainId: ethereum.chainId,
41+
chainIdNumber: parseInt(ethereum.chainId, 16),
42+
},
43+
}
44+
} catch (error: any) {
45+
// EIP-1193 userRejectedRequest error code is 4001
46+
// If this happens, the user rejected the connection request.
47+
48+
return {
49+
accounts: [],
50+
selectedAddress: null,
51+
info: {extensionFound: true, userRejected: error.code === 4001, error},
52+
}
53+
}
54+
}
55+
56+
export const getAccounts = async (): Promise<IEthereumRequestAccountsResult> => {
57+
if (typeof window === 'undefined' || !(window as any).ethereum) {
58+
return {accounts: [], selectedAddress: null, info: {extensionFound: false}}
59+
}
60+
const ethereum = (window as any).ethereum
61+
62+
const accounts: string[] = await ethereum.request({method: 'eth_accounts'})
63+
return {
64+
accounts,
65+
selectedAddress: ethereum.selectedAddress,
66+
info: {
67+
extensionFound: true,
68+
chainId: ethereum.chainId,
69+
chainIdNumber: parseInt(ethereum.chainId, 16),
70+
},
71+
}
72+
}
73+
74+
export const addChainToMetamask = async(chainData: AddEthereumChainParameter): Promise<void> => {
75+
const safeGetAccountsResult = await getAccounts()
76+
if (!safeGetAccountsResult.info.extensionFound) {
77+
throw new Error(`No browser extension found`)
78+
}
79+
const ethereum = (window as any).ethereum
80+
81+
if (ethereum.chainId === chainData.chainId) {
82+
console.log(`No need to add the chain to wallet - wallet already has ${chainData.chainName}'s chainId: ${ethereum.chainId} (${parseInt(ethereum.chainId)})`)
83+
return
84+
}
85+
86+
try {
87+
await ethereum.request({
88+
method: 'wallet_addEthereumChain',
89+
params: [chainData],
90+
})
91+
} catch (error: any) {
92+
console.error('Error during attempt to add chain to wallet', error.code, error)
93+
throw error
94+
}
95+
}
96+
97+
export const Ethereum = {
98+
requestAccounts,
99+
getAccounts,
100+
addChainToMetamask,
101+
}

src/ExtensionTools/index.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {Ethereum} from './ethereum'
2+
import {Polkadot} from './polkadot'
3+
4+
const ExtensionTools = {
5+
Ethereum,
6+
Polkadot,
7+
}
8+
9+
export {ExtensionTools, Ethereum, Polkadot}
10+
11+
export default ExtensionTools
12+
13+
// =========================================
14+
// types
15+
// =========================================
16+
17+
export type {
18+
AddEthereumChainParameter,
19+
IEthereumRequestAccountsResult,
20+
} from './ethereum'
21+
22+
export type {
23+
IPolkadotExtensionWallet,
24+
IPolkadotExtensionWalletInfo,
25+
IPolkadotExtensionAccount,
26+
IPolkadotExtensionGenericInfo,
27+
IPolkadotExtensionLoadWalletsResult,
28+
IPolkadotExtensionListWalletsResult,
29+
IPolkadotExtensionLoadWalletByNameResult,
30+
SignerPayloadJSON,
31+
SignerPayloadRaw,
32+
SignerPayloadRawWithAddressAndTypeOptional,
33+
SignerPayloadJSONWithAddressOptional,
34+
SignerResult,
35+
} from './polkadot'

0 commit comments

Comments
 (0)