Skip to content

Commit

Permalink
feat: implement sign with drep key
Browse files Browse the repository at this point in the history
signData now accepts drepId as key hash hex,
or type6 address.
Support for DRepID bech32 was removed to align with cip-95.

BREAKING CHANGE: SignData type no longer accepts bech32 DRepID
  • Loading branch information
mirceahasegan committed Dec 20, 2024
1 parent e39842b commit 44c3716
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 58 deletions.
9 changes: 6 additions & 3 deletions packages/dapp-connector/src/WalletApi/Cip30Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const CipMethodsMapping: Record<number, WalletMethod[]> = {
'signData',
'submitTx'
],
95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey']
95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey', 'signData']
};
export const WalletApiMethodNames: WalletMethod[] = Object.values(CipMethodsMapping).flat();

Expand Down Expand Up @@ -182,7 +182,8 @@ export class Cip30Wallet {
getUnusedAddresses: () => walletApi.getUnusedAddresses(),
getUsedAddresses: (paginate?: Paginate) => walletApi.getUsedAddresses(paginate),
getUtxos: (amount?: Cbor, paginate?: Paginate) => walletApi.getUtxos(amount, paginate),
signData: (addr: Cardano.PaymentAddress | Bytes, payload: Bytes) => walletApi.signData(addr, payload),
signData: (addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes) =>
walletApi.signData(addr, payload),
signTx: (tx: Cbor, partialSign?: Boolean) => walletApi.signTx(tx, partialSign),
submitTx: (tx: Cbor) => walletApi.submitTx(tx)
};
Expand All @@ -191,7 +192,9 @@ export class Cip30Wallet {
cip95: {
getPubDRepKey: () => walletApi.getPubDRepKey(),
getRegisteredPubStakeKeys: () => walletApi.getRegisteredPubStakeKeys(),
getUnregisteredPubStakeKeys: () => walletApi.getUnregisteredPubStakeKeys()
getUnregisteredPubStakeKeys: () => walletApi.getUnregisteredPubStakeKeys(),
signData: (addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes) =>
walletApi.signData(addr, payload)
}
};

Expand Down
3 changes: 2 additions & 1 deletion packages/dapp-connector/src/WalletApi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export type SignTx = (tx: Cbor, partialSign?: Boolean) => Promise<Cbor>;
* @throws DataSignError
*/
export type SignData = (
addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes,
addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes,
payload: Bytes
) => Promise<Cip30DataSignature>;

Expand Down Expand Up @@ -203,6 +203,7 @@ export interface Cip95WalletApi {
getRegisteredPubStakeKeys: () => Promise<Ed25519PublicKeyHex[]>;
getUnregisteredPubStakeKeys: () => Promise<Ed25519PublicKeyHex[]>;
getPubDRepKey: () => Promise<Ed25519PublicKeyHex>;
signData: SignData;
}

export type WalletApi = Cip30WalletApi & Cip95WalletApi;
Expand Down
2 changes: 1 addition & 1 deletion packages/hardware-ledger/src/LedgerKeyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ type OpenTransportForDeviceParams = {
};

const getDerivationPath = (
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID,
signWith: Cardano.PaymentAddress | Cardano.RewardAccount,
knownAddresses: GroupedAddress[],
accountIndex: number,
purpose: number
Expand Down
12 changes: 4 additions & 8 deletions packages/key-management/src/cip8/cip30signData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { DREP_KEY_DERIVATION_PATH, STAKE_KEY_DERIVATION_PATH } from '../util';

export interface Cip30SignDataRequest {
knownAddresses: GroupedAddress[];
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID;
signWith: Cardano.PaymentAddress | Cardano.RewardAccount;
payload: HexBlob;
sender?: MessageSender;
}
Expand All @@ -39,7 +39,7 @@ export class Cip30DataSignError<InnerError = unknown> extends ComposableError<In
}
}

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

if (!address) {
Expand All @@ -50,18 +50,14 @@ export const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.Rewar
};

const isPaymentAddress = (
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID
signWith: Cardano.PaymentAddress | Cardano.RewardAccount
): signWith is Cardano.PaymentAddress => signWith.startsWith('addr');

const getDerivationPath = async (
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID,
signWith: Cardano.PaymentAddress | Cardano.RewardAccount,
knownAddresses: GroupedAddress[],
dRepKeyHash: Crypto.Ed25519KeyHashHex
) => {
if (Cardano.DRepID.isValid(signWith)) {
return DREP_KEY_DERIVATION_PATH;
}

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

if (isRewardAccount) {
Expand Down
2 changes: 1 addition & 1 deletion packages/key-management/src/cip8/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export type CoseKeyCborHex = HexBlob;

export interface Cip8SignDataContext {
knownAddresses: GroupedAddress[];
signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID;
signWith: Cardano.PaymentAddress | Cardano.RewardAccount;
payload: HexBlob;
}

Expand Down
35 changes: 31 additions & 4 deletions packages/wallet/src/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
WithSenderContext
} from '@cardano-sdk/dapp-connector';
import { Cardano, Serialization, coalesceValueQuantities } from '@cardano-sdk/core';
import { Ed25519KeyHashHex, Hash28ByteBase16 } from '@cardano-sdk/crypto';
import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util';
import { InputSelectionError, InputSelectionFailure } from '@cardano-sdk/input-selection';
import { Logger } from 'ts-log';
Expand All @@ -42,7 +43,7 @@ export type SignDataCallbackParams = {
type: Cip30ConfirmationCallbackType.SignData;
sender: MessageSender;
data: {
addr: Cardano.PaymentAddress | Cardano.DRepID;
addr: Cardano.PaymentAddress | Cardano.RewardAccount;
payload: HexBlob;
};
};
Expand Down Expand Up @@ -258,6 +259,32 @@ const getSortedUtxos = async (observableUtxos: Observable<Cardano.Utxo[]>): Prom
return utxos.sort(compareUtxos);
};

/**
* Detect type of hex encoded addr and convert to PaymentAddress or RewardAddress.
*
* @param addr when hex encoded, it can be a PaymentAddress, RewardAddress or DRepKeyHash
* @returns PaymentAddress | RewardAddress DRepKeyHash is converted to a type 6 address
*/
const addrToSignWith = (
addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes
): Cardano.PaymentAddress | Cardano.RewardAccount => {
try {
return Cardano.isRewardAccount(addr) ? Cardano.RewardAccount(addr) : Cardano.PaymentAddress(addr);
} catch {
// Try to parse as drep key hash
const drepKeyHash = Ed25519KeyHashHex(addr);
const drepId = Cardano.DRepID.cip129FromCredential({
hash: Hash28ByteBase16.fromEd25519KeyHashHex(drepKeyHash),
type: Cardano.CredentialType.KeyHash
});
const drepAddr = Cardano.DRepID.toAddress(drepId)?.toAddress();
if (!drepAddr) {
throw new DataSignError(DataSignErrorCode.AddressNotPK, 'Invalid address');
}
return drepAddr.toBech32();
}
};

const baseCip30WalletApi = (
wallet$: Observable<ObservableWallet>,
confirmationCallback: CallbackConfirmation,
Expand Down Expand Up @@ -430,12 +457,12 @@ const baseCip30WalletApi = (
},
signData: async (
{ sender }: SenderContext,
addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes,
addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes,
payload: Bytes
): Promise<Cip30DataSignature> => {
logger.debug('signData');
const signWith = addrToSignWith(addr);
const hexBlobPayload = HexBlob(payload);
const signWith = Cardano.DRepID.isValid(addr) ? Cardano.DRepID(addr) : Cardano.PaymentAddress(addr);

const confirmationResult = await confirmationCallback
.signData({
Expand Down Expand Up @@ -579,7 +606,7 @@ const getPubStakeKeys = async (
const extendedCip95WalletApi = (
wallet$: Observable<ObservableWallet>,
{ logger }: Cip30WalletDependencies
): Cip95WalletApi => ({
): Omit<Cip95WalletApi, 'signData'> => ({
getPubDRepKey: async () => {
logger.debug('getting public DRep key');
try {
Expand Down
22 changes: 10 additions & 12 deletions packages/wallet/test/PersonalWallet/methods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { HexBlob } from '@cardano-sdk/util';
import { InitializeTxProps } from '@cardano-sdk/tx-construction';
import { babbageTx } from '../../../core/test/Serialization/testData';
import { buildDRepIDFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util';
import { buildDRepAddressFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util';
import { getPassphrase, stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks';
import { dummyLogger as logger } from 'ts-log';

Expand Down Expand Up @@ -494,9 +494,17 @@ describe('BaseWallet methods', () => {
});

it('signs with bech32 DRepID', async () => {
const drepPubKey = await wallet.governance.getPubDRepKey();
const drepAddr = (await buildDRepAddressFromDRepKey(drepPubKey!))?.toAddress()?.toBech32();

if (!drepAddr) {
expect(drepAddr).toBeDefined();
return;
}

const response = await wallet.signData({
payload: HexBlob('abc123'),
signWith: Cardano.DRepID('drep1vpzcgfrlgdh4fft0p0ju70czkxxkuknw0jjztl3x7aqgm9q3hqyaz')
signWith: drepAddr
});
expect(response).toHaveProperty('signature');
});
Expand All @@ -513,16 +521,6 @@ describe('BaseWallet methods', () => {
signWith: address
});
});

test('rejects if bech32 DRepID is not a type 6 address', async () => {
const dRepKey = await wallet.governance.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
101 changes: 88 additions & 13 deletions packages/wallet/test/integration/cip30mapping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construct
import { NEVER, firstValueFrom, of } from 'rxjs';
import { Providers, createWallet } from './util';
import { address_0_0, address_1_0, rewardAccount_0, rewardAccount_1 } from '../services/ChangeAddress/testData';
import { buildDRepIDFromDRepKey, signTx, waitForWalletStateSettle } from '../util';
import { buildDRepAddressFromDRepKey, signTx, waitForWalletStateSettle } from '../util';
import { dummyLogger as logger } from 'ts-log';
import { stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks';
import uniq from 'lodash/uniq.js';
Expand Down Expand Up @@ -686,30 +686,105 @@ describe('cip30', () => {
});

describe('api.signData', () => {
test('sign with address', async () => {
beforeEach(() => {
jest.clearAllMocks();
});

test('sign with bech32 address', async () => {
const [{ address }] = await firstValueFrom(wallet.addresses$);
const cip30dataSignature = await api.signData(context, address, HexBlob('abc123'));
expect(typeof cip30dataSignature.key).toBe('string');
expect(typeof cip30dataSignature.signature).toBe('string');
});

test('sign with bech32 DRepID', async () => {
test('sign with hex-encoded address', async () => {
const signDataSpy = jest.spyOn(wallet, 'signData');
const [{ address }] = await firstValueFrom(wallet.addresses$);
const addressHex = Cardano.Address.fromString(address)?.toBytes();
if (!addressHex) {
expect(addressHex).toBeDefined();
return;
}
const cip30dataSignature = await api.signData(context, addressHex, HexBlob('abc123'));
expect(typeof cip30dataSignature.key).toBe('string');
expect(typeof cip30dataSignature.signature).toBe('string');
expect(signDataSpy.mock.calls[0][0].signWith).toEqual(address);
});

test('sign with bech32 reward account', async () => {
const signDataSpy = jest.spyOn(wallet, 'signData');
const [{ rewardAccount }] = await firstValueFrom(wallet.addresses$);

const cip30dataSignature = await api.signData(context, rewardAccount, HexBlob('abc123'));
expect(typeof cip30dataSignature.key).toBe('string');
expect(typeof cip30dataSignature.signature).toBe('string');
expect(signDataSpy.mock.calls[0][0].signWith).toEqual(rewardAccount);
});

test('sign with hex-encoded reward account', async () => {
const signDataSpy = jest.spyOn(wallet, 'signData');
const [{ rewardAccount }] = await firstValueFrom(wallet.addresses$);
const rewardAccountHex = Cardano.Address.fromString(rewardAccount)?.toBytes();

const cip30dataSignature = await api.signData(context, rewardAccountHex!, HexBlob('abc123'));
expect(typeof cip30dataSignature.key).toBe('string');
expect(typeof cip30dataSignature.signature).toBe('string');
expect(signDataSpy.mock.calls[0][0].signWith).toEqual(rewardAccount);
});

test('sign with hex-encoded DRepID key hash hex', async () => {
const signDataSpy = jest.spyOn(wallet, 'signData');
const dRepKey = await api.getPubDRepKey(context);
const drepid = buildDRepIDFromDRepKey(dRepKey);
const drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex();

await api.signData(context, drepKeyHashHex, HexBlob('abc123'));
expect(signDataSpy).toHaveBeenCalledTimes(1);
// Wallet signData is called with the DRepID as bech32 address because it was transformed by the cip30Api.
// The address credential should be the drepKeyHash
const signAddr = Cardano.Address.fromString(signDataSpy.mock.calls[0][0].signWith);
expect(signAddr?.getProps().paymentPart?.hash).toEqual(drepKeyHashHex);
});

const cip95dataSignature = await api.signData(context, drepid, HexBlob('abc123'));
expect(typeof cip95dataSignature.key).toBe('string');
expect(typeof cip95dataSignature.signature).toBe('string');
test('sign with hex-encoded type 6 DRepID address', async () => {
const signDataSpy = jest.spyOn(wallet, 'signData');
const dRepKey = await api.getPubDRepKey(context);
const drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex();
const drepAddress = await buildDRepAddressFromDRepKey(dRepKey);
// CIP95 DRepID as type 6 hex-encoded Address
const drepAddressBytes = drepAddress?.toAddress()?.toBytes();

if (!drepAddressBytes) {
expect(drepAddressBytes).toBeDefined();
return;
}

await api.signData(context, drepAddressBytes, HexBlob('abc123'));
expect(signDataSpy).toHaveBeenCalledTimes(1);
// Wallet signData is called with the DRepID as bech32 address because it was transformed by the cip30Api.
// The address credential should be the drepKeyHash
const signAddr = Cardano.Address.fromString(signDataSpy.mock.calls[0][0].signWith);
expect(signAddr?.getProps().paymentPart?.hash).toEqual(drepKeyHashHex);
});

test('rejects if bech32 DRepID is not a type 6 address', async () => {
test('sign with bech32 type 6 DRepID address', async () => {
const signDataSpy = jest.spyOn(wallet, 'signData');
const dRepKey = await api.getPubDRepKey(context);
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(api.signData(context, drepid, HexBlob('abc123'))).rejects.toThrow();
}
const drepKeyHashHex = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex();
const drepAddress = await buildDRepAddressFromDRepKey(dRepKey);
// CIP95 DRepID as type 6 hex-encoded Address
const drepAddressBech32 = drepAddress?.toAddress()?.toBech32();

if (!drepAddressBech32) {
expect(drepAddressBech32).toBeDefined();
return;
}

await api.signData(context, drepAddressBech32, HexBlob('abc123'));
expect(signDataSpy).toHaveBeenCalledTimes(1);
// Wallet signData is called with the DRepID as bech32 address because it was transformed by the cip30Api.
// The address credential should be the drepKeyHash
const signAddr = Cardano.Address.fromString(signDataSpy.mock.calls[0][0].signWith);
expect(signAddr?.getProps().paymentPart?.hash).toEqual(drepKeyHashHex);
});

it('passes through sender from dapp connector context', async () => {
Expand Down
29 changes: 14 additions & 15 deletions packages/wallet/test/util.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as Crypto from '@cardano-sdk/crypto';
import { Cardano, Serialization } from '@cardano-sdk/core';
import { GroupedAddress, InMemoryKeyAgent, WitnessedTx, util } from '@cardano-sdk/key-management';
import { HexBlob } from '@cardano-sdk/util';
import { Observable, catchError, filter, firstValueFrom, throwError, timeout } from 'rxjs';
import { ObservableWallet, OutgoingTx, WalletUtil } from '../src';
import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto';
Expand Down Expand Up @@ -49,23 +48,23 @@ export const toSignedTx = (tx: Cardano.Tx): WitnessedTx => ({

export const dummyCbor = Serialization.TxCBOR('123');

/** Construct a type 6 address for a DRepKey using an appropriate Network Tag and a hash of a public DRep Key. */
export const buildDRepIDFromDRepKey = (
/**
* Construct a type 6 or 7 address for a DRepKey using a hash of a public DRep Key.
*
* @param dRepKey The public DRep key to hash and use in the address
* @param type The type of credential to use in the address.
* @returns A a type 6 address for keyHash credential type, or a type 7 address for script credential type.
*/
export const buildDRepAddressFromDRepKey = async (
dRepKey: Crypto.Ed25519PublicKeyHex,
networkId: Cardano.NetworkId = Cardano.NetworkId.Testnet,
addressType: Cardano.AddressType = Cardano.AddressType.EnterpriseKey
type: Cardano.CredentialType = Cardano.CredentialType.KeyHash
) => {
const dRepKeyBytes = Buffer.from(dRepKey, 'hex');
const dRepIdHex = Crypto.blake2b(28).update(dRepKeyBytes).digest('hex');
const paymentAddress = Cardano.EnterpriseAddress.packParts({
networkId,
paymentPart: {
hash: Crypto.Hash28ByteBase16(dRepIdHex),
type: Cardano.CredentialType.KeyHash
},
type: addressType
const drepKeyHash = (await Crypto.Ed25519PublicKey.fromHex(dRepKey).hash()).hex();
const drepId = Cardano.DRepID.cip129FromCredential({
hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(drepKeyHash),
type
});
return HexBlob.toTypedBech32<Cardano.DRepID>('drep', HexBlob.fromBytes(paymentAddress));
return Cardano.DRepID.toAddress(drepId);
};

export const createAsyncKeyAgent = async () =>
Expand Down

0 comments on commit 44c3716

Please sign in to comment.