@@ -11,6 +11,7 @@ import AccountMetamask, { invokeSnap, snapId } from './Metamask.js';
11
11
12
12
const snapMinVersion = '0.0.9' ;
13
13
const snapMaxVersion = '0.1.0' ;
14
+ const metamaskVersionPrefix = 'MetaMask/v' ;
14
15
15
16
interface SnapDetails {
16
17
blocked : boolean ;
@@ -22,90 +23,138 @@ interface SnapDetails {
22
23
23
24
/**
24
25
* A factory class that generates instances of AccountMetamask.
26
+ * @see {@link https://www.npmjs.com/package/@aeternity-snap/plugin | Aeternity snap }
25
27
*/
26
28
export default class AccountMetamaskFactory extends AccountBaseFactory {
27
- readonly provider : BaseProvider ;
29
+ // TODO: remove after removing `provider`
30
+ #provider: BaseProvider | undefined ;
31
+
32
+ /**
33
+ * @deprecated this class is not intended to provide raw access to the provider
34
+ */
35
+ get provider ( ) : BaseProvider {
36
+ if ( this . #provider == null ) throw new UnsupportedPlatformError ( 'Metamask is not detected yet' ) ;
37
+ return this . #provider;
38
+ }
39
+
40
+ async #getMetamaskAsInjected( ) : Promise < BaseProvider | undefined > {
41
+ if ( ! ( 'ethereum' in window ) || window . ethereum == null ) return ;
42
+ const provider = window . ethereum as BaseProvider ;
43
+ const version = await provider . request < string > ( { method : 'web3_clientVersion' } ) ;
44
+ if ( version == null ) throw new InternalError ( "Can't get Ethereum Provider version" ) ;
45
+ if ( ! version . startsWith ( metamaskVersionPrefix ) ) return ;
46
+ return provider ;
47
+ }
48
+
49
+ async #getMetamaskOverEip6963( ) : Promise < BaseProvider | undefined > {
50
+ setTimeout ( ( ) => window . dispatchEvent ( new Event ( 'eip6963:requestProvider' ) ) ) ;
51
+ return new Promise < BaseProvider | undefined > ( ( resolve ) => {
52
+ const handler = (
53
+ event : CustomEvent < { info : { rdns : string } ; provider : BaseProvider } > ,
54
+ ) : void => {
55
+ if ( event . detail . info . rdns !== 'io.metamask' ) return ;
56
+ window . removeEventListener ( 'eip6963:announceProvider' , handler ) ;
57
+ resolve ( event . detail . provider ) ;
58
+ } ;
59
+ window . addEventListener ( 'eip6963:announceProvider' , handler ) ;
60
+ setTimeout ( ( ) => {
61
+ window . removeEventListener ( 'eip6963:announceProvider' , handler ) ;
62
+ resolve ( undefined ) ;
63
+ } , 500 ) ;
64
+ } ) ;
65
+ }
66
+
67
+ #providerPromise: Promise < BaseProvider > | undefined ;
68
+
69
+ async #getProvider( ) : Promise < BaseProvider > {
70
+ this . #providerPromise ??= ( async ( ) => {
71
+ this . #provider ??=
72
+ ( await this . #getMetamaskAsInjected( ) ) ?? ( await this . #getMetamaskOverEip6963( ) ) ;
73
+ if ( this . #provider == null ) {
74
+ throw new UnsupportedPlatformError (
75
+ "Can't find a Metamask extension as an injected provider and over EIP-6963. Ensure that Metamask is installed or setup a provider." ,
76
+ ) ;
77
+ }
78
+ const version = await this . #provider. request < string > ( { method : 'web3_clientVersion' } ) ;
79
+ if ( version == null ) throw new InternalError ( "Can't get Ethereum Provider version" ) ;
80
+ const args = [ version . slice ( metamaskVersionPrefix . length ) , '12.2.4' ] as const ;
81
+ if ( ! semverSatisfies ( ...args ) ) throw new UnsupportedVersionError ( 'Metamask' , ...args ) ;
82
+ return this . #provider;
83
+ } ) ( ) ;
84
+ return this . #providerPromise;
85
+ }
28
86
29
87
/**
30
88
* @param provider - Connection to MetaMask to use
31
89
*/
32
90
constructor ( provider ?: BaseProvider ) {
33
91
super ( ) ;
34
- if ( provider != null ) {
35
- this . provider = provider ;
36
- return ;
37
- }
38
- if ( window == null ) {
92
+ this . #provider = provider ;
93
+ if ( this . #provider == null && window == null ) {
39
94
throw new UnsupportedPlatformError (
40
95
'Window object not found, you can run AccountMetamaskFactory only in browser or setup a provider' ,
41
96
) ;
42
97
}
43
- if ( ! ( 'ethereum' in window ) || window . ethereum == null ) {
44
- throw new UnsupportedPlatformError (
45
- '`ethereum` object not found, you can run AccountMetamaskFactory only with Metamask enabled or setup a provider' ,
46
- ) ;
47
- }
48
- this . provider = window . ethereum as BaseProvider ;
49
98
}
50
99
51
100
/**
52
- * It throws an exception if MetaMask has an incompatible version.
101
+ * Request MetaMask to install Aeternity snap.
102
+ * @deprecated use `requestSnap` instead
53
103
*/
54
- async #ensureMetamaskSupported( ) : Promise < void > {
55
- const version = await this . provider . request < string > ( { method : 'web3_clientVersion' } ) ;
56
- if ( version == null ) throw new InternalError ( "Can't get Ethereum Provider version" ) ;
57
- const metamaskPrefix = 'MetaMask/v' ;
58
- if ( ! version . startsWith ( metamaskPrefix ) ) {
59
- throw new UnsupportedPlatformError ( `Expected Metamask, got ${ version } instead` ) ;
60
- }
61
- const args = [ version . slice ( metamaskPrefix . length ) , '12.2.4' ] as const ;
62
- if ( ! semverSatisfies ( ...args ) ) throw new UnsupportedVersionError ( 'Metamask' , ...args ) ;
104
+ async installSnap ( ) : Promise < SnapDetails > {
105
+ const provider = await this . #getProvider( ) ;
106
+ const details = ( await provider . request ( {
107
+ method : 'wallet_requestSnaps' ,
108
+ params : { [ snapId ] : { version : snapMinVersion } } ,
109
+ } ) ) as { [ key in typeof snapId ] : SnapDetails } ;
110
+ return details [ snapId ] ;
63
111
}
64
112
65
- #ensureReadyPromise?: Promise < void > ;
66
-
67
113
/**
68
- * Request MetaMask to install Aeternity snap.
114
+ * Request MetaMask to install Aeternity snap or connect it to the current aepp.
115
+ * MetaMask can have only one Aeternity snap version installed at a time.
116
+ * This method is intended to upgrade the snap to a specified version if needed by the aepp.
117
+ * If Aeternity snap is installed but wasn't used by the aepp, then the user still needs to approve the connection.
118
+ * If the currently installed version corresponds to the version range, then the snap won't be upgraded.
119
+ * To downgrade the snap, the user must manually uninstall the current version.
120
+ * @param version - Snap version range (e.g. `1`, `0.1.*`, `^0.0.9`, `~0.0.9`; `>=0.0.9 <0.1.0`)
121
+ * (default: a version range supported by sdk)
69
122
*/
70
- async installSnap ( ) : Promise < SnapDetails > {
71
- await this . #ensureMetamaskSupported ( ) ;
72
- const details = ( await this . provider . request ( {
123
+ async requestSnap ( version = `>= ${ snapMinVersion } < ${ snapMaxVersion } ` ) : Promise < SnapDetails > {
124
+ const provider = await this . #getProvider ( ) ;
125
+ const details = ( await provider . request ( {
73
126
method : 'wallet_requestSnaps' ,
74
- params : { [ snapId ] : { version : snapMinVersion } } ,
127
+ params : { [ snapId ] : { version } } ,
75
128
} ) ) as { [ key in typeof snapId ] : SnapDetails } ;
76
- this . #ensureReadyPromise = Promise . resolve ( ) ;
77
129
return details [ snapId ] ;
78
130
}
79
131
80
132
/**
81
133
* It throws an exception if MetaMask or Aeternity snap has an incompatible version or is not
82
- * installed.
134
+ * installed or is not connected to the aepp.
135
+ * @deprecated use `requestSnap` instead
83
136
*/
84
137
async ensureReady ( ) : Promise < void > {
85
138
const snapVersion = await this . getSnapVersion ( ) ;
86
139
const args = [ snapVersion , snapMinVersion , snapMaxVersion ] as const ;
87
140
if ( ! semverSatisfies ( ...args ) )
88
141
throw new UnsupportedVersionError ( 'Aeternity snap in MetaMask' , ...args ) ;
89
- this . #ensureReadyPromise = Promise . resolve ( ) ;
90
- }
91
-
92
- async #ensureReady( ) : Promise < void > {
93
- this . #ensureReadyPromise ??= this . ensureReady ( ) ;
94
- return this . #ensureReadyPromise;
95
142
}
96
143
97
144
/**
98
145
* @returns the version of snap installed in MetaMask
99
146
*/
100
147
async getSnapVersion ( ) : Promise < string > {
101
- await this . #ensureMetamaskSupported ( ) ;
102
- const snaps = ( await this . provider . request ( { method : 'wallet_getSnaps' } ) ) as Record <
148
+ const provider = await this . #getProvider ( ) ;
149
+ const snaps = ( await provider . request ( { method : 'wallet_getSnaps' } ) ) as Record <
103
150
string ,
104
151
{ version : string }
105
152
> ;
106
153
const version = snaps [ snapId ] ?. version ;
107
154
if ( version == null )
108
- throw new UnsupportedPlatformError ( 'Aeternity snap is not installed to MetaMask' ) ;
155
+ throw new UnsupportedPlatformError (
156
+ 'Aeternity snap is not installed to MetaMask or not connected to this aepp' ,
157
+ ) ;
109
158
return version ;
110
159
}
111
160
@@ -114,13 +163,14 @@ export default class AccountMetamaskFactory extends AccountBaseFactory {
114
163
* @param accountIndex - Index of account
115
164
*/
116
165
async initialize ( accountIndex : number ) : Promise < AccountMetamask > {
117
- await this . #ensureReady( ) ;
166
+ await this . requestSnap ( ) ;
167
+ const provider = await this . #getProvider( ) ;
118
168
const address = await invokeSnap < Encoded . AccountAddress > (
119
- this . provider ,
169
+ provider ,
120
170
'getPublicKey' ,
121
171
{ derivationPath : [ `${ accountIndex } '` , "0'" , "0'" ] } ,
122
172
'publicKey' ,
123
173
) ;
124
- return new AccountMetamask ( this . provider , accountIndex , address ) ;
174
+ return new AccountMetamask ( provider , accountIndex , address ) ;
125
175
}
126
176
}
0 commit comments