/
base.ts
289 lines (259 loc) · 8.13 KB
/
base.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
/* eslint-disable @typescript-eslint/ban-types */
import { Chain, defaultChains, updateChainRPCs } from "@thirdweb-dev/chains";
import { AsyncStorage, createAsyncLocalStorage } from "../../core/AsyncStorage";
import type { DAppMetaData } from "../../core/types/dAppMeta";
import { DEFAULT_DAPP_META } from "../constants/dappMeta";
import { EVMWallet } from "../interfaces";
import { ConnectParams, Connector } from "../interfaces/connector";
import { AbstractWallet } from "./abstract";
import { track } from "../utils/analytics";
/**
* General options required for creating a wallet instance
*/
export type WalletOptions<TOpts extends Record<string, any> = {}> = {
/**
* chains supported by the wallet
*/
chains?: Chain[];
/**
* Unique identifier for the wallet ( name of the wallet )
*/
walletId?: string;
/**
* Storage to use for saving the wallet data
*/
walletStorage?: AsyncStorage;
/**
* Metadata for the dapp. Some wallets use some of this data to display in their UI
*/
dappMetadata?: DAppMetaData;
/**
* thirdweb apiKey client id. This is required to use thirdweb's infrastructure services like RPCs, IPFS Storage etc.
*
* You can create an API key for free on [thirdweb Dashboard](https://thirdweb.com/create-api-key)
*/
clientId?: string;
/**
* Specify if analytics should be enabled or disabled for the wallet
*/
analytics?: "enabled" | "disabled";
} & TOpts;
export type WalletMeta = {
name: string;
iconURL: string;
urls?: {
android?: string;
ios?: string;
chrome?: string;
firefox?: string;
};
};
/**
* The base class for all client-side wallets (web, mobile) in the Wallet SDK. It extends AbstractWallet and adds client side specific logic.
* A client side wallet delegates the wallet-specific connection logic to a Connector.
*
* This wallet is not meant to be used directly, but instead be extended to [build your own wallet](https://portal.thirdweb.com/wallet-sdk/v2/build)
*
* @abstractWallet
*/
export abstract class AbstractClientWallet<
// eslint-disable-next-line @typescript-eslint/ban-types
TAdditionalOpts extends Record<string, any> = {},
// eslint-disable-next-line @typescript-eslint/ban-types
TConnectParams extends Record<string, any> = {},
> extends AbstractWallet {
walletId: string;
protected walletStorage;
protected chains;
protected dappMetadata: DAppMetaData;
protected options?: WalletOptions<TAdditionalOpts>;
private _connectParams: ConnectParams<TConnectParams> | undefined;
/**
* @internal
*/
static meta: WalletMeta;
/**
* @internal
*/
getMeta() {
return (this.constructor as typeof AbstractClientWallet).meta;
}
/**
* Creates an returns instance of `AbstractClientWallet`
*
* @param walletId - A Unique identifier for the wallet ( name of the wallet )
* @param options - Options for creating wallet instance
*/
constructor(walletId: string, options?: WalletOptions<TAdditionalOpts>) {
super();
this.walletId = walletId;
this.options = options;
this.chains = (options?.chains || defaultChains).map((c) =>
updateChainRPCs(c, options?.clientId),
);
this.dappMetadata = options?.dappMetadata || DEFAULT_DAPP_META;
this.walletStorage =
options?.walletStorage || createAsyncLocalStorage(this.walletId);
}
/**
* Returns the Wallet Connector used by the wallet
*/
protected abstract getConnector(): Promise<Connector<TConnectParams>>;
/**
* auto-connect the wallet if possible
* @returns
*/
async autoConnect(
connectOptions?: ConnectParams<TConnectParams>,
): Promise<string> {
// remove chainId when auto-connecting to prevent switch-network popup on page load
const options = connectOptions
? { ...connectOptions, chainId: undefined }
: undefined;
return this._connect(true, options);
}
/**
* Connect wallet
* @param connectOptions - Options for connecting to the wallet
* @returns
*/
async connect(
connectOptions?: ConnectParams<TConnectParams>,
): Promise<string> {
this._connectParams = connectOptions;
const address = await this._connect(false, connectOptions);
if (!address) {
throw new Error("Failed to connect to the wallet.");
}
return address;
}
/**
* @internal
* Get the options used for connecting to the wallet
* @returns
*/
getConnectParams() {
return this._connectParams;
}
/**
* @internal
* Get the options used for creating the wallet instance
*/
getOptions() {
return this.options;
}
protected async _connect(
isAutoConnect: boolean,
connectOptions?: ConnectParams<TConnectParams>,
) {
const connector = await this.getConnector();
this._subscribeToEvents(connector);
const isConnected = await connector.isConnected();
// if already connected, return the address and setup listeners
if (isConnected) {
const address = await connector.getAddress();
connector.setupListeners();
// ensure that connector is connected to the correct chain
if (connectOptions?.chainId) {
await connector.switchChain(connectOptions?.chainId);
}
this.emit("connect", {
address,
chainId: await this.getChainId(),
});
this._trackConnection(address);
return address;
}
if (isAutoConnect) {
throw new Error("Failed to auto connect to the wallet.");
}
try {
const address = await connector.connect(connectOptions);
this._trackConnection(address);
return address;
} catch (error) {
throw new Error((error as Error).message);
}
}
private _trackConnection(address: string) {
track({
clientId: this.options?.clientId || "",
source: "connectWallet",
action: "connect",
walletType: this.walletId,
walletAddress: address,
});
}
private async _subscribeToEvents(connector: Connector) {
// subscribe to connector for events
connector.on("connect", (data) => {
this.emit("connect", {
address: data.account,
chainId: data.chain?.id,
});
});
connector.on("change", (data) => {
this.emit("change", { address: data.account, chainId: data.chain?.id });
});
connector.on("message", (data) => {
this.emit("message", data);
});
connector.on("disconnect", async () => {
this.emit("disconnect");
});
connector.on("error", (error) => this.emit("error", error));
}
/**
* Get [ethers Signer](https://docs.ethers.org/v5/api/signer/) object of the connected wallet
*/
async getSigner() {
const connector = await this.getConnector();
if (!connector) {
throw new Error("Wallet not connected");
}
return await connector.getSigner();
}
/**
* Disconnect the wallet
*/
public async disconnect() {
const connector = await this.getConnector();
if (connector) {
await connector.disconnect();
this.emit("disconnect");
connector.removeAllListeners();
}
}
/**
* Switch to different Network/Blockchain in the connected wallet
* @param chainId - The chainId of the network to switch to
*/
async switchChain(chainId: number): Promise<void> {
const connector = await this.getConnector();
if (!connector) {
throw new Error("Wallet not connected");
}
if (!connector.switchChain) {
throw new Error("Wallet does not support switching chains");
}
return await connector.switchChain(chainId);
}
/**
* Update the chains supported by the wallet. This is useful if wallet was initialized with some chains and this needs to be updated without re-initializing the wallet
*/
async updateChains(chains: Chain[]) {
this.chains = chains.map((c) => {
return updateChainRPCs(c, this.options?.clientId);
});
const connector = await this.getConnector();
connector.updateChains(this.chains);
}
/**
* If the wallet uses another "personal wallet" under the hood, return it
*
* This is only useful for wallets like Safe or Smart Wallet uses a "personal wallet" under the hood to sign transactions. This method returns that wallet
*/
getPersonalWallet(): EVMWallet | undefined {
return undefined;
}
}