Skip to content

Commit

Permalink
feat(browser-extension-signing-manager): multiple extensions support
Browse files Browse the repository at this point in the history
- Adds `getExtensions` to get list of all web3 providers available
in injected window interface
- Adds `getAccountWithMeta` to get list of all account addresses
along with their metadata such name, type, genesisHash
- `BrowserSigningManager.create` now only enables the given extension
instead of enabling all available provider
  • Loading branch information
prashantasdeveloper committed Mar 21, 2023
1 parent 97ef900 commit cfb05c2
Show file tree
Hide file tree
Showing 6 changed files with 611 additions and 294 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"private": true,
"dependencies": {
"@polkadot/api": "^8.0.2",
"@polkadot/extension-dapp": "^0.42.7",
"@polkadot/extension-dapp": "^0.44.7",
"@polkadot/util": "^9.0.1",
"@polkadot/util-crypto": "^9.0.1",
"cross-fetch": "^3.1.5",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { web3Enable } from '@polkadot/extension-dapp';
import { InjectedAccount } from '@polkadot/extension-inject/types';
import { PolkadotSigner } from '@polymeshassociation/signing-manager-types';

import { Extension } from '../types';
import { changeAddressFormat } from '../utils';
import * as utilsModule from '../utils';
import { BrowserExtensionSigningManager } from './browser-extension-signing-manager';

jest.mock('@polkadot/extension-dapp', () => ({
Expand All @@ -12,78 +12,64 @@ jest.mock('@polkadot/extension-dapp', () => ({
describe('BrowserExtensionSigningManager Class', () => {
let signingManager: BrowserExtensionSigningManager;
let args: { appName: string; extensionName?: string };
const enableStub = web3Enable as jest.MockedFunction<typeof web3Enable>;
let enableStub: jest.SpyInstance;
const accountsGetStub = jest.fn();
const accountsSubscribeStub = jest.fn();
const networkSubscribeStub = jest.fn();

const accounts: InjectedAccount[] = [
{
name: 'ACCOUNT 1',
address: '5Ef2XHepJvTUJLhhx39Nf5iqu6AACrfFAmc6AW8a3hKF4Rdc',
genesisHash: 'someHash',
type: 'ed25519',
},
{
name: 'ACCOUNT 2',
address: '5HQLVKFYkytr9HisQRWoUArUWw8YNWUmhLdXztRFjqysiNUx',
genesisHash: 'someHash',
type: 'ed25519',
},
];

beforeAll(() => {
enableStub = jest.spyOn(utilsModule, 'enableWeb3Extension');
args = {
appName: 'testDApp',
extensionName: 'polywallet',
};
});

beforeEach(async () => {
enableStub.mockResolvedValue([
{
name: 'polywallet',
accounts: {
get: accountsGetStub,
subscribe: accountsSubscribeStub,
},
network: {
subscribe: networkSubscribeStub,
get: jest.fn(),
},
version: '1.5.5',
signer: 'signer' as unknown as PolkadotSigner,
enableStub.mockResolvedValue({
name: 'polywallet',
accounts: {
get: accountsGetStub,
subscribe: accountsSubscribeStub,
},
] as Extension[]);
signingManager = await BrowserExtensionSigningManager.create(args);

signingManager.setSs58Format(42);
network: {
subscribe: networkSubscribeStub,
get: jest.fn(),
},
version: '1.5.5',
signer: 'signer' as unknown as PolkadotSigner,
} as Extension);
});

describe('method: create', () => {
it('should throw an error if there is no extension with the passed name', () => {
enableStub.mockResolvedValue([
{
name: 'other-wallet',
},
] as Extension[]);

expect(BrowserExtensionSigningManager.create(args)).rejects.toThrow(
'There is no extension named "polywallet", or the extension has blocked the "testDApp" dApp'
);
});

it('should throw an error if there is more than one extension with the passed name', () => {
enableStub.mockResolvedValue([
{
name: 'polywallet',
},
{
name: 'polywallet',
},
] as Extension[]);

expect(
BrowserExtensionSigningManager.create({ ...args, extensionName: undefined })
).rejects.toThrow('There is more than one extension named "polywallet"');
it('should create instance of BrowserExtensionSigningManager', async () => {
expect(enableStub).toHaveBeenCalledTimes(1);
expect(enableStub).toHaveBeenCalledWith(args.appName, args.extensionName);
enableStub.mockClear();

await BrowserExtensionSigningManager.create({ appName: 'someOtherApp' });
expect(enableStub).toHaveBeenCalledTimes(1);
expect(enableStub).toHaveBeenCalledWith('someOtherApp', 'polywallet');
});
});

describe('method: getAccounts', () => {
it('should return all Accounts held in the extension, respecting the SS58 format', async () => {
const accounts = [
{
address: '5Ef2XHepJvTUJLhhx39Nf5iqu6AACrfFAmc6AW8a3hKF4Rdc',
},
{
address: '5HQLVKFYkytr9HisQRWoUArUWw8YNWUmhLdXztRFjqysiNUx',
},
];
accountsGetStub.mockResolvedValue(accounts);

let result = await signingManager.getAccounts();
Expand All @@ -94,7 +80,45 @@ describe('BrowserExtensionSigningManager Class', () => {

result = await signingManager.getAccounts();

expect(result).toEqual(accounts.map(({ address }) => changeAddressFormat(address, 0)));
expect(result).toEqual(
accounts.map(({ address }) => utilsModule.changeAddressFormat(address, 0))
);
});

it("should throw an error if the Signing Manager doesn't have a SS58 format", async () => {
signingManager = await BrowserExtensionSigningManager.create(args);

expect(signingManager.getAccounts()).rejects.toThrow(
"Cannot call 'getAccounts' before calling 'setSs58Format'. Did you forget to use this Signing Manager to connect with the Polymesh SDK?"
);
});
});

describe('method: getAccountsWithMeta', () => {
it('should return all Accounts along with its metadata held in the extension, respecting the SS58 format', async () => {
accountsGetStub.mockResolvedValue(accounts);

let result = await signingManager.getAccountsWithMeta();

expect(result).toEqual(
accounts.map(({ address, genesisHash, name, type }) => ({
address,
meta: { genesisHash, name, source: 'polywallet' },
type,
}))
);

signingManager.setSs58Format(0);

result = await signingManager.getAccountsWithMeta();

expect(result).toEqual(
accounts.map(({ address, genesisHash, name, type }) => ({
address: utilsModule.changeAddressFormat(address, 0),
meta: { genesisHash, name, source: 'polywallet' },
type,
}))
);
});

it("should throw an error if the Signing Manager doesn't have a SS58 format", async () => {
Expand All @@ -117,31 +141,29 @@ describe('BrowserExtensionSigningManager Class', () => {

describe('method: onAccountChange', () => {
it('should pass the new Accounts to the callback, respecting the SS58 format', () => {
const newAccounts = [
{
address: '5Ef2XHepJvTUJLhhx39Nf5iqu6AACrfFAmc6AW8a3hKF4Rdc',
},
{
address: '5HQLVKFYkytr9HisQRWoUArUWw8YNWUmhLdXztRFjqysiNUx',
},
];
accountsSubscribeStub.mockImplementation(cb => {
cb(newAccounts);
cb(accounts);
});

const callback = jest.fn();
signingManager.onAccountChange(callback);
signingManager.onAccountChange(callback, false);

expect(callback).toHaveBeenCalledWith(newAccounts.map(({ address }) => address));
expect(callback).toHaveBeenCalledWith(accounts.map(({ address }) => address));

signingManager.setSs58Format(0);

callback.mockReset();
signingManager.onAccountChange(callback);

expect(callback).toHaveBeenCalledWith(
newAccounts.map(({ address }) => changeAddressFormat(address, 0))
accounts.map(({ address }) => utilsModule.changeAddressFormat(address, 0))
);

callback.mockReset();
signingManager.onAccountChange(callback, true);
signingManager.setSs58Format(42);

expect(callback).toHaveBeenCalledWith(utilsModule.mapAccounts('polywallet', accounts, 42));
});

it("should throw an error if the Signing Manager doesn't have a SS58 format", async () => {
Expand Down Expand Up @@ -175,4 +197,18 @@ describe('BrowserExtensionSigningManager Class', () => {
expect(callback).toHaveBeenCalledWith(newNetwork);
});
});

describe('method: getExtensionList', () => {
it('should return the list of all available extensions', () => {
jest.spyOn(utilsModule, 'getExtensions').mockReturnValue({
polywallet: {
version: '5.4.1',
},
talisman: {
version: '1.1.0',
},
});
expect(BrowserExtensionSigningManager.getExtensionList()).toEqual(['polywallet', 'talisman']);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { web3Enable } from '@polkadot/extension-dapp';
import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types';
import { PolkadotSigner, SigningManager } from '@polymeshassociation/signing-manager-types';

import { Extension, NetworkInfo, UnsubCallback } from '../types';
import { changeAddressFormat } from '../utils';
import { changeAddressFormat, enableWeb3Extension, getExtensions, mapAccounts } from '../utils';

export class BrowserExtensionSigningManager implements SigningManager {
private _ss58Format?: number;
Expand All @@ -26,21 +26,8 @@ export class BrowserExtensionSigningManager implements SigningManager {
extensionName?: string;
}): Promise<BrowserExtensionSigningManager> {
const { appName, extensionName = 'polywallet' } = args;
const extensions = await web3Enable(appName);

const filteredExtensions = extensions.filter(({ name }) => name === extensionName);

if (filteredExtensions.length === 0) {
throw new Error(
`There is no extension named "${extensionName}", or the extension has blocked the "${appName}" dApp`
);
}

if (filteredExtensions.length > 1) {
throw new Error(`There is more than one extension named "${extensionName}"`);
}

return new BrowserExtensionSigningManager(filteredExtensions[0] as Extension);
const extension = await enableWeb3Extension(appName, extensionName);
return new BrowserExtensionSigningManager(extension as Extension);
}

private constructor(private readonly extension: Extension) {}
Expand All @@ -66,6 +53,20 @@ export class BrowserExtensionSigningManager implements SigningManager {
return accounts.map(({ address }) => changeAddressFormat(address, ss58Format));
}

/**
* Return the addresses of all Accounts along with other metadata in the Browser Wallet Extension
*
* @throws if called before calling `setSs58Format`. Normally, `setSs58Format` will be called by the SDK when instantiated
*/
public async getAccountsWithMeta(): Promise<InjectedAccountWithMeta[]> {
const ss58Format = this.getSs58Format('getAccounts');

const accounts = await this.extension.accounts.get();

// we make sure the addresses are returned in the correct SS58 format
return mapAccounts(this.extension.name, accounts, ss58Format);
}

/**
* Return a signer object that uses the extension Accounts to sign
*/
Expand All @@ -82,17 +83,28 @@ export class BrowserExtensionSigningManager implements SigningManager {
* Convention is for the current selected Account to be the first value in the array, but it
* depends on the extension implementation
*
* @param callbackWithMeta pass this value as true if the callback parameter expects `InjectedAccountWithMeta[]` as param
* @throws if the callback is triggered before calling `setSs58Format`. Normally, `setSs58Format` will be called by the SDK when instantiated
*/
public onAccountChange(cb: (accounts: string[]) => void): UnsubCallback {
public onAccountChange(
cb: (accounts: string[] | InjectedAccountWithMeta[]) => void,
callbackWithMeta = false
): UnsubCallback {
let ss58Format: number;

return this.extension.accounts.subscribe(accounts => {
if (!ss58Format) {
ss58Format = this.getSs58Format('onAccountChange callback');
}

cb(accounts.map(({ address }) => changeAddressFormat(address, ss58Format)));
let callbackAccounts;
if (callbackWithMeta) {
callbackAccounts = mapAccounts(this.extension.name, accounts, ss58Format);
} else {
callbackAccounts = accounts.map(({ address }) => changeAddressFormat(address, ss58Format));
}

cb(callbackAccounts);
});
}

Expand Down Expand Up @@ -121,4 +133,12 @@ export class BrowserExtensionSigningManager implements SigningManager {

return format;
}

/**
* Returns the list of all available extensions
*/
public static getExtensionList(): string[] {
const extensions = getExtensions();
return Object.keys(extensions);
}
}
Empty file.

0 comments on commit cfb05c2

Please sign in to comment.