Skip to content

Commit 7693fec

Browse files
committed
chore: extension: refactored and added subscriptions
1 parent 4bda774 commit 7693fec

4 files changed

Lines changed: 160 additions & 80 deletions

File tree

src/ExtensionTools/ethereum.ts

Lines changed: 145 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
export interface IEthereumRequestAccountsResult {
2-
accounts: string[]
3-
selectedAddress: string | null
4-
info: {
5-
extensionFound: boolean
6-
uniqueChainName?: UNIQUE_CHAIN,
7-
chainId?: string
8-
chainIdNumber?: number
9-
userRejected?: boolean
10-
error?: Error
11-
}
1+
import {documentReadyPromiseAndWindowIsOk} from './utils'
2+
3+
export const windowIsOkSync = (): boolean => {
4+
return typeof window !== 'undefined' && !!(window as any).ethereum;
5+
}
6+
7+
type IEthereumExtensionError = Error & {
8+
extensionFound: boolean
9+
isUserRejected: boolean
1210
}
1311

1412
export interface AddEthereumChainParameter {
@@ -25,77 +23,101 @@ export interface AddEthereumChainParameter {
2523
}
2624

2725
type UNIQUE_CHAIN = 'unique' | 'quartz' | 'opal' | 'sapphire'
28-
const UNIQUE_CHAIN_IDS: Record<UNIQUE_CHAIN, number> = {
26+
const chainNameToChainId: Record<UNIQUE_CHAIN, number> = {
2927
unique: 8880,
3028
quartz: 8881,
3129
opal: 8882,
3230
sapphire: 8883
3331
}
34-
const chainNameByChainId: Record<number, UNIQUE_CHAIN> = {
32+
const chainIdToChainName: Record<number, UNIQUE_CHAIN> = {
3533
8880: 'unique',
3634
8881: 'quartz',
3735
8882: 'opal',
3836
8883: 'sapphire'
3937
}
4038

39+
const isUniqueChainFactory = (chainName: UNIQUE_CHAIN) => (): boolean => {
40+
if (!windowIsOkSync()) {
41+
return false
42+
}
43+
const chainId = parseInt((window as any).ethereum.chainId, 16)
44+
return chainId === chainNameToChainId[chainName]
45+
}
46+
const currentChainIs: Record<UNIQUE_CHAIN, () => boolean> & { anyUniqueChain: (chainId: string | number) => boolean } = {
47+
unique: isUniqueChainFactory('unique'),
48+
quartz: isUniqueChainFactory('quartz'),
49+
opal: isUniqueChainFactory('opal'),
50+
sapphire: isUniqueChainFactory('sapphire'),
51+
anyUniqueChain: (chainId: string | number | null | undefined): boolean => {
52+
if (!chainId) return false
4153

42-
export const requestAccounts = async (): Promise<IEthereumRequestAccountsResult> => {
43-
if (typeof window === 'undefined' || !(window as any).ethereum) {
44-
return {accounts: [], selectedAddress: null, info: {extensionFound: false}}
54+
const chainName: UNIQUE_CHAIN | undefined =
55+
chainIdToChainName[typeof chainId === "number" ? chainId : parseInt(chainId, 16)]
56+
57+
return (typeof chainName === 'string')
58+
}
59+
}
60+
61+
62+
export type IEthereumAccountResult = {
63+
address: string
64+
chainId: number
65+
error: null
66+
} | {
67+
address: null
68+
chainId: number | null
69+
error: IEthereumExtensionError
70+
}
71+
72+
const getOrRequestAccounts = async (requestInsteadOfGet: boolean = false): Promise<IEthereumAccountResult> => {
73+
const windowIsOk = await documentReadyPromiseAndWindowIsOk()
74+
if (!windowIsOk || !(window as any).ethereum) {
75+
const error = new Error('No extension found') as IEthereumExtensionError
76+
error.extensionFound = false
77+
error.isUserRejected = false
78+
79+
return {
80+
address: null,
81+
chainId: null,
82+
error
83+
}
4584
}
85+
4686
const ethereum = (window as any).ethereum
4787
let accounts: string[] = []
4888
try {
49-
accounts = await ethereum.request({method: 'eth_requestAccounts'})
50-
const chainIdNumber = parseInt(ethereum.chainId, 16)
89+
accounts = await ethereum.request({method: requestInsteadOfGet ? 'eth_requestAccounts' : 'eth_accounts'})
90+
const chainId = parseInt(ethereum.chainId, 16)
5191

5292
return {
53-
accounts,
54-
selectedAddress: ethereum.selectedAddress,
55-
info: {
56-
extensionFound: true,
57-
chainId: ethereum.chainId,
58-
chainIdNumber,
59-
uniqueChainName: chainNameByChainId[chainIdNumber],
60-
},
93+
address: accounts[0],
94+
chainId,
95+
error: null
6196
}
62-
} catch (error: any) {
97+
} catch (_error: any) {
6398
// EIP-1193 userRejectedRequest error code is 4001
6499
// If this happens, the user rejected the connection request.
65100

101+
const error = _error as IEthereumExtensionError
102+
error.isUserRejected = _error.code === 4001
103+
error.extensionFound = true
104+
105+
const chainIdStr = (window as any).ethereum?.chainId
106+
const chainId = typeof chainIdStr === 'string' ? parseInt(chainIdStr, 16) : NaN
107+
66108
return {
67-
accounts: [],
68-
selectedAddress: null,
69-
info: {extensionFound: true, userRejected: error.code === 4001, error},
109+
address: null,
110+
chainId: isNaN(chainId) ? null : chainId,
111+
error,
70112
}
71113
}
72114
}
73115

74-
export const getAccounts = async (): Promise<IEthereumRequestAccountsResult> => {
75-
if (typeof window === 'undefined' || !(window as any).ethereum) {
76-
return {accounts: [], selectedAddress: null, info: {extensionFound: false}}
77-
}
78-
const ethereum = (window as any).ethereum
79116

80-
const accounts: string[] = await ethereum.request({method: 'eth_accounts'})
81-
const chainIdNumber = parseInt(ethereum.chainId, 16)
82-
return {
83-
accounts,
84-
selectedAddress: ethereum.selectedAddress,
85-
info: {
86-
extensionFound: true,
87-
chainId: ethereum.chainId,
88-
chainIdNumber,
89-
uniqueChainName: chainNameByChainId[chainIdNumber],
90-
},
91-
}
92-
}
117+
export const addChainToExtension = async (chainData: AddEthereumChainParameter): Promise<void> => {
118+
const windowIsOk = await documentReadyPromiseAndWindowIsOk()
119+
if (!windowIsOk) return
93120

94-
export const addChainToMetamask = async (chainData: AddEthereumChainParameter): Promise<void> => {
95-
const safeGetAccountsResult = await getAccounts()
96-
if (!safeGetAccountsResult.info.extensionFound) {
97-
throw new Error(`No browser extension found`)
98-
}
99121
const ethereum = (window as any).ethereum
100122

101123
if (ethereum.chainId === chainData.chainId) {
@@ -115,7 +137,7 @@ export const addChainToMetamask = async (chainData: AddEthereumChainParameter):
115137
}
116138

117139

118-
export const UNIQUE_CHAINS_DATA_FOR_EXTENSIONS: Record<UNIQUE_CHAIN, AddEthereumChainParameter> = {
140+
const UNIQUE_CHAINS_DATA_FOR_EXTENSIONS: Record<UNIQUE_CHAIN, AddEthereumChainParameter> = {
119141
unique: {
120142
chainId: "0x22b0",
121143
chainName: 'Unique',
@@ -128,8 +150,6 @@ export const UNIQUE_CHAINS_DATA_FOR_EXTENSIONS: Record<UNIQUE_CHAIN, AddEthereum
128150
iconUrls: [`https://ipfs.unique.network/ipfs/QmbJ7CGZ2GxWMp7s6jy71UGzRsMe4w3KANKXDAExYWdaFR`],
129151
blockExplorerUrls: ['https://uniquescan.io/unique/'],
130152
},
131-
132-
133153
quartz: {
134154
chainId: "0x22b1",
135155
chainName: "Quartz by Unique",
@@ -168,20 +188,79 @@ export const UNIQUE_CHAINS_DATA_FOR_EXTENSIONS: Record<UNIQUE_CHAIN, AddEthereum
168188
},
169189
}
170190

171-
const AddUniqueChainToMetamask: Record<UNIQUE_CHAIN, () => Promise<void>> = {
172-
unique: () => addChainToMetamask(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.unique),
173-
quartz: () => addChainToMetamask(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.quartz),
174-
opal: () => addChainToMetamask(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.opal),
175-
sapphire: () => addChainToMetamask(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.sapphire),
191+
const addChain: Record<UNIQUE_CHAIN, () => Promise<void>> & { anyChain: (chainData: AddEthereumChainParameter) => Promise<void> } = {
192+
unique: () => addChainToExtension(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.unique),
193+
quartz: () => addChainToExtension(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.quartz),
194+
opal: () => addChainToExtension(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.opal),
195+
sapphire: () => addChainToExtension(UNIQUE_CHAINS_DATA_FOR_EXTENSIONS.sapphire),
196+
anyChain: (chainData: AddEthereumChainParameter) => addChainToExtension(chainData),
197+
}
198+
199+
const switchToChain = async (chainId: number | string): Promise<void> => {
200+
const windowIsOk = await documentReadyPromiseAndWindowIsOk()
201+
if (!windowIsOk) return
202+
203+
const parsedChainId: string = typeof chainId === 'string' ? chainId : '0x' + chainId.toString(16)
204+
205+
await (window as any).ethereum.request({method: 'wallet_switchEthereumChain', params: [{chainId: parsedChainId}]})
206+
}
207+
const switchChainTo: Record<UNIQUE_CHAIN, () => Promise<void>> & { anyChain: (chainId: number | string) => Promise<void> } = {
208+
unique: () => switchToChain(chainNameToChainId.unique),
209+
quartz: () => switchToChain(chainNameToChainId.quartz),
210+
opal: () => switchToChain(chainNameToChainId.opal),
211+
sapphire: () => switchToChain(chainNameToChainId.sapphire),
212+
anyChain: (chainId) => switchToChain(chainId)
213+
}
214+
215+
export type UpdateReason = 'account' | 'chain'
216+
217+
const subscribeOnChanges = (cb: (result: { reason: UpdateReason, chainId: number | null, address: string | null }) => void): (() => void) => {
218+
if (typeof window === 'undefined' || !(window as any).ethereum) {
219+
return () => undefined
220+
}
221+
222+
const ethereum = (window as any).ethereum
223+
224+
const getAccounts = (reason: UpdateReason) => {
225+
if (ethereum.chainId && ethereum.selectedAddress) {
226+
cb({reason, address: ethereum.selectedAddress, chainId: parseInt(ethereum.chainId, 16)})
227+
} else {
228+
getOrRequestAccounts().then(({chainId, address, error}) => {
229+
if (error) {
230+
throw error
231+
}
232+
233+
cb({reason, chainId, address})
234+
})
235+
}
236+
}
237+
238+
ethereum.on('accountsChanged', () => {
239+
getAccounts('account')
240+
})
241+
ethereum.on('chainChanged', () => {
242+
getAccounts('chain')
243+
})
244+
return () => {
245+
ethereum.removeListener('accountsChanged', getAccounts)
246+
ethereum.removeListener('networkChanged', getAccounts)
247+
}
176248
}
177249

178250
export type {UNIQUE_CHAIN}
179251

180252
export const Ethereum = {
181-
requestAccounts,
182-
getAccounts,
183-
addChainToMetamask,
184-
UNIQUE_CHAIN_IDS,
253+
getOrRequestAccounts,
254+
requestAccounts: () => getOrRequestAccounts(true),
255+
getAccounts: () => getOrRequestAccounts(),
256+
subscribeOnChanges,
257+
258+
chainNameToChainId,
259+
chainIdToChainName,
260+
261+
currentChainIs,
262+
addChain,
263+
switchChainTo,
264+
185265
UNIQUE_CHAINS_DATA_FOR_EXTENSIONS,
186-
AddUniqueChainToMetamask,
187266
}

src/ExtensionTools/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export default ExtensionTools
1616

1717
export type {
1818
AddEthereumChainParameter,
19-
IEthereumRequestAccountsResult,
19+
UNIQUE_CHAIN,
20+
UpdateReason,
21+
IEthereumAccountResult,
2022
} from './ethereum'
2123

2224
export type {

src/ExtensionTools/polkadot.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// =========================================
22
// Polkadot types
33
// =========================================
4+
import {documentReadyPromiseAndWindowIsOk} from './utils'
45
export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum'
56

67
interface InjectedAccount {
@@ -167,23 +168,12 @@ const knownPolkadotExtensions: IPolkadotExtensionGenericInfo[] = Object.entries(
167168
}
168169
}).sort((a, b) => compareTwoStrings(a.name, b.name))
169170

170-
171-
const documentReadyPromise = (): Promise<void> => {
172-
if (typeof window === 'undefined' || window.document.readyState === 'complete') {
173-
return Promise.resolve()
174-
} else {
175-
return new Promise<void>(resolve => window.addEventListener('load', () => resolve()));
176-
}
177-
}
178-
179-
180171
const isWeb3Environment = async (): Promise<boolean> => {
181-
if (typeof window === 'undefined') {
172+
const windowIsOk = await documentReadyPromiseAndWindowIsOk()
173+
if (!windowIsOk) {
182174
return false
183175
}
184176

185-
await documentReadyPromise()
186-
187177
const injectedWeb3 = (window as any).injectedWeb3
188178

189179
return !!injectedWeb3 && Object.keys(injectedWeb3).length !== 0

src/ExtensionTools/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const documentReadyPromiseAndWindowIsOk = (): Promise<boolean> => {
2+
if (typeof window === 'undefined') {
3+
return Promise.resolve(false)
4+
} else if(window.document.readyState === 'complete') {
5+
return Promise.resolve(true)
6+
} else {
7+
return new Promise<true>(resolve => window.addEventListener('load', () => resolve(true), {once: true}));
8+
}
9+
}

0 commit comments

Comments
 (0)