Skip to content

Commit

Permalink
Merge pull request #907 from input-output-hk/feature/LW-8295-signData…
Browse files Browse the repository at this point in the history
…-DRepID-support

feat: add support for signing data with a DRepID in CIP-95 API
  • Loading branch information
lgobbi-atix committed Sep 20, 2023
2 parents 948d377 + 3057cce commit 1daae5f
Show file tree
Hide file tree
Showing 16 changed files with 225 additions and 22 deletions.
29 changes: 29 additions & 0 deletions packages/core/src/Cardano/Address/DRepID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Address, AddressType } from './Address';
import { OpaqueString, assertIsBech32WithPrefix, typedBech32 } from '@cardano-sdk/util';
/**
* DRepID as bech32 string
*/
export type DRepID = OpaqueString<'DRepID'>;

/**
* @param {string} value DRepID as bech32 string
* @throws InvalidStringError
*/
export const DRepID = (value: string): DRepID => typedBech32(value, ['drep']);

DRepID.isValid = (value: string): boolean => {
try {
assertIsBech32WithPrefix(value, 'drep');
return true;
} catch {
return false;
}
};

DRepID.canSign = (value: string): boolean => {
try {
return DRepID.isValid(value) && Address.fromBech32(value).getType() === AddressType.EnterpriseKey;
} catch {
return false;
}
};
5 changes: 3 additions & 2 deletions packages/core/src/Cardano/Address/PaymentAddress.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Address, AddressType } from './Address';
import { DRepID } from './DRepID';
import {
HexBlob,
InvalidStringError,
Expand Down Expand Up @@ -37,7 +38,7 @@ const isRewardAccount = (address: string) => {
*/
export const PaymentAddress = (value: string): PaymentAddress => {
if (Address.isValid(value)) {
if (isRewardAccount(value)) {
if (isRewardAccount(value) || DRepID.isValid(value)) {
throw new InvalidStringError(value, 'Address type can only be used for payment addresses');
}
return value as unknown as PaymentAddress;
Expand Down Expand Up @@ -87,7 +88,7 @@ export interface InputResolver {
* @param address The address to get the network id from.
* @returns The network ID.
*/
export const addressNetworkId = (address: RewardAccount | PaymentAddress): NetworkId => {
export const addressNetworkId = (address: RewardAccount | PaymentAddress | DRepID): NetworkId => {
const addr = Address.fromString(address);
return addr!.getNetworkId();
};
1 change: 1 addition & 0 deletions packages/core/src/Cardano/Address/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './Address';
export * from './BaseAddress';
export * from './ByronAddress';
export * from './DRepID';
export * from './EnterpriseAddress';
export * from './PaymentAddress';
export * from './PointerAddress';
Expand Down
32 changes: 32 additions & 0 deletions packages/core/test/Cardano/Address/DRepID.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DRepID } from '../../../src/Cardano';
import { InvalidStringError } from '@cardano-sdk/util';

describe('Cardano/Address/DRepID', () => {
it('DRepID() accepts a valid bech32 string with drep as prefix', () => {
expect(() => DRepID('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).not.toThrow();
});

it('DRepID() throws an error if the bech32 string has the wrong prefix', () => {
expect(() => DRepID('addr_test1vpudzrw5uq46qwl6h5szlc66fydr0l2rlsw4nvaaxfld40g3ys07c')).toThrowError(
InvalidStringError
);
});

describe('isValid', () => {
it('is true if string is a valid DRepID', () => {
expect(DRepID.isValid('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).toBe(true);
});
it('is false if string is not a valid DRepID', () => {
expect(DRepID.isValid('addr_test1vpudzrw5uq46qwl6h5szlc66fydr0l2rlsw4nvaaxfld40g3ys07c')).toBe(false);
});
});

describe('canSign', () => {
it('is true if DRepID is a valid type 6 address', () => {
expect(DRepID.canSign('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).toBe(true);
});
it('is false if DRepID is not a type 6 address', () => {
expect(DRepID.canSign('drep1wpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9qcluy2z')).toBe(false);
});
});
});
18 changes: 18 additions & 0 deletions packages/core/test/Cardano/Address/PaymentAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ describe('PaymentAddress', () => {
).toThrowError(InvalidStringError);
});

it('PaymentAddress() throws an error when passing a DRepID', () => {
expect(() => Cardano.PaymentAddress('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')).toThrowError(
InvalidStringError
);
});

describe('addressNetworkId', () => {
it('parses testnet address', () => {
expect(
Expand Down Expand Up @@ -97,6 +103,18 @@ describe('PaymentAddress', () => {
)
).toBe(Cardano.NetworkId.Mainnet);
});

it('parses testnet DRepID', () => {
expect(
Cardano.addressNetworkId(Cardano.DRepID('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz'))
).toBe(Cardano.NetworkId.Testnet);
});

it('parses mainnet DRepID', () => {
expect(
Cardano.addressNetworkId(Cardano.DRepID('drep1v9gkc6jge96t40w46592tahq94n2rzhdhk2puvtz3dsfzys04jeym'))
).toBe(Cardano.NetworkId.Mainnet);
});
});

describe('from hex-encoded bytes', () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/dapp-connector/src/WalletApi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ export type SignTx = (tx: Cbor, partialSign?: Boolean) => Promise<Cbor>;
* @throws ApiError
* @throws DataSignError
*/
export type SignData = (addr: Cardano.PaymentAddress | Bytes, payload: Bytes) => Promise<Cip30DataSignature>;
export type SignData = (
addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes,
payload: Bytes
) => Promise<Cip30DataSignature>;

/**
* As wallets should already have this ability, we allow dApps to request that a transaction be sent through it.
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/test/web-extension/extension/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ export const selectors = {
btnDelegate: '#multiDelegation .delegate button',
btnGrantAccess: '#requestAccessGrant',
btnSignAndBuildTx: '#buildAndSignTx',
btnSignDataWithDRepId: '#signDataWithDRepId',
deactivateWallet: '#deactivateWallet',
destroyWallet: '#destroyWallet',
divAdaPrice: '#adaPrice',
divBgPortDisconnectStatus: '#remoteApiPortDisconnect .bgPortDisconnect',
divDataSignature: '#dataSignature',
divSignature: '#signature',
divUiPortDisconnectStatus: '#remoteApiPortDisconnect .uiPortDisconnect',
liPercents: '#multiDelegation .distribution li .percent',
Expand Down
3 changes: 3 additions & 0 deletions packages/e2e/test/web-extension/extension/ui.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ <h3>Delegation distribution:</h3>
</div>
<button id="buildAndSignTx">Build & Sign TX</button>
<div>Signature: <span id="signature">-</span></div>

<button id="signDataWithDRepId">Sign Data with DRepID</button>
<div>Signature: <span id="dataSignature">-</span></div>
<script src="ui.js"></script>
</body>

Expand Down
22 changes: 22 additions & 0 deletions packages/e2e/test/web-extension/extension/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { bip32Ed25519Factory, keyManagementFactory } from '../../../src';

import { Cardano } from '@cardano-sdk/core';
import { HexBlob } from '@cardano-sdk/util';
import { combineLatest, firstValueFrom, of } from 'rxjs';
import { runtime } from 'webextension-polyfill';
import { setupWallet } from '@cardano-sdk/wallet';
Expand Down Expand Up @@ -113,6 +114,23 @@ const sendDelegationTx = async (portfolio: { pool: Cardano.StakePool; weight: nu
document.querySelector('#multiDelegation .delegateTxId')!.textContent = msg;
};

const signDataWithDRepID = async (): Promise<void> => {
let msg: string;
const dRepId = 'drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz';
try {
const signature = await wallet.signData({
payload: HexBlob('abc123'),
signWith: Cardano.PaymentAddress(dRepId)
});
msg = JSON.stringify(signature);
} catch (error) {
msg = `ERROR signing data with DRepID: ${JSON.stringify(error)}`;
}

// Set text with signature or error
document.querySelector(selectors.divDataSignature)!.textContent = msg;
};

const setAddresses = ({ address, stakeAddress }: { address: string; stakeAddress: string }): void => {
document.querySelector(selectors.spanAddress)!.textContent = address;
document.querySelector(selectors.spanStakeAddress)!.textContent = stakeAddress;
Expand Down Expand Up @@ -262,6 +280,10 @@ document.querySelector(selectors.btnSignAndBuildTx)!.addEventListener('click', a
setSignature(signedTx.witness.signatures.values().next().value);
});

document
.querySelector(selectors.btnSignDataWithDRepId)!
.addEventListener('click', async () => await signDataWithDRepID());

// Code below tests that a disconnected port in background script will result in the consumed API method call promise to reject
// UI consumes API -> BG exposes fake API that closes port
const disconnectPortTestObj = consumeRemoteApi(
Expand Down
2 changes: 1 addition & 1 deletion packages/e2e/test/web-extension/specs/dapp-cip95.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('dapp/cip95', () => {
it('getPubDRepKey gets the DRep key from cip95 wallet api', async () => {
const dappDrepKey = await $(dappGetPubDrepKey).getText();
expect(dappDrepKey.length).toBeGreaterThan(0);
await expect($(dappDrepId)).toHaveTextContaining('drep_id');
await expect($(dappDrepId)).toHaveTextContaining('drep');
});
});
});
16 changes: 15 additions & 1 deletion packages/e2e/test/web-extension/specs/wallet.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ describe('wallet', () => {
liPools,
liPercents,
divBgPortDisconnectStatus,
divUiPortDisconnectStatus
divUiPortDisconnectStatus,
btnSignDataWithDRepId,
divDataSignature
} = selectors;

// The address is filled in by the tests, which are order dependent
Expand Down Expand Up @@ -135,6 +137,18 @@ describe('wallet', () => {
await buildAndSign();
});

it('can sign data with a DRepID', async () => {
(await $(btnSignDataWithDRepId)).click();
const signature = await $(divDataSignature).getText();
expect(signature).toHaveTextContaining(
JSON.stringify({
key: 'a5010102581d60a7484b9d9185af363f9412627c42f47c7ae14e95b3a4603f4c34860403272006215820a76722da33bcd685429f4aca04e57fd1366a0b3410770fc0f5c161934b8ba1af',
signature:
'84584aa3012704581d60a7484b9d9185af363f9412627c42f47c7ae14e95b3a4603f4c3486046761646472657373581d60a7484b9d9185af363f9412627c42f47c7ae14e95b3a4603f4c348604a166686173686564f443abc1235840ea25fdcd108a591e67987de272b8c822cd2f9cf621ff1938db594cafb1cfdb879de42a81dab5698c41dd968515583a50d12abc4bbee356a2d6ac97e54e3a680f'
})
);
});

it('can destroy second wallet before switching back to the first wallet', async () => {
// Destroy also clears associated store. Store will be rebuilt during future activation of same wallet
await $(destroyWallet).click();
Expand Down
18 changes: 14 additions & 4 deletions packages/key-management/src/cip8/cip30signData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,12 @@ import { Cardano, util } from '@cardano-sdk/core';
import { Cip30DataSignature } from '@cardano-sdk/dapp-connector';
import { ComposableError, HexBlob } from '@cardano-sdk/util';
import { CoseLabel } from './util';
import { STAKE_KEY_DERIVATION_PATH } from '../util';
import { DREP_KEY_DERIVATION_PATH, STAKE_KEY_DERIVATION_PATH } from '../util';
import { filter, firstValueFrom } from 'rxjs';

export interface Cip30SignDataRequest {
keyAgent: AsyncKeyAgent;
signWith: Cardano.PaymentAddress | Cardano.RewardAccount;
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID;
payload: HexBlob;
}

Expand All @@ -39,7 +39,7 @@ export class Cip30DataSignError<InnerError = unknown> extends ComposableError<In
}
}

const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccount) => {
const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID) => {
const address = Cardano.Address.fromString(signWith);

if (!address) {
Expand All @@ -49,7 +49,14 @@ const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccoun
return Buffer.from(address.toBytes(), 'hex');
};

const getDerivationPath = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount, keyAgent: AsyncKeyAgent) => {
const getDerivationPath = async (
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID,
keyAgent: AsyncKeyAgent
) => {
if (Cardano.DRepID.isValid(signWith)) {
return DREP_KEY_DERIVATION_PATH;
}

const isRewardAccount = signWith.startsWith('stake');

const knownAddresses = await firstValueFrom(
Expand Down Expand Up @@ -114,6 +121,9 @@ export const cip30signData = async ({
signWith,
payload
}: Cip30SignDataRequest): Promise<Cip30DataSignature> => {
if (Cardano.DRepID.isValid(signWith) && !Cardano.DRepID.canSign(signWith)) {
throw new Cip30DataSignError(Cip30DataSignErrorCode.AddressNotPK, 'Invalid address');
}
const addressBytes = getAddressBytes(signWith);
const derivationPath = await getDerivationPath(signWith, keyAgent);

Expand Down
9 changes: 6 additions & 3 deletions packages/wallet/src/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export enum Cip30ConfirmationCallbackType {
export type SignDataCallbackParams = {
type: Cip30ConfirmationCallbackType.SignData;
data: {
addr: Cardano.PaymentAddress;
addr: Cardano.PaymentAddress | Cardano.DRepID;
payload: HexBlob;
};
};
Expand Down Expand Up @@ -397,10 +397,13 @@ const baseCip30WalletApi = (
scope.dispose();
}
},
signData: async (addr: Cardano.PaymentAddress | Bytes, payload: Bytes): Promise<Cip30DataSignature> => {
signData: async (
addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes,
payload: Bytes
): Promise<Cip30DataSignature> => {
logger.debug('signData');
const hexBlobPayload = HexBlob(payload);
const signWith = Cardano.PaymentAddress(addr);
const signWith = Cardano.DRepID.isValid(addr) ? Cardano.DRepID(addr) : Cardano.PaymentAddress(addr);

const shouldProceed = await confirmationCallback
.signData({
Expand Down
28 changes: 24 additions & 4 deletions packages/wallet/test/PersonalWallet/methods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import { CML, Cardano, CardanoNodeErrors, ProviderError, ProviderFailure, TxCBOR
import { HexBlob } from '@cardano-sdk/util';
import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction';
import { PersonalWallet, TxInFlight, setupWallet } from '../../src';
import { buildDRepIDFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util';
import { getPassphrase, stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks';
import { dummyLogger as logger } from 'ts-log';
import { toOutgoingTx, waitForWalletStateSettle } from '../util';
import delay from 'delay';

const { mockChainHistoryProvider, mockRewardsProvider, utxo } = mocks;
Expand Down Expand Up @@ -547,9 +547,29 @@ describe('PersonalWallet methods', () => {
});
});

it('signData calls cip30signData', async () => {
const response = await wallet.signData({ payload: HexBlob('abc123'), signWith: address });
expect(response).toHaveProperty('signature');
describe('signData', () => {
it('calls cip30signData', async () => {
const response = await wallet.signData({ payload: HexBlob('abc123'), signWith: address });
expect(response).toHaveProperty('signature');
});

it('signs with bech32 DRepID', async () => {
const response = await wallet.signData({
payload: HexBlob('abc123'),
signWith: Cardano.DRepID('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')
});
expect(response).toHaveProperty('signature');
});

test('rejects if bech32 DRepID is not a type 6 address', async () => {
const dRepKey = await wallet.getPubDRepKey();
for (const type in Cardano.AddressType) {
if (!Number.isNaN(Number(type)) && Number(type) !== Cardano.AddressType.EnterpriseKey) {
const drepid = buildDRepIDFromDRepKey(dRepKey, 0, type as unknown as Cardano.AddressType);
await expect(wallet.signData({ payload: HexBlob('abc123'), signWith: drepid })).rejects.toThrow();
}
}
});
});

it('getPubDRepKey', async () => {
Expand Down
Loading

0 comments on commit 1daae5f

Please sign in to comment.