Skip to content

Commit

Permalink
core: improve network and account change detection
Browse files Browse the repository at this point in the history
**Summary**

This commit refactors the Connector interface and the Starknet context to better
track network and account changes. When the user changes the network/account
from the wallet, this change is reflected in the Starknet context.
  • Loading branch information
fracek committed Oct 25, 2023
1 parent 792a27a commit ca15368
Show file tree
Hide file tree
Showing 15 changed files with 501 additions and 163 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-trainers-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@starknet-react/core": patch
---

Improve network and account change detection
8 changes: 6 additions & 2 deletions packages/core/src/connectors/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export type ConnectorIcons = {
export type ConnectorData = {
/** Connector account. */
account?: string;
/** Connector network. */
chainId?: bigint;
};

/** Connector events. */
Expand All @@ -38,9 +40,11 @@ export abstract class Connector extends EventEmitter<ConnectorEvents> {
/** Whether connector is already authorized */
abstract ready(): Promise<boolean>;
/** Connect wallet. */
abstract connect(): Promise<AccountInterface>;
abstract connect(): Promise<ConnectorData>;
/** Disconnect wallet. */
abstract disconnect(): Promise<void>;
/** Get current account. */
abstract account(): Promise<AccountInterface | null>;
abstract account(): Promise<AccountInterface>;
/** Get current chain id. */
abstract chainId(): Promise<bigint>;
}
6 changes: 6 additions & 0 deletions packages/core/src/connectors/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
export { Connector } from "./base";
export { InjectedConnector, type InjectedConnectorOptions } from "./injected";

export {
MockConnector,
type MockConnectorAccounts,
type MockConnectorOptions,
} from "./mock";

import { InjectedConnector } from "./injected";

export function argent(): InjectedConnector {
Expand Down
99 changes: 73 additions & 26 deletions packages/core/src/connectors/injected.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { goerli, mainnet } from "@starknet-react/chains";
import { StarknetWindowObject } from "get-starknet-core";
import { AccountInterface } from "starknet";
import {
Expand All @@ -6,7 +7,7 @@ import {
UserNotConnectedError,
UserRejectedRequestError,
} from "../errors";
import { Connector, ConnectorIcons } from "./base";
import { Connector, ConnectorData, ConnectorIcons } from "./base";

/** Injected connector options. */
export interface InjectedConnectorOptions {
Expand Down Expand Up @@ -44,52 +45,62 @@ export class InjectedConnector extends Connector {
return this._wallet !== undefined;
}

async chainId(): Promise<bigint> {
this.ensureWallet();

if (!this._wallet) {
throw new ConnectorNotConnectedError();
}

const chainIdHex = await this._wallet.provider.getChainId();
const chainId = BigInt(chainIdHex);
return chainId;
}

async ready(): Promise<boolean> {
this.ensureWallet();

if (!this._wallet) return false;
return await this._wallet.isPreauthorized();
}

async connect(): Promise<AccountInterface> {
async connect(): Promise<ConnectorData> {
this.ensureWallet();

if (!this._wallet) {
throw new ConnectorNotFoundError();
}

let accounts;
try {
await this._wallet.enable({ starknetVersion: "v5" });
accounts = await this._wallet.enable({ starknetVersion: "v5" });
} catch {
// NOTE: Argent v3.0.0 swallows the `.enable` call on reject, so this won't get hit.
throw new UserRejectedRequestError();
}

if (!this._wallet.isConnected) {
if (!this._wallet.isConnected || !accounts) {
// NOTE: Argent v3.0.0 swallows the `.enable` call on reject, so this won't get hit.
throw new UserRejectedRequestError();
}

this._wallet.on("accountsChanged", (accounts: string[] | string) => {
let account;
if (typeof accounts === "string") {
account = accounts;
} else {
account = accounts[0];
}

if (account) {
this.emit("change", { account });
} else {
this.emit("disconnect");
}
this._wallet.on("accountsChanged", async (accounts: string[] | string) => {
await this.onAccountsChanged(accounts);
});

this._wallet.on("networkChanged", (_network?: string) => {
// TODO: Handle network change.
this._wallet.on("networkChanged", (network?: string) => {
this.onNetworkChanged(network);
});

return this._wallet.account;
await this.onAccountsChanged(accounts);

const account = this._wallet.account.address;
const chainId = await this.chainId();

return {
account,
chainId,
};
}

async disconnect(): Promise<void> {
Expand All @@ -102,19 +113,17 @@ export class InjectedConnector extends Connector {
if (!this._wallet?.isConnected) {
throw new UserNotConnectedError();
}

this.emit("disconnect");
}

async account(): Promise<AccountInterface | null> {
async account(): Promise<AccountInterface> {
this.ensureWallet();

if (!this._wallet) {
if (!this._wallet || !this._wallet.account) {
throw new ConnectorNotConnectedError();
}

if (!this._wallet.account) {
return null;
}

return this._wallet.account;
}

Expand All @@ -125,6 +134,44 @@ export class InjectedConnector extends Connector {
this._wallet = wallet;
}
}

private async onAccountsChanged(accounts: string[] | string): Promise<void> {
let account;
if (typeof accounts === "string") {
account = accounts;
} else {
account = accounts[0];
}

if (account) {
const chainId = await this.chainId();
this.emit("change", { account, chainId });
} else {
this.emit("disconnect");
}
}

private onNetworkChanged(network?: string): void {
switch (network) {
// Argent
case "SN_MAIN":
this.emit("change", { chainId: mainnet.id });
break;
case "SN_GOERLI":
this.emit("change", { chainId: goerli.id });
break;
// Braavos
case "mainnet-alpha":
this.emit("change", { chainId: mainnet.id });
break;
case "goerli-alpha":
this.emit("change", { chainId: goerli.id });
break;
default:
this.emit("change", {});
break;
}
}
}

// biome-ignore lint: window could contain anything
Expand Down
142 changes: 142 additions & 0 deletions packages/core/src/connectors/mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { goerli, mainnet } from "@starknet-react/chains";
import { AccountInterface } from "starknet";
import {
ConnectorNotConnectedError,
ConnectorNotFoundError,
UserRejectedRequestError,
} from "../errors";
import { Connector, ConnectorData, ConnectorIcons } from "./base";

export type MockConnectorOptions = {
/** The wallet id. */
id: string;
/** Wallet human readable name. */
name: string;
/** Wallet icons. */
icon: ConnectorIcons;
/** Whether the connector is available for use. */
available?: boolean;
/** Whether the connector should fail to connect. */
failConnect?: boolean;
/** Include account when switching chain. */
unifiedSwitchAccountAndChain?: boolean;
/** Emit change account event when switching chain. */
emitChangeAccountOnChainSwitch?: boolean;
};

export type MockConnectorAccounts = {
goerli: AccountInterface[];
mainnet: AccountInterface[];
};

export class MockConnector extends Connector {
private _accounts: MockConnectorAccounts;
private _accountIndex = 0;
private _options: MockConnectorOptions;
private _connected = false;
private _chainId: bigint = goerli.id;

constructor({
accounts,
options,
}: { accounts: MockConnectorAccounts; options: MockConnectorOptions }) {
super();

if (accounts.mainnet.length === 0 || accounts.goerli.length === 0) {
throw new Error("MockConnector: accounts must not be empty");
}

this._accounts = accounts;
this._options = options;
}

switchChain(chainId: bigint): void {
this._chainId = chainId;
this._accountIndex = 0;
let account;
if (this._options.unifiedSwitchAccountAndChain) {
account = this._account.address;
}

this.emit("change", { chainId, account });

if (this._options.emitChangeAccountOnChainSwitch ?? true) {
this.switchAccount(this._accountIndex);
}
}

switchAccount(accountIndex: number): void {
this._accountIndex = accountIndex;
this.emit("change", { account: this._account.address });
}

get id(): string {
return this._options.id;
}

get name(): string {
return this._options.name;
}

get icon(): ConnectorIcons {
return this._options.icon;
}

available(): boolean {
return this._options.available ?? true;
}

async chainId(): Promise<bigint> {
return this._chainId;
}

async ready(): Promise<boolean> {
return this._connected;
}

async connect(): Promise<ConnectorData> {
if (this._options.failConnect) {
throw new UserRejectedRequestError();
}

this._connected = true;

return {
account: this._account.address,
chainId: this._chainId,
};
}

async disconnect(): Promise<void> {
this._connected = false;

this.emit("disconnect");
}

async account(): Promise<AccountInterface> {
if (!this.available()) {
throw new ConnectorNotFoundError();
}

if (!this._connected) {
throw new ConnectorNotConnectedError();
}

return this._account;
}

private get _account(): AccountInterface {
let account;
if (this._chainId === mainnet.id) {
account = this._accounts.mainnet[this._accountIndex];
} else {
account = this._accounts.goerli[this._accountIndex];
}

if (!account) {
throw new ConnectorNotConnectedError();
}

return account;
}
}
Loading

0 comments on commit ca15368

Please sign in to comment.