dappress 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -82,7 +82,7 @@ env:
82
82
  DAPPRESS_SEED_PHRASE: ${{ secrets.DAPPRESS_SEED_PHRASE }}
83
83
  ```
84
84
 
85
- When neither is provided, Dappress uses the public Hardhat / Anvil development wallet.
85
+ When neither is provided, Dappress uses the public Hardhat / Anvil development wallet. Give your test suite a seed phrase of its own: that one is shared with everybody.
86
86
 
87
87
  Add the command types to `tsconfig.json`:
88
88
 
@@ -134,6 +134,9 @@ The wallet is imported once, before the first test of each spec. Each command wa
134
134
  | `cy.rejectSwitchNetwork()` | Denies that permission |
135
135
  | `cy.approveAddToken()` | Adds the token requested by `wallet_watchAsset` |
136
136
  | `cy.rejectAddToken()` | Rejects the token |
137
+ | `cy.addAccount()` | Adds an account to the wallet and selects it. Yields its name, such as `Account 2`. |
138
+ | `cy.importAccount(privateKey)` | Imports an account from its private key and selects it. Yields its name. The key stays out of the command log. |
139
+ | `cy.switchAccount(name)` | Selects an account of the wallet by its name |
137
140
  | `cy.useNetwork(network?)` | Moves the dapp onto a network, adding it to MetaMask when needed. Defaults to the network of the wallet setup. |
138
141
  | `cy.getAccountAddress()` | Yields the address the dapp is connected with |
139
142
  | `cy.setupMetaMask()` | Imports the wallet, or unlocks it. Called automatically before each spec. |
@@ -153,19 +156,20 @@ Settings normally live in `cypress/wallet.setup.ts`. Secrets go in `cypress.env.
153
156
  | `seedPhrase` | `DAPPRESS_SEED_PHRASE` | `test test test test test test test test test test test junk`, the Hardhat / Anvil development wallet. Its first account is `0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266`. |
154
157
  | `password` | `DAPPRESS_PASSWORD` | `Tester@1234`. It only protects the throwaway browser profile Cypress creates for each run. |
155
158
  | `network` | | None. The dapp stays on the network MetaMask starts on, Ethereum mainnet. |
159
+ | `backupAndSync` | | `false`. Dappress turns off MetaMask's backup and sync while importing the wallet, so that an account a test adds is not restored by the next import of the same seed phrase. With `true`, MetaMask keeps saving and restoring the accounts and contacts of that phrase. |
156
160
  | `autoSetup` | | `true`. Dappress imports or unlocks the wallet before the first test of each spec. Set to `false` to call `cy.setupMetaMask()` yourself. |
157
161
  | `cache` | | `false`. The wallet is imported in every run, about fifteen seconds. With `true`, it is imported once, in a browser Dappress opens before the run, and the resulting profile is reused by later runs, which then start by unlocking the wallet. |
158
162
  | `timeout` | | `20000` ms. The time allowed for MetaMask to display a request before a command fails. |
159
163
 
160
164
  ### Security
161
165
 
162
- Use a wallet dedicated to testing, funded on test networks only. The default seed phrase is public. The seed phrase and password remain on the Node.js side: they are never exposed to the browser nor written to the Cypress command log. The wallet setup file contains no secret and can be committed.
166
+ Use a wallet dedicated to testing, funded on test networks only. The default seed phrase is public: anyone can spend from it, and its accounts can carry activity nobody controls. It is fine for a first run, not for a test suite you rely on. The seed phrase and password remain on the Node.js side: they are never exposed to the browser nor written to the Cypress command log. The wallet setup file contains no secret and can be committed.
163
167
 
164
168
  With `cache: true`, the profile under `~/.cache/dappress/profiles` holds the wallet's vault, encrypted by MetaMask with the password. Treat that directory like the seed phrase: keep it on the machine, and do not store it in a CI cache that other people or workflows can restore.
165
169
 
166
170
  ## Conformance suite
167
171
 
168
- The suite runs one test per command against [MetaMask's test dapp](https://metamask.github.io/test-dapp/), with a local [Anvil](https://getfoundry.sh) node for the funded transaction. It requires Chrome for Testing and Foundry.
172
+ The suite runs one test per command against [MetaMask's test dapp](https://metamask.github.io/test-dapp/). Each run makes a wallet nobody has used, with Foundry's `cast`, and starts a local [Anvil](https://getfoundry.sh) node that funds it. It requires Chrome for Testing and Foundry.
169
173
 
170
174
  ```bash
171
175
  npm run conformance # default MetaMask version
@@ -209,10 +213,12 @@ Selectors are MetaMask's `data-testid` attributes, each with the button's Englis
209
213
 
210
214
  - **Cypress exits immediately with `MODULE_NOT_FOUND`.** The terminal sets `ELECTRON_RUN_AS_NODE=1`, as some IDEs do. Run `env -u ELECTRON_RUN_AS_NODE npx cypress run …`.
211
215
  - **"MetaMask showed no confirmation".** The dapp sent no request, or sent it on a network whose RPC endpoint is unreachable.
216
+ - **Adding or importing an account never finishes.** `chromeWebSecurity: false` is set in the Cypress config. MetaMask then cannot start its snaps, which its account screens wait for. Leave Chrome's web security on, the Cypress default.
217
+ - **The wallet shows accounts you did not create.** The seed phrase is used elsewhere, and MetaMask restored what its cloud holds for it. This happens with the public default phrase. Use a phrase made for your tests.
212
218
 
213
219
  ## Status
214
220
 
215
- Verified with MetaMask 13.49.0 and 13.50.0, Cypress 16 and Chrome for Testing 154 on macOS, in headed mode. Headless execution and continuous integration are not validated yet.
221
+ Verified with MetaMask 13.49.0 and 13.50.0, Cypress 16 and Chrome for Testing 154, in headed mode, on macOS and on GitHub's Linux runners. Headless execution is not validated yet.
216
222
 
217
223
  ## License
218
224
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dappress",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "MetaMask automation for Cypress: load the real extension, import a test wallet, answer dapp requests with cy.* commands. Tracks the latest MetaMask release.",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Lassaut",
package/src/actions.js CHANGED
@@ -27,6 +27,17 @@ function createTasks(options) {
27
27
  return state;
28
28
  }
29
29
 
30
+ /** A task that drives the wallet's own screens, from its full-screen page. */
31
+ const onHomePage = (flow) => async (browser, argument) => {
32
+ const home = await getHomePage(browser, await metamaskId(browser));
33
+ try {
34
+ await home.bringToFront();
35
+ return await flow(home, argument);
36
+ } finally {
37
+ await home.close();
38
+ }
39
+ };
40
+
30
41
  /** Approve the prompt a network change raised: "Add network" or the permission to switch. */
31
42
  async function approveNetworkChange(browser) {
32
43
  const page = await getConfirmationPage(browser, await metamaskId(browser), options.timeout);
@@ -39,7 +50,13 @@ function createTasks(options) {
39
50
  await metamask.decide(decision, page, options.timeout);
40
51
  };
41
52
 
42
- const actions = { setupWallet, approveNetworkChange };
53
+ const actions = {
54
+ setupWallet,
55
+ approveNetworkChange,
56
+ addAccount: onHomePage(metamask.addAccount),
57
+ switchAccount: onHomePage(metamask.switchAccount),
58
+ importAccount: onHomePage(metamask.importAccount),
59
+ };
43
60
  for (const decision of Object.keys(metamask.decisions)) actions[decision] = decide(decision);
44
61
 
45
62
  // cy.task() needs a value back: null when the action has nothing to say
package/src/config.js CHANGED
@@ -9,6 +9,9 @@ const DEFAULTS = {
9
9
  password: 'Tester@1234',
10
10
  // The network cy.connectToDapp() moves the dapp onto, as a wallet_addEthereumChain parameter; none by default
11
11
  network: null,
12
+ // MetaMask's backup and sync saves the accounts and contacts of a seed phrase
13
+ // and restores them on other installs. Off, a test's accounts don't come back.
14
+ backupAndSync: false,
12
15
  // Run cy.setupMetaMask() automatically before the first test of each spec
13
16
  autoSetup: true,
14
17
  // Import the wallet once and reuse the profile across runs (keeps the vault under cacheDir)
package/src/index.js CHANGED
@@ -16,6 +16,9 @@ const { createTasks } = require('./actions');
16
16
  */
17
17
  function configureDappress(on, config, userOptions = {}) {
18
18
  const options = resolveOptions(userOptions, config);
19
+ if (config.chromeWebSecurity === false) {
20
+ console.warn('[dappress] chromeWebSecurity is off: MetaMask cannot start its snaps, so adding or importing an account will hang');
21
+ }
19
22
 
20
23
  // Build the wallet profile before the run, out of the time Cypress allows a browser to come up
21
24
  on('before:run', async ({ browser }) => {
package/src/metamask.js CHANGED
@@ -30,6 +30,13 @@ const selectors = {
30
30
  metricsContinue: [testId('metametrics-i-agree'), buttonText('Continue')],
31
31
  downloadAppContinue: testId('download-app-continue'),
32
32
  done: [testId('onboarding-complete-done'), buttonText('Done')],
33
+ // "Manage default settings", offered next to "Done"
34
+ manageDefaultSettings: testId('manage-default-settings'),
35
+ // The first category, "General": its testid carries the translated label
36
+ generalSettings: 'xpath/(.//*[starts-with(@data-testid, "category-item-")])[1]',
37
+ backupAndSyncToggle: testId('backup-and-sync-toggle-button'),
38
+ categoryBack: testId('category-back-button'),
39
+ settingsBack: testId('privacy-settings-back-button'),
33
40
  },
34
41
  unlock: {
35
42
  password: [testId('unlock-password'), passwordInput(1)],
@@ -37,6 +44,18 @@ const selectors = {
37
44
  },
38
45
  home: {
39
46
  header: testId('parent-selector-header-navbar'),
47
+ accountMenu: testId('account-menu-icon'),
48
+ },
49
+ // The account list, opened from the home header
50
+ accounts: {
51
+ name: '[data-testid^="multichain-account-cell-name-"]',
52
+ cell: (name) => `xpath/.//*[contains(@class, "multichain-account-cell")][.//*[@data-testid="multichain-account-cell-name-${name}"]]`,
53
+ add: 'xpath/(.//*[@data-testid="add-multichain-account-button"])[1]',
54
+ addWallet: testId('account-list-add-wallet-button'),
55
+ importAccount: testId('choose-wallet-type-import-account'),
56
+ privateKey: '#private-key-box',
57
+ importConfirm: [testId('import-account-confirm-button'), buttonText('Import')],
58
+ back: testId('account-list-page-back-button'),
40
59
  },
41
60
  // "Connect this website with MetaMask"
42
61
  connect: {
@@ -98,7 +117,7 @@ async function walletState(page) {
98
117
  }
99
118
 
100
119
  /** Import a wallet from its seed phrase on a fresh MetaMask install. */
101
- async function onboard(page, { seedPhrase, password }) {
120
+ async function onboard(page, { seedPhrase, password, backupAndSync }) {
102
121
  const s = selectors.onboarding;
103
122
  await click(page, s.importWallet, { timeout: 30000 });
104
123
  await click(page, s.importWithSrp);
@@ -111,7 +130,7 @@ async function onboard(page, { seedPhrase, password }) {
111
130
  await click(page, s.passwordTerms);
112
131
  await clickWhenEnabled(page, s.passwordSubmit);
113
132
 
114
- await reachHome(page, { password });
133
+ await reachHome(page, { password, backupAndSync });
115
134
  }
116
135
 
117
136
  // MetaMask's sandbox (LavaMoat) blocks a scripted paste, so the phrase is
@@ -139,9 +158,10 @@ async function unlock(page, { password }) {
139
158
  * "Done", MetaMask takes a moment to record the onboarding as complete and
140
159
  * redirects to it until then, so the home page is reloaded.
141
160
  */
142
- async function reachHome(page, { password }) {
161
+ async function reachHome(page, { password, backupAndSync }) {
143
162
  const s = selectors.onboarding;
144
163
  const deadline = Date.now() + 90000;
164
+ let syncTurnedOff = false;
145
165
  while (Date.now() < deadline) {
146
166
  if (await isVisible(page, selectors.home.header, 500)) return;
147
167
  if (await isVisible(page, s.passkeyMaybeLater, 500)) await click(page, s.passkeyMaybeLater);
@@ -150,6 +170,10 @@ async function reachHome(page, { password }) {
150
170
  await click(page, s.metricsContinue);
151
171
  }
152
172
  if (await isVisible(page, s.downloadAppContinue, 500)) await click(page, s.downloadAppContinue);
173
+ if (backupAndSync === false && !syncTurnedOff && (await isVisible(page, s.manageDefaultSettings, 500))) {
174
+ await turnOffBackupAndSync(page);
175
+ syncTurnedOff = true;
176
+ }
153
177
  if (await isVisible(page, s.done, 500)) {
154
178
  await click(page, s.done);
155
179
  await sleep(1000);
@@ -163,6 +187,19 @@ async function reachHome(page, { password }) {
163
187
  throw await failure(page, 'MetaMask did not show the wallet');
164
188
  }
165
189
 
190
+ // Backup and sync restores, for a seed phrase, the accounts and contacts saved
191
+ // from other installs. Off, every import starts from the same state.
192
+ async function turnOffBackupAndSync(page) {
193
+ const s = selectors.onboarding;
194
+ await click(page, s.manageDefaultSettings);
195
+ await click(page, s.generalSettings);
196
+ const toggle = await waitFor(page, s.backupAndSyncToggle);
197
+ const on = await toggle.evaluate((el) => el.closest('.toggle-button')?.classList.contains('toggle-button--on') ?? el.className.includes('--on'));
198
+ if (on) await toggle.click();
199
+ await click(page, s.categoryBack);
200
+ await click(page, s.settingsBack);
201
+ }
202
+
166
203
  async function optOutOfMetrics(page) {
167
204
  const checkbox = await waitFor(page, selectors.onboarding.metricsCheckbox);
168
205
  const checked = await checkbox.evaluate((el) => el.getAttribute('data-checked') === 'true');
@@ -177,6 +214,69 @@ async function reloadHome(page) {
177
214
  await page.goto(homeUrl);
178
215
  }
179
216
 
217
+ /** Add an account to the wallet and select it. Yields its name, "Account N". */
218
+ async function addAccount(page) {
219
+ await click(page, selectors.home.accountMenu);
220
+ const before = await accountNames(page);
221
+ await clickWhenEnabled(page, selectors.accounts.add, { timeout: 30000 });
222
+ const deadline = Date.now() + 30000;
223
+ while (Date.now() < deadline) {
224
+ const [added] = (await accountNames(page)).filter((name) => !before.includes(name));
225
+ if (added) {
226
+ await selectAccount(page, added);
227
+ return added;
228
+ }
229
+ await sleep(250);
230
+ }
231
+ throw await failure(page, 'MetaMask added no account');
232
+ }
233
+
234
+ async function accountNames(page) {
235
+ await waitFor(page, selectors.accounts.name);
236
+ return page.$$eval(selectors.accounts.name, (cells) => cells.map((cell) => cell.textContent.trim()));
237
+ }
238
+
239
+ /** Make `name` the selected account of the wallet. */
240
+ async function switchAccount(page, name) {
241
+ await click(page, selectors.home.accountMenu);
242
+ await selectAccount(page, name);
243
+ }
244
+
245
+ // Picking an account closes the list and shows it in the home header
246
+ async function selectAccount(page, name) {
247
+ await click(page, selectors.accounts.cell(name));
248
+ const deadline = Date.now() + 15000;
249
+ while (Date.now() < deadline) {
250
+ if ((await selectedAccount(page)) === name) return;
251
+ await sleep(250);
252
+ }
253
+ throw await failure(page, `MetaMask did not select "${name}"`);
254
+ }
255
+
256
+ async function selectedAccount(page) {
257
+ const menu = await page.$(selectors.home.accountMenu);
258
+ return menu ? menu.evaluate((el) => el.textContent.trim()) : null;
259
+ }
260
+
261
+ /** Import an account from its private key and select it. Yields its name. */
262
+ async function importAccount(page, privateKey) {
263
+ await waitFor(page, selectors.home.accountMenu);
264
+ const before = await selectedAccount(page);
265
+ // Straight to the form the account list's "Add wallet" menu leads to
266
+ const homeUrl = page.url().split('#')[0];
267
+ await page.goto('about:blank');
268
+ await page.goto(`${homeUrl}#/add-wallet-page`);
269
+ await fill(page, selectors.accounts.privateKey, privateKey, { timeout: 30000 });
270
+ await clickWhenEnabled(page, selectors.accounts.importConfirm);
271
+ const deadline = Date.now() + 30000;
272
+ while (Date.now() < deadline) {
273
+ const selected = await selectedAccount(page);
274
+ if (selected && selected !== before) return selected;
275
+ await sleep(250);
276
+ }
277
+ throw await failure(page, 'MetaMask did not import the account');
278
+ }
279
+
180
280
  /** Press the button of `decision` on the confirmation shown on `page`. */
181
281
  function decide(decision, page, timeout) {
182
282
  return pressAndWaitForDismissal(page, decisions[decision], timeout);
@@ -191,7 +291,9 @@ function approveNetworkChange(page, timeout) {
191
291
  * Press a footer button, then wait for the confirmation to go away: the
192
292
  * popup closes, the side panel goes back to the home screen. A click that
193
293
  * lands while the confirmation is still settling is lost, so the click is
194
- * repeated while the button stays.
294
+ * repeated while the button stays. If it stays anyway, the error says
295
+ * whether it is the same request or a new one the dapp sent meanwhile, and
296
+ * what MetaMask logged.
195
297
  */
196
298
  async function pressAndWaitForDismissal(page, button, timeout) {
197
299
  await waitFor(page, button, { timeout });
@@ -199,14 +301,41 @@ async function pressAndWaitForDismissal(page, button, timeout) {
199
301
  if (await isVisible(page, selectors.confirmation.scrollToBottom, 500)) {
200
302
  await click(page, selectors.confirmation.scrollToBottom);
201
303
  }
202
- await clickWhenEnabled(page, button);
203
- for (let attempt = 0; attempt < 3; attempt++) {
204
- if (await isVisible(page, selectors.alert.acknowledge, 500)) await click(page, selectors.alert.acknowledge);
205
- if (await isGone(page, button, 3000)) return;
206
- await dismissModal(page);
207
- await dispatchClick(page, button, { timeout: 3000 }).catch(() => {});
304
+ const logged = recordErrors(page);
305
+ const request = page.url();
306
+ try {
307
+ await clickWhenEnabled(page, button);
308
+ for (let attempt = 0; attempt < 3; attempt++) {
309
+ if (await isVisible(page, selectors.alert.acknowledge, 500)) await click(page, selectors.alert.acknowledge);
310
+ if (await isGone(page, button, 3000)) return;
311
+ await dismissModal(page);
312
+ await dispatchClick(page, button, { timeout: 3000 }).catch(() => {});
313
+ }
314
+ const what = page.url() === request ? 'MetaMask kept the same request open' : 'MetaMask shows a new request: the dapp asked again';
315
+ const distinct = [...new Set(logged.errors)].slice(-3);
316
+ const errors = distinct.length ? `MetaMask logged: ${distinct.join(' | ')}` : 'MetaMask logged no error';
317
+ throw await failure(page, `"${describe(button)}" is still showing after being clicked. ${what}. ${errors}`);
318
+ } finally {
319
+ logged.stop();
208
320
  }
209
- throw await failure(page, `"${describe(button)}" is still showing after being clicked`);
321
+ }
322
+
323
+ /** Collect the errors MetaMask's page logs to its console, until stop(). */
324
+ function recordErrors(page) {
325
+ const errors = [];
326
+ const onConsole = (message) => {
327
+ if (message.type() === 'error') errors.push(message.text().slice(0, 300));
328
+ };
329
+ const onPageError = (error) => errors.push(String(error.message || error).slice(0, 300));
330
+ page.on('console', onConsole);
331
+ page.on('pageerror', onPageError);
332
+ return {
333
+ errors,
334
+ stop() {
335
+ page.off('console', onConsole);
336
+ page.off('pageerror', onPageError);
337
+ },
338
+ };
210
339
  }
211
340
 
212
341
  /** Close a modal shown over the confirmation (Escape, or its last button: "Cancel"). */
@@ -217,4 +346,4 @@ async function dismissModal(page) {
217
346
  await dispatchClick(page, selectors.modal.lastButton);
218
347
  }
219
348
 
220
- module.exports = { decisions, walletState, onboard, unlock, decide, approveNetworkChange };
349
+ module.exports = { decisions, walletState, onboard, unlock, addAccount, switchAccount, importAccount, decide, approveNetworkChange };
package/src/profile.js CHANGED
@@ -16,7 +16,7 @@ const builds = new Map();
16
16
 
17
17
  /** The cached profile for these options, built on first use. */
18
18
  function prepareProfile({ browserPath, extensionDir, options }) {
19
- const key = crypto.createHash('sha256').update([options.metamaskVersion, options.seedPhrase, options.password].join('|')).digest('hex').slice(0, 16);
19
+ const key = crypto.createHash('sha256').update([options.metamaskVersion, options.seedPhrase, options.password, options.backupAndSync].join('|')).digest('hex').slice(0, 16);
20
20
  if (!builds.has(key)) builds.set(key, buildProfile(path.join(options.cacheDir, 'profiles', key), { browserPath, extensionDir, options }));
21
21
  return builds.get(key);
22
22
  }
package/src/support.js CHANGED
@@ -7,8 +7,9 @@
7
7
 
8
8
  const options = Cypress.expose('dappress') || {};
9
9
 
10
- function metamask(action, argument = null) {
11
- Cypress.log({ name: 'metamask', message: argument ? `${action} ${argument}` : action });
10
+ // `shown` is what the command log displays for the argument, if anything
11
+ function metamask(action, argument = null, shown = '') {
12
+ Cypress.log({ name: 'metamask', message: `${action} ${shown}`.trim() });
12
13
  return cy.task(`dappress:${action}`, argument, { log: false });
13
14
  }
14
15
 
@@ -29,6 +30,12 @@ Cypress.Commands.add('rejectTransaction', () => metamask('rejectTransaction'));
29
30
  Cypress.Commands.add('approveAddToken', () => metamask('approveAddToken'));
30
31
  Cypress.Commands.add('rejectAddToken', () => metamask('rejectAddToken'));
31
32
 
33
+ // The wallet's own accounts. A new or imported account becomes the selected
34
+ // one; the dapp sees it once it connects with it.
35
+ Cypress.Commands.add('addAccount', () => metamask('addAccount'));
36
+ Cypress.Commands.add('switchAccount', (name) => metamask('switchAccount', name, name));
37
+ // The private key is kept out of the command log
38
+ Cypress.Commands.add('importAccount', (privateKey) => metamask('importAccount', privateKey));
32
39
 
33
40
  // Connect, then move the dapp onto the network from the wallet setup, if there is one
34
41
  Cypress.Commands.add('connectToDapp', () => {
package/types/index.d.ts CHANGED
@@ -23,6 +23,11 @@ export interface DappressOptions extends WalletSetup {
23
23
  password?: string;
24
24
  /** MetaMask release to load, downloaded from GitHub on first run. */
25
25
  metamaskVersion?: string;
26
+ /**
27
+ * Keep MetaMask's backup and sync on. It saves the accounts and contacts of a
28
+ * seed phrase and restores them on other installs. Default: false.
29
+ */
30
+ backupAndSync?: boolean;
26
31
  /** Run cy.setupMetaMask() before the first test of each spec. Default: true. */
27
32
  autoSetup?: boolean;
28
33
  /** Import the wallet once and reuse the profile across runs. Keeps the vault under cacheDir. Default: false. */
@@ -27,6 +27,12 @@ declare global {
27
27
  /** Answers wallet_watchAsset ("Add suggested tokens"). */
28
28
  approveAddToken(): Chainable<void>;
29
29
  rejectAddToken(): Chainable<void>;
30
+ /** Adds an account to the wallet and selects it. Yields its name, "Account N". */
31
+ addAccount(): Chainable<string>;
32
+ /** Selects an account of the wallet by its name. */
33
+ switchAccount(name: string): Chainable<void>;
34
+ /** Imports an account from its private key and selects it. Yields its name. */
35
+ importAccount(privateKey: string): Chainable<string>;
30
36
  /** Moves the dapp onto a network, adding it to MetaMask if needed. Defaults to the wallet setup's network. Yields the chain id. */
31
37
  useNetwork(network?: Network): Chainable<string>;
32
38
  /** The address the dapp is connected with. */