Skip to content

Commit

Permalink
Abstract SignerConfigurator + PopUpConfigurator (#1136)
Browse files Browse the repository at this point in the history
* SignerConfigurator

* connection -> signer

* isCoinbaseWallet

* prettier

* PopUpConfigurator

* typecheck
  • Loading branch information
fan-zhang-sv authored Mar 20, 2024
1 parent 05d95b1 commit afcc0c4
Show file tree
Hide file tree
Showing 9 changed files with 308 additions and 266 deletions.
3 changes: 0 additions & 3 deletions packages/wallet-sdk/src/CoinbaseWalletProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ describe('EIP1193Provider', () => {

beforeEach(() => {
provider = new CoinbaseWalletProvider({
scwUrl: 'http://fooUrl.com',
appName: 'TestApp',
appChainIds: [],
smartWalletOnly: false,
Expand All @@ -27,7 +26,6 @@ describe('EIP1193Provider', () => {
describe('default chain id', () => {
it('uses the first chain id when appChainIds is not empty', () => {
const provider = new CoinbaseWalletProvider({
scwUrl: 'http://fooUrl.com',
appName: 'TestApp',
appChainIds: [8453, 84532],
smartWalletOnly: false,
Expand All @@ -37,7 +35,6 @@ describe('EIP1193Provider', () => {

it('fallback to 1 when appChainIds is empty', () => {
const provider = new CoinbaseWalletProvider({
scwUrl: 'http://fooUrl.com',
appName: 'TestApp',
appChainIds: [],
smartWalletOnly: false,
Expand Down
5 changes: 4 additions & 1 deletion packages/wallet-sdk/src/CoinbaseWalletProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { AccountsUpdate, ChainUpdate } from './sign/UpdateListenerInterface';
import { SubscriptionRequestHandler } from './subscription/SubscriptionRequestHandler';

interface ConstructorOptions {
scwUrl?: string;
appName: string;
appLogoUrl?: string | null;
appChainIds: number[];
Expand All @@ -36,6 +35,10 @@ export class CoinbaseWalletProvider extends EventEmitter implements ProviderInte
return this.chain.id;
}

public get isCoinbaseWallet() {
return true;
}

constructor(options: Readonly<ConstructorOptions>) {
super();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export abstract class CrossDomainCommunicator {
return this._connected;
}

protected set connected(value: boolean) {
this._connected = value;
}

protected abstract onConnect(): Promise<void>;
protected abstract onDisconnect(): void;
protected abstract onEvent(event: MessageEvent<Message>): void;
Expand All @@ -19,12 +23,13 @@ export abstract class CrossDomainCommunicator {
}

disconnect(): void {
this._connected = false;
this.connected = false;
this.onDisconnect();
}

protected peerWindow: Window | null = null;
protected postMessage(message: Message, options?: { bypassTargetOriginCheck: boolean }) {

postMessage(message: Message, options?: { bypassTargetOriginCheck: boolean }) {
let targetOrigin = this.url?.origin;
if (targetOrigin === undefined) {
if (options?.bypassTargetOriginCheck) {
Expand All @@ -33,6 +38,11 @@ export abstract class CrossDomainCommunicator {
throw standardErrors.rpc.internal('Communicator: No target origin');
}
}
this.peerWindow?.postMessage(message, targetOrigin);

if (!this.peerWindow) {
throw standardErrors.rpc.internal('Communicator: No peer window found');
}

this.peerWindow.postMessage(message, targetOrigin);
}
}
153 changes: 20 additions & 133 deletions packages/wallet-sdk/src/sign/SignRequestHandler.ts
Original file line number Diff line number Diff line change
@@ -1,132 +1,49 @@
import { SCWSigner } from './scw/SCWSigner';
import { ConnectionType } from './scw/transport/ConfigMessage';
import { PopUpCommunicator } from './scw/transport/PopUpCommunicator';
import { Signer, SignerUpdateListener } from './SignerInterface';
import { SignerConfigurator } from './SignerConfigurator';
import { SignRequestHandlerListener } from './UpdateListenerInterface';
import { WLSigner } from './walletlink/WLSigner';
import { CB_KEYS_URL } from ':core/constants';
import { standardErrorCodes, standardErrors } from ':core/error';
import { ScopedLocalStorage } from ':core/storage/ScopedLocalStorage';
import { AddressString } from ':core/type';
import { RequestArguments } from ':core/type/ProviderInterface';
import { RequestHandler } from ':core/type/RequestHandlerInterface';

interface SignRequestHandlerOptions {
scwUrl?: string;
appName: string;
appLogoUrl?: string | null;
appChainIds: number[];
smartWalletOnly: boolean;
updateListener: SignRequestHandlerListener;
}

const SIGNER_TYPE_KEY = 'SignerType';

export class SignRequestHandler implements RequestHandler {
private appName: string;
private appLogoUrl: string | null;
private appChainIds: number[];
private smartWalletOnly: boolean;

private connectionType: string | null;
private connectionTypeSelectionResolver: ((value: unknown) => void) | undefined;
private signer: Signer | undefined;

// should be encapsulated under ConnectorConfigurator
private signerTypeStorage = new ScopedLocalStorage('CBWSDK', 'SignRequestHandler');
private popupCommunicator: PopUpCommunicator;
private updateListener: SignRequestHandlerListener;
private signerConfigurator: SignerConfigurator;

constructor(options: Readonly<SignRequestHandlerOptions>) {
this.popupCommunicator = new PopUpCommunicator({
url: options.scwUrl || CB_KEYS_URL,
url: CB_KEYS_URL,
});
this.updateListener = options.updateListener;

// getWalletLinkUrl is called by the PopUpCommunicator when
// it receives message.type === 'wlQRCodeUrl' from the cb-wallet-scw popup
// its injected because we don't want to instantiate WalletLinkSigner until we have to
this.getWalletLinkUrl = this.getWalletLinkUrl.bind(this);
this.popupCommunicator.setWLQRCodeUrlCallback(this.getWalletLinkUrl);

this.appName = options.appName;
this.appLogoUrl = options.appLogoUrl ?? null;
this.appChainIds = options.appChainIds;

const persistedConnectionType = this.signerTypeStorage.getItem(SIGNER_TYPE_KEY);
this.connectionType = persistedConnectionType;
this.smartWalletOnly = options.smartWalletOnly;

if (persistedConnectionType) {
this.initSigner();
}

this.setConnectionType = this.setConnectionType.bind(this);
this.initWalletLinkSigner = this.initWalletLinkSigner.bind(this);
}

private readonly updateRelay: SignerUpdateListener = {
onAccountsUpdate: (signer, ...rest) => {
if (this.signer && signer !== this.signer) return; // ignore events from inactive signers
this.updateListener.onAccountsUpdate(...rest);
},
onChainUpdate: (signer, ...rest) => {
if (this.signer && signer !== this.signer) return; // ignore events from inactive signers
if (signer instanceof WLSigner) {
this.connectionTypeSelectionResolver?.('walletlink');
}
this.updateListener.onChainUpdate(...rest);
},
};

private initSigner = () => {
if (this.connectionType === 'scw') {
this.initScwSigner();
} else if (this.connectionType === 'walletlink') {
this.initWalletLinkSigner();
}
};

private initScwSigner() {
if (this.signer instanceof SCWSigner) return;
this.signer = new SCWSigner({
appName: this.appName,
appLogoUrl: this.appLogoUrl,
appChainIds: this.appChainIds,
puc: this.popupCommunicator,
updateListener: this.updateRelay,
this.signerConfigurator = new SignerConfigurator({
...options,
popupCommunicator: this.popupCommunicator,
});
}

private initWalletLinkSigner() {
if (this.signer instanceof WLSigner) return;

this.signer = new WLSigner({
appName: this.appName,
appLogoUrl: this.appLogoUrl,
updateListener: this.updateRelay,
});
}

async onDisconnect() {
this.connectionType = null;
this.signerTypeStorage.removeItem(SIGNER_TYPE_KEY);
await this.signer?.disconnect();
}

async handleRequest(request: RequestArguments, accounts: AddressString[]) {
try {
if (request.method === 'eth_requestAccounts') {
return await this.eth_requestAccounts(accounts);
}

if (!this.signer || accounts.length <= 0) {
if (!this.signerConfigurator.signer || accounts.length <= 0) {
throw standardErrors.provider.unauthorized(
"Must call 'eth_requestAccounts' before other methods"
);
}

return await this.signer.request(request);
return await this.signerConfigurator.signer.request(request);
} catch (err) {
if ((err as { code?: unknown })?.code === standardErrorCodes.provider.unauthorized) {
this.updateListener.onResetConnection();
Expand All @@ -135,27 +52,26 @@ export class SignRequestHandler implements RequestHandler {
}
}

async eth_requestAccounts(accounts: AddressString[]): Promise<AddressString[]> {
private async eth_requestAccounts(accounts: AddressString[]): Promise<AddressString[]> {
if (accounts.length > 0) {
this.updateListener.onConnect();
return Promise.resolve(accounts);
}

if (!this.connectionType) {
if (!this.signerConfigurator.signerType) {
// WL: this promise hangs until the QR code is scanned
// SCW: this promise hangs until the user signs in with passkey
const connectionType = await this.completeConnectionTypeSelection();
this.setConnectionType(connectionType as ConnectionType);
await this.signerConfigurator.completeSignerTypeSelection();
}

try {
// in the case of walletlink, this doesn't do anything since signer is initialized
// when the wallet link QR code url is requested
this.initSigner();
this.signerConfigurator.initSigner();

const ethAddresses = await this.signer?.handshake();
const ethAddresses = await this.signerConfigurator.signer?.handshake();
if (Array.isArray(ethAddresses)) {
if (this.connectionType === 'walletlink') {
if (this.signerConfigurator.signerType === 'walletlink') {
this.popupCommunicator.walletLinkQrScanned();
}
this.updateListener.onConnect();
Expand All @@ -164,47 +80,14 @@ export class SignRequestHandler implements RequestHandler {

return Promise.reject(standardErrors.rpc.internal('Failed to get accounts'));
} catch (err) {
if (this.connectionType === 'walletlink') {
if (this.signerConfigurator.signerType === 'walletlink') {
this.popupCommunicator.disconnect();
this.onDisconnect();
await this.onDisconnect();
}
throw err;
}
}

private getWalletLinkUrl() {
this.initWalletLinkSigner();
if (!(this.signer instanceof WLSigner)) {
throw standardErrors.rpc.internal(
'Signer not initialized or Signer.getWalletLinkUrl not defined'
);
}
return this.signer.getQRCodeUrl();
}

private async completeConnectionTypeSelection() {
await this.popupCommunicator.connect();

return new Promise((resolve) => {
this.connectionTypeSelectionResolver = resolve.bind(this);

this.popupCommunicator
.selectConnectionType({
smartWalletOnly: this.smartWalletOnly,
})
.then((connectionType) => {
resolve(connectionType);
});
});
}

// storage methods
private setConnectionType(connectionType: string) {
if (this.connectionType === connectionType) return;
this.connectionType = connectionType;
this.signerTypeStorage.setItem(SIGNER_TYPE_KEY, this.connectionType);
}

canHandleRequest(request: RequestArguments): boolean {
const methodsThatRequireSigning = [
'eth_requestAccounts',
Expand All @@ -226,4 +109,8 @@ export class SignRequestHandler implements RequestHandler {

return methodsThatRequireSigning.includes(request.method);
}

async onDisconnect() {
await this.signerConfigurator.onDisconnect();
}
}
Loading

0 comments on commit afcc0c4

Please sign in to comment.