Skip to content

Commit e316698

Browse files
committed
feat: track cip30 method call origin
add optional origin properties to sign method props, as well as cip30 callbacks
1 parent 468cfd5 commit e316698

File tree

13 files changed

+207
-102
lines changed

13 files changed

+207
-102
lines changed

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

+9
Original file line numberDiff line numberDiff line change
@@ -212,3 +212,12 @@ export interface CipExtensionApis {
212212
}
213213

214214
export type Cip30WalletApiWithPossibleExtensions = Cip30WalletApi & Partial<CipExtensionApis>;
215+
216+
export type DAppConnectorContext = { origin: string };
217+
type FnWithDAppConnectorContext<T> = T extends (...args: infer Args) => infer R
218+
? (context: DAppConnectorContext, ...args: Args) => R
219+
: T;
220+
221+
export type WithDAppConnectorContext<T> = {
222+
[K in keyof T]: FnWithDAppConnectorContext<T[K]>;
223+
};

packages/e2e/test/web-extension/extension/background/cip30.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ import { walletName } from '../const';
99

1010
// this should come from remote api
1111
const confirmationCallback: walletCip30.CallbackConfirmation = {
12-
signData: async () => true,
13-
signTx: async () => true,
12+
signData: async ({ context }) => {
13+
if (!context?.origin) throw new Error('No origin context');
14+
logger.info(`signData request from ${context.origin}`);
15+
return true;
16+
},
17+
signTx: async ({ context }) => {
18+
if (!context?.origin) throw new Error('No origin context');
19+
logger.info(`signTx request from ${context.origin}`);
20+
return true;
21+
},
1422
submitTx: async () => true
1523
};
1624

packages/key-management/src/cip8/cip30signData.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface Cip30SignDataRequest {
2424
witnesser: Bip32Ed25519Witnesser;
2525
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID;
2626
payload: HexBlob;
27+
origin?: string;
2728
}
2829

2930
export enum Cip30DataSignErrorCode {
@@ -87,10 +88,11 @@ const createSigStructureHeaders = (addressBytes: Uint8Array) => {
8788
const signSigStructure = (
8889
witnesser: Bip32Ed25519Witnesser,
8990
derivationPath: AccountKeyDerivationPath,
90-
sigStructure: SigStructure
91+
sigStructure: SigStructure,
92+
origin?: string
9193
) => {
9294
try {
93-
return witnesser.signBlob(derivationPath, util.bytesToHex(sigStructure.to_bytes()));
95+
return witnesser.signBlob(derivationPath, util.bytesToHex(sigStructure.to_bytes()), origin);
9496
} catch (error) {
9597
throw new Cip30DataSignError(Cip30DataSignErrorCode.UserDeclined, 'Failed to sign', error);
9698
}
@@ -115,7 +117,8 @@ export const cip30signData = async ({
115117
knownAddresses,
116118
witnesser,
117119
signWith,
118-
payload
120+
payload,
121+
origin
119122
}: Cip30SignDataRequest): Promise<Cip30DataSignature> => {
120123
if (Cardano.DRepID.isValid(signWith) && !Cardano.DRepID.canSign(signWith)) {
121124
throw new Cip30DataSignError(Cip30DataSignErrorCode.AddressNotPK, 'Invalid address');
@@ -129,7 +132,7 @@ export const cip30signData = async ({
129132
false
130133
);
131134
const sigStructure = builder.make_data_to_sign();
132-
const { signature, publicKey } = await signSigStructure(witnesser, derivationPath, sigStructure);
135+
const { signature, publicKey } = await signSigStructure(witnesser, derivationPath, sigStructure, origin);
133136
const coseSign1 = builder.build(Buffer.from(signature, 'hex'));
134137

135138
const coseKey = createCoseKey(addressBytes, publicKey);

packages/key-management/src/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export interface SignTransactionOptions {
141141
export interface SignTransactionContext {
142142
txInKeyPathMap: TxInKeyPathMap;
143143
knownAddresses: GroupedAddress[];
144+
origin?: string;
144145
}
145146

146147
export interface KeyAgent {
@@ -231,5 +232,5 @@ export interface Witnesser {
231232
/**
232233
* @throws AuthenticationError
233234
*/
234-
signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob): Promise<SignBlobResult>;
235+
signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob, origin?: string): Promise<SignBlobResult>;
235236
}

packages/key-management/src/util/createWitnesser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class Bip32Ed25519Witnesser implements Witnesser {
2525
return { signatures: await this.#keyAgent.signTransaction(txInternals, context, options) };
2626
}
2727

28-
async signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob): Promise<SignBlobResult> {
28+
async signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob, _origin?: string): Promise<SignBlobResult> {
2929
return this.#keyAgent.signBlob(derivationPath, blob);
3030
}
3131
}

packages/key-management/test/cip8/cip30signData.test.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
KeyRole,
99
cip8
1010
} from '../../src';
11+
import { Bip32Ed25519Witnesser } from '../../src/util';
1112
import { COSEKey, COSESign1, SigStructure } from '@emurgo/cardano-message-signing-nodejs';
1213
import { Cardano, util } from '@cardano-sdk/core';
1314
import { CoseLabel } from '../../src/cip8/util';
@@ -17,6 +18,7 @@ import { testAsyncKeyAgent, testKeyAgent } from '../mocks';
1718
describe('cip30signData', () => {
1819
const addressDerivationPath = { index: 0, type: AddressType.External };
1920
let keyAgent: KeyAgent;
21+
let witnesser: Bip32Ed25519Witnesser;
2022
let asyncKeyAgent: AsyncKeyAgent;
2123
let address: GroupedAddress;
2224
const cryptoProvider = new Crypto.SodiumBip32Ed25519();
@@ -26,17 +28,20 @@ describe('cip30signData', () => {
2628
keyAgent = await keyAgentReady;
2729
asyncKeyAgent = await testAsyncKeyAgent(undefined, keyAgentReady);
2830
address = await asyncKeyAgent.deriveAddress(addressDerivationPath, 0);
31+
witnesser = KeyManagementUtil.createBip32Ed25519Witnesser(asyncKeyAgent);
2932
});
3033

3134
const signAndDecode = async (
3235
signWith: Cardano.PaymentAddress | Cardano.RewardAccount,
33-
knownAddresses: GroupedAddress[]
36+
knownAddresses: GroupedAddress[],
37+
origin?: string
3438
) => {
3539
const dataSignature = await cip8.cip30signData({
3640
knownAddresses,
41+
origin,
3742
payload: HexBlob('abc123'),
3843
signWith,
39-
witnesser: KeyManagementUtil.createBip32Ed25519Witnesser(asyncKeyAgent)
44+
witnesser
4045
});
4146

4247
const coseKey = COSEKey.from_bytes(Buffer.from(dataSignature.key, 'hex'));
@@ -102,4 +107,11 @@ describe('cip30signData', () => {
102107
)
103108
).toBe(true);
104109
});
110+
111+
it('passes through origin to witnesser', async () => {
112+
const signBlobSpy = jest.spyOn(witnesser, 'signBlob');
113+
const origin = 'https://lace.io';
114+
await signAndDecode(address.address, [address], origin);
115+
expect(signBlobSpy).toBeCalledWith(expect.anything(), expect.anything(), origin);
116+
});
105117
});

packages/wallet/src/PersonalWallet/PersonalWallet.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -531,14 +531,15 @@ export class PersonalWallet implements ObservableWallet {
531531
return initializeTx(props, this.getTxBuilderDependencies());
532532
}
533533

534-
async finalizeTx({ tx, ...rest }: FinalizeTxProps, stubSign = false): Promise<Cardano.Tx> {
534+
async finalizeTx({ tx, origin, ...rest }: FinalizeTxProps, stubSign = false): Promise<Cardano.Tx> {
535535
const knownAddresses = await firstValueFrom(this.addresses$);
536536
const { tx: signedTx } = await finalizeTx(
537537
tx,
538538
{
539539
...rest,
540540
signingContext: {
541541
knownAddresses,
542+
origin,
542543
txInKeyPathMap: await util.createTxInKeyPathMap(tx.body, knownAddresses, this.util)
543544
}
544545
},

packages/wallet/src/cip30.ts

+19-10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Cbor,
66
Cip30DataSignature,
77
Cip95WalletApi,
8+
DAppConnectorContext,
89
DataSignError,
910
DataSignErrorCode,
1011
Paginate,
@@ -13,7 +14,8 @@ import {
1314
TxSignError,
1415
TxSignErrorCode,
1516
WalletApi,
16-
WalletApiExtension
17+
WalletApiExtension,
18+
WithDAppConnectorContext
1719
} from '@cardano-sdk/dapp-connector';
1820
import { Cardano, Serialization, TxCBOR, coalesceValueQuantities } from '@cardano-sdk/core';
1921
import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util';
@@ -36,13 +38,15 @@ export enum Cip30ConfirmationCallbackType {
3638

3739
export type SignDataCallbackParams = {
3840
type: Cip30ConfirmationCallbackType.SignData;
41+
context: DAppConnectorContext;
3942
data: {
4043
addr: Cardano.PaymentAddress | Cardano.DRepID;
4144
payload: HexBlob;
4245
};
4346
};
4447

4548
export type SignTxCallbackParams = {
49+
context: DAppConnectorContext;
4650
type: Cip30ConfirmationCallbackType.SignTx;
4751
data: Cardano.Tx;
4852
};
@@ -279,9 +283,10 @@ const baseCip30WalletApi = (
279283
}
280284
},
281285
// eslint-disable-next-line max-statements, sonarjs/cognitive-complexity,complexity
282-
getCollateral: async ({
283-
amount = new Serialization.Value(MAX_COLLATERAL_AMOUNT).toCbor()
284-
}: { amount?: Cbor } = {}): Promise<
286+
getCollateral: async (
287+
_: DAppConnectorContext,
288+
{ amount = new Serialization.Value(MAX_COLLATERAL_AMOUNT).toCbor() }: { amount?: Cbor } = {}
289+
): Promise<
285290
Cbor[] | null
286291
// eslint-disable-next-line sonarjs/cognitive-complexity, max-statements
287292
> => {
@@ -371,7 +376,7 @@ const baseCip30WalletApi = (
371376
logger.debug('getting unused addresses');
372377
return Promise.resolve([]);
373378
},
374-
getUsedAddresses: async (_paginate?: Paginate): Promise<Cbor[]> => {
379+
getUsedAddresses: async (): Promise<Cbor[]> => {
375380
logger.debug('getting used addresses');
376381

377382
const wallet = await firstValueFrom(wallet$);
@@ -383,7 +388,7 @@ const baseCip30WalletApi = (
383388
return addresses.map((groupAddresses) => cardanoAddressToCbor(groupAddresses.address));
384389
}
385390
},
386-
getUtxos: async (amount?: Cbor, paginate?: Paginate): Promise<Cbor[] | null> => {
391+
getUtxos: async (_: DAppConnectorContext, amount?: Cbor, paginate?: Paginate): Promise<Cbor[] | null> => {
387392
const scope = new ManagedFreeableScope();
388393
try {
389394
const wallet = await firstValueFrom(wallet$);
@@ -402,6 +407,7 @@ const baseCip30WalletApi = (
402407
}
403408
},
404409
signData: async (
410+
context: DAppConnectorContext,
405411
addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes,
406412
payload: Bytes
407413
): Promise<Cip30DataSignature> => {
@@ -411,6 +417,7 @@ const baseCip30WalletApi = (
411417

412418
const shouldProceed = await confirmationCallback
413419
.signData({
420+
context,
414421
data: {
415422
addr: signWith,
416423
payload: hexBlobPayload
@@ -422,14 +429,15 @@ const baseCip30WalletApi = (
422429
if (shouldProceed) {
423430
const wallet = await firstValueFrom(wallet$);
424431
return wallet.signData({
432+
origin: context.origin,
425433
payload: hexBlobPayload,
426434
signWith
427435
});
428436
}
429437
logger.debug('sign data declined');
430438
throw new DataSignError(DataSignErrorCode.UserDeclined, 'user declined signing');
431439
},
432-
signTx: async (tx: Cbor, partialSign?: Boolean): Promise<Cbor> => {
440+
signTx: async (context: DAppConnectorContext, tx: Cbor, partialSign?: Boolean): Promise<Cbor> => {
433441
const scope = new ManagedFreeableScope();
434442
logger.debug('signTx');
435443
const txDecoded = Serialization.Transaction.fromCbor(TxCBOR(tx));
@@ -438,6 +446,7 @@ const baseCip30WalletApi = (
438446
const coreTx = txDecoded.toCore();
439447
const shouldProceed = await confirmationCallback
440448
.signTx({
449+
context,
441450
data: coreTx,
442451
type: Cip30ConfirmationCallbackType.SignTx
443452
})
@@ -455,7 +464,7 @@ const baseCip30WalletApi = (
455464
);
456465
const {
457466
witness: { signatures }
458-
} = await wallet.finalizeTx({ tx: { ...coreTx, hash } });
467+
} = await wallet.finalizeTx({ origin: context.origin, tx: { ...coreTx, hash } });
459468

460469
// If partialSign is true, the wallet only tries to sign what it can. However, if
461470
// signatures size is 0 then throw.
@@ -484,7 +493,7 @@ const baseCip30WalletApi = (
484493
throw new TxSignError(TxSignErrorCode.UserDeclined, 'user declined signing tx');
485494
}
486495
},
487-
submitTx: async (input: Cbor): Promise<string> => {
496+
submitTx: async (_: DAppConnectorContext, input: Cbor): Promise<string> => {
488497
logger.debug('submitting tx');
489498
const { cbor, tx } = processTxInput(input);
490499
const shouldProceed = await confirmationCallback
@@ -570,7 +579,7 @@ export const createWalletApi = (
570579
wallet$: Observable<ObservableWallet>,
571580
confirmationCallback: CallbackConfirmation,
572581
{ logger }: Cip30WalletDependencies
573-
): WalletApi => ({
582+
): WithDAppConnectorContext<WalletApi> => ({
574583
...baseCip30WalletApi(wallet$, confirmationCallback, { logger }),
575584
...extendedCip95WalletApi(wallet$, { logger })
576585
});

packages/wallet/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface SyncStatus extends Shutdown {
4646
export type FinalizeTxProps = Omit<TxContext, 'signingContext'> & {
4747
tx: Cardano.TxBodyWithHash;
4848
signingOptions?: SignTransactionOptions;
49+
origin?: string;
4950
};
5051

5152
export type HandleInfo = HandleResolution & Asset.AssetInfo;

packages/wallet/test/PersonalWallet/methods.test.ts

+23-6
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,22 @@ describe('PersonalWallet methods', () => {
216216
expect(getPassphrase).not.toBeCalled();
217217
});
218218

219-
it('finalizeTx', async () => {
220-
const txInternals = await wallet.initializeTx(props);
221-
const tx = await wallet.finalizeTx({ tx: txInternals });
222-
expect(tx.body).toBe(txInternals.body);
223-
expect(tx.id).toBe(txInternals.hash);
224-
expect(tx.witness.signatures.size).toBe(2); // spending key and stake key for withdrawal
219+
describe('finalizeTx', () => {
220+
it('resolves with TransactionWitnessSet', async () => {
221+
const txInternals = await wallet.initializeTx(props);
222+
const tx = await wallet.finalizeTx({ tx: txInternals });
223+
expect(tx.body).toBe(txInternals.body);
224+
expect(tx.id).toBe(txInternals.hash);
225+
expect(tx.witness.signatures.size).toBe(2); // spending key and stake key for withdrawal
226+
});
227+
228+
it('passes through origin to witnesser', async () => {
229+
const origin = 'https://lace.io';
230+
const witnessSpy = jest.spyOn(witnesser, 'witness');
231+
const txInternals = await wallet.initializeTx(props);
232+
await wallet.finalizeTx({ origin, tx: txInternals });
233+
expect(witnessSpy).toBeCalledWith(expect.anything(), expect.objectContaining({ origin }), void 0);
234+
});
225235
});
226236

227237
describe('submitTx', () => {
@@ -421,6 +431,13 @@ describe('PersonalWallet methods', () => {
421431
expect(response).toHaveProperty('signature');
422432
});
423433

434+
it('passes through origin to witnesser', async () => {
435+
const origin = 'https://lace.io';
436+
const signBlobSpy = jest.spyOn(witnesser, 'signBlob');
437+
await wallet.signData({ origin, payload: HexBlob('abc123'), signWith: address });
438+
expect(signBlobSpy).toBeCalledWith(expect.anything(), expect.anything(), origin);
439+
});
440+
424441
test('rejects if bech32 DRepID is not a type 6 address', async () => {
425442
const dRepKey = await wallet.getPubDRepKey();
426443
for (const type in Cardano.AddressType) {

0 commit comments

Comments
 (0)