Skip to content

Commit 1b54c26

Browse files
Merge pull request #1496 from input-output-hk/feat/lw-11630-dapp-custom-extensions-and-inject
feat(dapp-connector): custom inject key, extensions and api
2 parents 622d766 + 15b68e6 commit 1b54c26

File tree

4 files changed

+137
-52
lines changed

4 files changed

+137
-52
lines changed

packages/dapp-connector/src/WalletApi/Cip30Wallet.ts

Lines changed: 71 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -32,54 +32,6 @@ export const CipMethodsMapping: Record<number, WalletMethod[]> = {
3232
};
3333
export const WalletApiMethodNames: WalletMethod[] = Object.values(CipMethodsMapping).flat();
3434

35-
/**
36-
* Wrap the proxy API object with a regular javascript object to avoid interop issues with some dApps.
37-
*
38-
* Only return the allowed API methods.
39-
*/
40-
const wrapAndEnableApi = (
41-
walletApi: WalletApi,
42-
enabledExtensions?: WalletApiExtension[]
43-
): Cip30WalletApiWithPossibleExtensions => {
44-
const baseApi: Cip30WalletApiWithPossibleExtensions = {
45-
// Add experimental.getCollateral to CIP-30 API
46-
experimental: {
47-
getCollateral: (params?: { amount?: Cbor }) => walletApi.getCollateral(params)
48-
},
49-
getBalance: () => walletApi.getBalance(),
50-
getChangeAddress: () => walletApi.getChangeAddress(),
51-
getCollateral: (params?: { amount?: Cbor }) => walletApi.getCollateral(params),
52-
getExtensions: () => Promise.resolve(enabledExtensions || []),
53-
getNetworkId: () => walletApi.getNetworkId(),
54-
getRewardAddresses: () => walletApi.getRewardAddresses(),
55-
getUnusedAddresses: () => walletApi.getUnusedAddresses(),
56-
getUsedAddresses: (paginate?: Paginate) => walletApi.getUsedAddresses(paginate),
57-
getUtxos: (amount?: Cbor, paginate?: Paginate) => walletApi.getUtxos(amount, paginate),
58-
signData: (addr: Cardano.PaymentAddress | Bytes, payload: Bytes) => walletApi.signData(addr, payload),
59-
signTx: (tx: Cbor, partialSign?: Boolean) => walletApi.signTx(tx, partialSign),
60-
submitTx: (tx: Cbor) => walletApi.submitTx(tx)
61-
};
62-
63-
const additionalCipApis: CipExtensionApis = {
64-
cip95: {
65-
getPubDRepKey: () => walletApi.getPubDRepKey(),
66-
getRegisteredPubStakeKeys: () => walletApi.getRegisteredPubStakeKeys(),
67-
getUnregisteredPubStakeKeys: () => walletApi.getUnregisteredPubStakeKeys()
68-
}
69-
};
70-
71-
if (enabledExtensions) {
72-
for (const extension of enabledExtensions) {
73-
const cipName = `cip${extension.cip}` as keyof CipExtensionApis;
74-
if (additionalCipApis[cipName]) {
75-
baseApi[cipName] = additionalCipApis[cipName];
76-
}
77-
}
78-
}
79-
80-
return baseApi;
81-
};
82-
8335
/** CIP30 API version */
8436
export type ApiVersion = string;
8537

@@ -93,7 +45,19 @@ export type WalletName = string;
9345
*/
9446
export type WalletIcon = string;
9547

96-
export type WalletProperties = { icon: WalletIcon; walletName: WalletName };
48+
export type WalletProperties = {
49+
icon: WalletIcon;
50+
walletName: WalletName;
51+
supportedExtensions?: WalletApiExtension[];
52+
/** Deviations from the CIP30 spec */
53+
cip30ApiDeviations?: {
54+
/**
55+
* Instead of throwing an error when no collateral is found, return empty array.
56+
* This is the way the Nami wallet works and some DApps rely on it (i.e. https://app.indigoprotocol.io/)
57+
*/
58+
getCollateralEmptyArray?: boolean;
59+
};
60+
};
9761

9862
export type WalletDependencies = {
9963
logger: Logger;
@@ -108,20 +72,26 @@ export class Cip30Wallet {
10872
readonly apiVersion: ApiVersion = '0.1.0';
10973
readonly name: WalletName;
11074
readonly icon: WalletIcon;
75+
/** Support the full api by default */
11176
readonly supportedExtensions: WalletApiExtension[] = [{ cip: 95 }];
11277

11378
readonly #logger: Logger;
11479
readonly #api: WalletApi;
11580
readonly #authenticator: RemoteAuthenticator;
81+
readonly #deviations: WalletProperties['cip30ApiDeviations'];
11682

11783
constructor(properties: WalletProperties, { api, authenticator, logger }: WalletDependencies) {
11884
this.icon = properties.icon;
11985
this.name = properties.walletName;
12086
this.#api = api;
12187
this.#logger = logger;
12288
this.#authenticator = authenticator;
89+
this.#deviations = properties.cip30ApiDeviations;
12390
this.enable = this.enable.bind(this);
12491
this.isEnabled = this.isEnabled.bind(this);
92+
if (properties.supportedExtensions) {
93+
this.supportedExtensions = properties.supportedExtensions;
94+
}
12595
}
12696

12797
#validateExtensions(extensions: WalletApiExtension[] = []): void {
@@ -173,9 +143,60 @@ export class Cip30Wallet {
173143
const extensions = options?.extensions?.filter(({ cip: requestedCip }) =>
174144
this.supportedExtensions.some(({ cip: supportedCip }) => supportedCip === requestedCip)
175145
);
176-
return wrapAndEnableApi(this.#api, extensions);
146+
return this.#wrapAndEnableApi(extensions);
177147
}
178148
this.#logger.debug(`${location.origin} not authorized to access wallet api`);
179149
throw new ApiError(APIErrorCode.Refused, 'wallet not authorized.');
180150
}
151+
152+
async #wrapGetCollateral(params?: { amount?: Cbor }) {
153+
const collateral = await this.#api.getCollateral(params);
154+
return this.#deviations?.getCollateralEmptyArray ? collateral ?? [] : collateral;
155+
}
156+
157+
/**
158+
* Wrap the proxy API object with a regular javascript object to avoid interop issues with some dApps.
159+
*
160+
* Only return the allowed API methods.
161+
*/
162+
#wrapAndEnableApi(enabledExtensions?: WalletApiExtension[]): Cip30WalletApiWithPossibleExtensions {
163+
const walletApi = this.#api;
164+
const baseApi: Cip30WalletApiWithPossibleExtensions = {
165+
// Add experimental.getCollateral to CIP-30 API
166+
experimental: {
167+
getCollateral: async (params?: { amount?: Cbor }) => this.#wrapGetCollateral(params)
168+
},
169+
getBalance: () => walletApi.getBalance(),
170+
getChangeAddress: () => walletApi.getChangeAddress(),
171+
getCollateral: (params?: { amount?: Cbor }) => this.#wrapGetCollateral(params),
172+
getExtensions: () => Promise.resolve(enabledExtensions || []),
173+
getNetworkId: () => walletApi.getNetworkId(),
174+
getRewardAddresses: () => walletApi.getRewardAddresses(),
175+
getUnusedAddresses: () => walletApi.getUnusedAddresses(),
176+
getUsedAddresses: (paginate?: Paginate) => walletApi.getUsedAddresses(paginate),
177+
getUtxos: (amount?: Cbor, paginate?: Paginate) => walletApi.getUtxos(amount, paginate),
178+
signData: (addr: Cardano.PaymentAddress | Bytes, payload: Bytes) => walletApi.signData(addr, payload),
179+
signTx: (tx: Cbor, partialSign?: Boolean) => walletApi.signTx(tx, partialSign),
180+
submitTx: (tx: Cbor) => walletApi.submitTx(tx)
181+
};
182+
183+
const additionalCipApis: CipExtensionApis = {
184+
cip95: {
185+
getPubDRepKey: () => walletApi.getPubDRepKey(),
186+
getRegisteredPubStakeKeys: () => walletApi.getRegisteredPubStakeKeys(),
187+
getUnregisteredPubStakeKeys: () => walletApi.getUnregisteredPubStakeKeys()
188+
}
189+
};
190+
191+
if (enabledExtensions) {
192+
for (const extension of enabledExtensions) {
193+
const cipName = `cip${extension.cip}` as keyof CipExtensionApis;
194+
if (additionalCipApis[cipName]) {
195+
baseApi[cipName] = additionalCipApis[cipName];
196+
}
197+
}
198+
}
199+
200+
return baseApi;
201+
}
181202
}

packages/dapp-connector/src/injectGlobal.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ import { Logger } from 'ts-log';
33

44
export type WindowMaybeWithCardano = Window & { cardano?: { [k: string]: Cip30Wallet } };
55

6-
export const injectGlobal = (window: WindowMaybeWithCardano, wallet: Cip30Wallet, logger: Logger): void => {
6+
export const injectGlobal = (
7+
window: WindowMaybeWithCardano,
8+
wallet: Cip30Wallet,
9+
logger: Logger,
10+
injectKey?: string
11+
): void => {
12+
injectKey = injectKey ?? wallet.name;
713
if (!window.cardano) {
814
logger.debug(
915
{
@@ -22,7 +28,7 @@ export const injectGlobal = (window: WindowMaybeWithCardano, wallet: Cip30Wallet
2228
'Cardano global scope exists'
2329
);
2430
}
25-
window.cardano[wallet.name] = window.cardano[wallet.name] || wallet;
31+
window.cardano[injectKey] = window.cardano[injectKey] || wallet;
2632
logger.debug(
2733
{
2834
module: 'injectWindow',

packages/dapp-connector/test/WalletApi/Cip30Wallet.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ describe('Wallet', () => {
1717
let authenticator: RemoteAuthenticator;
1818

1919
let wallet: Cip30Wallet;
20+
let walletNoExtensions: Cip30Wallet;
21+
let walletWithApiDeviations: Cip30Wallet;
2022

2123
beforeEach(async () => {
2224
await browser.storage.local.clear();
@@ -26,6 +28,24 @@ describe('Wallet', () => {
2628
authenticator,
2729
logger
2830
});
31+
32+
walletNoExtensions = new Cip30Wallet(
33+
{ ...testWallet.properties, supportedExtensions: [] },
34+
{
35+
api: testWallet.api,
36+
authenticator,
37+
logger
38+
}
39+
);
40+
41+
walletWithApiDeviations = new Cip30Wallet(
42+
{ ...testWallet.properties, cip30ApiDeviations: { getCollateralEmptyArray: true } },
43+
{
44+
api: testWallet.api,
45+
authenticator,
46+
logger
47+
}
48+
);
2949
});
3050

3151
test('constructed state', async () => {
@@ -41,6 +61,11 @@ describe('Wallet', () => {
4161
expect(typeof wallet.enable).toBe('function');
4262
});
4363

64+
test('constructed state without extensions', async () => {
65+
expect(walletNoExtensions.name).toBe(testWallet.properties.walletName);
66+
expect(walletNoExtensions.supportedExtensions).toEqual<WalletApiExtension[]>([]);
67+
});
68+
4469
it('should return initial api as plain javascript object', () => {
4570
// Verbose to enable easy detection of which are missing
4671
expect(wallet.hasOwnProperty('apiVersion')).toBe(true);
@@ -73,6 +98,13 @@ describe('Wallet', () => {
7398
expect(await api.getExtensions()).toEqual([{ cip: 95 }]);
7499
});
75100

101+
test('no extensions wallet cannot enable cip95 extension', async () => {
102+
const api = await walletNoExtensions.enable({ extensions: [{ cip: 95 }] });
103+
expect(await walletNoExtensions.isEnabled()).toBe(true);
104+
expect(await api.getExtensions()).toEqual([]);
105+
expect(api.cip95).toBeUndefined();
106+
});
107+
76108
test('change extensions after enabling once', async () => {
77109
const cip30api = await wallet.enable();
78110
const cip30methods = new Set(Object.keys(cip30api));
@@ -158,9 +190,11 @@ describe('Wallet', () => {
158190

159191
describe('api', () => {
160192
let api: Cip30WalletApiWithPossibleExtensions;
193+
let apiWithDeviations: Cip30WalletApiWithPossibleExtensions;
161194

162195
beforeAll(async () => {
163196
api = await wallet.enable();
197+
apiWithDeviations = await walletWithApiDeviations.enable();
164198
});
165199

166200
test('getNetworkId', async () => {
@@ -262,5 +296,15 @@ describe('Wallet', () => {
262296
const extensions = await api.getExtensions();
263297
expect(extensions).toEqual([]);
264298
});
299+
300+
test('getCollateral', async () => {
301+
expect(api.getCollateral).toBeDefined();
302+
expect(apiWithDeviations.getCollateral).toBeDefined();
303+
expect(typeof api.getCollateral).toBe('function');
304+
305+
const collateral = await api.getCollateral();
306+
expect(collateral).toEqual(null);
307+
expect(await apiWithDeviations.getCollateral()).toEqual([]);
308+
});
265309
});
266310
});

packages/dapp-connector/test/injectGlobal.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,5 +71,19 @@ describe('injectGlobal', () => {
7171
]);
7272
expect(window.cardano['another-obj']).toBe(anotherObj);
7373
});
74+
75+
it('injects the wallet public API using custom injection name', () => {
76+
const wallet = new Cip30Wallet(properties, { api, authenticator: stubAuthenticator(), logger });
77+
injectGlobal(window, wallet, logger, 'customKey');
78+
expect(window.cardano.customKey.name).toBe(properties.walletName);
79+
expect(Object.keys(window.cardano.customKey)).toEqual([
80+
'apiVersion',
81+
'supportedExtensions',
82+
'icon',
83+
'name',
84+
'enable',
85+
'isEnabled'
86+
]);
87+
});
7488
});
7589
});

0 commit comments

Comments
 (0)