diff --git a/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts b/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts index 71d5282e1ac..0f44f186148 100644 --- a/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts +++ b/packages/dapp-connector/src/WalletApi/Cip30Wallet.ts @@ -28,7 +28,7 @@ export const CipMethodsMapping: Record = { 'signData', 'submitTx' ], - 95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey'] + 95: ['getRegisteredPubStakeKeys', 'getUnregisteredPubStakeKeys', 'getPubDRepKey', 'signData'] }; export const WalletApiMethodNames: WalletMethod[] = Object.values(CipMethodsMapping).flat(); @@ -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) }; @@ -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) } }; diff --git a/packages/dapp-connector/src/WalletApi/types.ts b/packages/dapp-connector/src/WalletApi/types.ts index b75a246712e..c5144aed7c5 100644 --- a/packages/dapp-connector/src/WalletApi/types.ts +++ b/packages/dapp-connector/src/WalletApi/types.ts @@ -153,7 +153,7 @@ export type SignTx = (tx: Cbor, partialSign?: Boolean) => Promise; * @throws DataSignError */ export type SignData = ( - addr: Cardano.PaymentAddress | Cardano.DRepID | Bytes, + addr: Cardano.PaymentAddress | Cardano.RewardAccount | Bytes, payload: Bytes ) => Promise; @@ -203,6 +203,7 @@ export interface Cip95WalletApi { getRegisteredPubStakeKeys: () => Promise; getUnregisteredPubStakeKeys: () => Promise; getPubDRepKey: () => Promise; + signData: SignData; } export type WalletApi = Cip30WalletApi & Cip95WalletApi; diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index 1174e721d45..552ceebd26d 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -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 diff --git a/packages/key-management/src/cip8/cip30signData.ts b/packages/key-management/src/cip8/cip30signData.ts index 8bedda15571..359664eeb08 100644 --- a/packages/key-management/src/cip8/cip30signData.ts +++ b/packages/key-management/src/cip8/cip30signData.ts @@ -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; } @@ -39,7 +39,7 @@ export class Cip30DataSignError extends ComposableError { +export const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccount) => { const address = Cardano.Address.fromString(signWith); if (!address) { @@ -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) { diff --git a/packages/key-management/src/cip8/types.ts b/packages/key-management/src/cip8/types.ts index 5d4cae4e660..3cb5c2e1ecf 100644 --- a/packages/key-management/src/cip8/types.ts +++ b/packages/key-management/src/cip8/types.ts @@ -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; } diff --git a/packages/wallet/src/cip30.ts b/packages/wallet/src/cip30.ts index 016f63bd6e6..380f2407f1d 100644 --- a/packages/wallet/src/cip30.ts +++ b/packages/wallet/src/cip30.ts @@ -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'; @@ -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; }; }; @@ -258,6 +259,32 @@ const getSortedUtxos = async (observableUtxos: Observable): 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, confirmationCallback: CallbackConfirmation, @@ -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 => { 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({ @@ -579,7 +606,7 @@ const getPubStakeKeys = async ( const extendedCip95WalletApi = ( wallet$: Observable, { logger }: Cip30WalletDependencies -): Cip95WalletApi => ({ +): Omit => ({ getPubDRepKey: async () => { logger.debug('getting public DRep key'); try { diff --git a/packages/wallet/test/PersonalWallet/methods.test.ts b/packages/wallet/test/PersonalWallet/methods.test.ts index 11b902797fa..eb47ab19c46 100644 --- a/packages/wallet/test/PersonalWallet/methods.test.ts +++ b/packages/wallet/test/PersonalWallet/methods.test.ts @@ -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'; @@ -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'); }); @@ -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 () => { diff --git a/packages/wallet/test/integration/cip30mapping.test.ts b/packages/wallet/test/integration/cip30mapping.test.ts index a67c833788e..ae69aee3836 100644 --- a/packages/wallet/test/integration/cip30mapping.test.ts +++ b/packages/wallet/test/integration/cip30mapping.test.ts @@ -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'; @@ -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 () => { diff --git a/packages/wallet/test/util.ts b/packages/wallet/test/util.ts index 328bd08591a..e4b20c78fc8 100644 --- a/packages/wallet/test/util.ts +++ b/packages/wallet/test/util.ts @@ -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'; @@ -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('drep', HexBlob.fromBytes(paymentAddress)); + return Cardano.DRepID.toAddress(drepId); }; export const createAsyncKeyAgent = async () =>