diff --git a/packages/e2e/src/factories.ts b/packages/e2e/src/factories.ts index b0fd426077f..99927dec185 100644 --- a/packages/e2e/src/factories.ts +++ b/packages/e2e/src/factories.ts @@ -10,7 +10,6 @@ import { PersonalWallet, PollingConfig, SingleAddressDiscovery, - setupWallet, storage } from '@cardano-sdk/wallet'; import { @@ -27,6 +26,7 @@ import { } from '@cardano-sdk/core'; import { AsyncKeyAgent, + Bip32Account, CommunicationType, InMemoryKeyAgent, KeyAgentDependencies, @@ -349,24 +349,25 @@ export const getWallet = async (props: GetWalletProps) => { const keyManagementParams = { ...envKeyParams, ...(idx === undefined ? {} : { accountIndex: idx }) }; const bip32Ed25519 = await bip32Ed25519Factory.create(env.KEY_MANAGEMENT_PARAMS.bip32Ed25519, null, logger); - const { wallet } = await setupWallet({ - bip32Ed25519, - createKeyAgent: keyAgent - ? () => Promise.resolve(keyAgent) - : await keyManagementFactory.create(env.KEY_MANAGEMENT_PROVIDER, keyManagementParams, logger), - createWallet: async (asyncKeyAgent: AsyncKeyAgent) => - new PersonalWallet( - { name, polling }, - { - ...providers, - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), - logger, - stores, - witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) - } - ), - logger - }); + const asyncKeyAgent = + keyAgent || + (await ( + await keyManagementFactory.create(env.KEY_MANAGEMENT_PROVIDER, keyManagementParams, logger) + )({ + bip32Ed25519, + logger + })); + const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + const wallet = new PersonalWallet( + { name, polling }, + { + ...providers, + bip32Account, + logger, + stores, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); const [{ address, rewardAccount }] = await firstValueFrom(wallet.addresses$); logger.info(`Created wallet "${wallet.name}": ${address}/${rewardAccount}`); @@ -375,7 +376,7 @@ export const getWallet = async (props: GetWalletProps) => { polling?.maxInterval || (polling?.interval && polling.interval * DEFAULT_POLLING_CONFIG.maxIntervalMultiplier) || DEFAULT_POLLING_CONFIG.maxInterval; - return { providers, wallet: patchInitializeTxToRespectEpochBoundary(wallet, maxInterval) }; + return { bip32Account, providers, wallet: patchInitializeTxToRespectEpochBoundary(wallet, maxInterval) }; }; export type TestWallet = Awaited>; diff --git a/packages/e2e/src/scripts/mnemonic.ts b/packages/e2e/src/scripts/mnemonic.ts index 20c29eb0270..1b9ce68d99c 100644 --- a/packages/e2e/src/scripts/mnemonic.ts +++ b/packages/e2e/src/scripts/mnemonic.ts @@ -18,7 +18,6 @@ import { localNetworkChainId } from '../util'; }, { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - inputResolver: { resolveInput: async () => null }, logger: console } ); diff --git a/packages/e2e/src/util/StubKeyAgent.ts b/packages/e2e/src/util/StubKeyAgent.ts deleted file mode 100644 index 59638da3383..00000000000 --- a/packages/e2e/src/util/StubKeyAgent.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { - AccountAddressDerivationPath, - AccountKeyDerivationPath, - GroupedAddress, - KeyAgent, - SerializableKeyAgentData, - SignBlobResult, - SignTransactionOptions -} from '@cardano-sdk/key-management'; -import { Cardano, NotImplementedError } from '@cardano-sdk/core'; -import { HexBlob } from '@cardano-sdk/util'; - -export class StubKeyAgent implements KeyAgent { - readonly #knownAddresses: GroupedAddress[]; - - constructor(groupedAddress: GroupedAddress) { - this.#knownAddresses = [groupedAddress]; - } - - get knownAddresses(): GroupedAddress[] { - return this.#knownAddresses; - } - - get bip32Ed25519(): Crypto.Bip32Ed25519 { - throw new NotImplementedError('bip32Ed25519'); - } - - get chainId(): Cardano.ChainId { - throw new NotImplementedError('chainId'); - } - - get accountIndex(): number { - throw new NotImplementedError('accountIndex'); - } - - get serializableData(): SerializableKeyAgentData { - throw new NotImplementedError('serializableData'); - } - - get extendedAccountPublicKey(): Crypto.Bip32PublicKeyHex { - throw new NotImplementedError('extendedAccountPublicKey'); - } - - deriveAddress(_derivationPath: AccountAddressDerivationPath): Promise { - throw new NotImplementedError('deriveAddress'); - } - - derivePublicKey(_derivationPath: AccountKeyDerivationPath): Promise { - throw new NotImplementedError('derivePublicKey'); - } - - signBlob(_derivationPath: AccountKeyDerivationPath, _blob: HexBlob): Promise { - throw new NotImplementedError('signBlob'); - } - - signTransaction( - _txInternals: Cardano.TxBodyWithHash, - _options?: SignTransactionOptions | undefined - ): Promise { - throw new NotImplementedError('signTransaction'); - } - - exportRootPrivateKey(): Promise { - throw new NotImplementedError('exportRootPrivateKey'); - } -} diff --git a/packages/e2e/src/util/createMockKeyAgent.ts b/packages/e2e/src/util/createMockKeyAgent.ts new file mode 100644 index 00000000000..3df1ed1595e --- /dev/null +++ b/packages/e2e/src/util/createMockKeyAgent.ts @@ -0,0 +1,31 @@ +import { Bip32PublicKeyHex, SodiumBip32Ed25519 } from '@cardano-sdk/crypto'; +import { Cardano } from '@cardano-sdk/core'; +import { GroupedAddress, KeyAgent, KeyAgentType } from '@cardano-sdk/key-management'; + +const accountIndex = 0; +const chainId = Cardano.ChainIds.Preview; +const extendedAccountPublicKey = Bip32PublicKeyHex( + '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' +); + +export const createMockKeyAgent = (deriveAddressesReturn: GroupedAddress[] = []): jest.Mocked => { + const remainingDeriveAddressesReturn = [...deriveAddressesReturn]; + return { + accountIndex, + bip32Ed25519: new SodiumBip32Ed25519(), + chainId, + deriveAddress: jest.fn().mockImplementation(async () => remainingDeriveAddressesReturn.shift()), + derivePublicKey: jest.fn(), + exportRootPrivateKey: jest.fn(), + extendedAccountPublicKey, + serializableData: { + __typename: KeyAgentType.InMemory, + accountIndex, + chainId, + encryptedRootPrivateKeyBytes: [], + extendedAccountPublicKey + }, + signBlob: jest.fn(), + signTransaction: jest.fn() + }; +}; diff --git a/packages/e2e/src/util/handle-util.ts b/packages/e2e/src/util/handle-util.ts index 8874a038fad..5c535b8ac7b 100644 --- a/packages/e2e/src/util/handle-util.ts +++ b/packages/e2e/src/util/handle-util.ts @@ -90,7 +90,8 @@ export const mint = async ( txMetadatum: Cardano.Metadatum, datum?: Cardano.PlutusData ) => { - const [{ address }] = await firstValueFrom(wallet.addresses$); + const knownAddresses = await firstValueFrom(wallet.addresses$); + const [{ address }] = knownAddresses; const { policyScript, policySigner } = await createHandlePolicy(keyAgent); const auxiliaryData = { diff --git a/packages/e2e/src/util/index.ts b/packages/e2e/src/util/index.ts index 38b56988819..e5d7fbeaa59 100644 --- a/packages/e2e/src/util/index.ts +++ b/packages/e2e/src/util/index.ts @@ -1,4 +1,4 @@ -export * from './StubKeyAgent'; +export * from './createMockKeyAgent'; export * from './localNetworkChainId'; export * from './util'; export * from './handle-util'; diff --git a/packages/e2e/src/util/util.ts b/packages/e2e/src/util/util.ts index a56cb693d67..0d5d50eeacc 100644 --- a/packages/e2e/src/util/util.ts +++ b/packages/e2e/src/util/util.ts @@ -204,7 +204,8 @@ export const getTxConfirmationEpoch = async (wallet: PersonalWallet, tx: Cardano * @param wallet The wallet */ export const submitCertificate = async (certificate: Cardano.Certificate, wallet: TestWallet) => { - const walletAddress = (await firstValueFrom(wallet.wallet.addresses$))[0].address; + const knownAddresses = await firstValueFrom(wallet.wallet.addresses$); + const walletAddress = knownAddresses[0].address; const txProps: InitializeTxProps = { certificates: [certificate], outputs: new Set([{ address: walletAddress, value: { coins: 3_000_000n } }]) @@ -238,7 +239,7 @@ export const createStandaloneKeyAgent = async ( getPassphrase: async () => Buffer.from(''), mnemonicWords: mnemonics }, - { bip32Ed25519, inputResolver: { resolveInput: async () => null }, logger } + { bip32Ed25519, logger } ); /** diff --git a/packages/e2e/test/artillery/wallet-restoration/WalletRestoration.ts b/packages/e2e/test/artillery/wallet-restoration/WalletRestoration.ts index 0ff13771794..1e5aa4802a8 100644 --- a/packages/e2e/test/artillery/wallet-restoration/WalletRestoration.ts +++ b/packages/e2e/test/artillery/wallet-restoration/WalletRestoration.ts @@ -3,7 +3,7 @@ import { AddressesModel, WalletVars } from './types'; import { Cardano } from '@cardano-sdk/core'; import { FunctionHook } from '../artillery'; import { Pool } from 'pg'; -import { StubKeyAgent, getEnv, getWallet, waitForWalletStateSettle, walletVariables } from '../../../src'; +import { createMockKeyAgent, getEnv, getWallet, waitForWalletStateSettle, walletVariables } from '../../../src'; import { findAddressesWithRegisteredStakeKey } from './queries'; import { logger } from '@cardano-sdk/util-dev'; @@ -89,7 +89,7 @@ export const walletRestoration: FunctionHook = async ({ vars, _uid } try { // Creates Stub KeyAgent - const keyAgent = util.createAsyncKeyAgent(new StubKeyAgent(currentAddress)); + const keyAgent = util.createAsyncKeyAgent(createMockKeyAgent([currentAddress])); // Start to measure wallet restoration time const startedAt = Date.now(); diff --git a/packages/e2e/test/load-test-custom/wallet-init/wallet-init.test.ts b/packages/e2e/test/load-test-custom/wallet-init/wallet-init.test.ts index 8b48b8d4a9f..c81fd56104f 100644 --- a/packages/e2e/test/load-test-custom/wallet-init/wallet-init.test.ts +++ b/packages/e2e/test/load-test-custom/wallet-init/wallet-init.test.ts @@ -6,10 +6,11 @@ import path from 'path'; dotenv.config({ path: path.join(__dirname, '../../../.env') }); import { Logger } from 'ts-log'; -import { PersonalWallet, createLazyWalletUtil } from '@cardano-sdk/wallet'; +import { PersonalWallet } from '@cardano-sdk/wallet'; import { bufferCount, bufferTime, from, mergeAll, tap } from 'rxjs'; import { logger } from '@cardano-sdk/util-dev'; +import { Bip32Account, util } from '@cardano-sdk/key-management'; import { MeasurementUtil, assetProviderFactory, @@ -26,7 +27,6 @@ import { waitForWalletStateSettle, walletVariables } from '../../../src'; -import { util } from '@cardano-sdk/key-management'; // Example call that creates 5000 wallets in 10 minutes: // VIRTUAL_USERS_GENERATE_DURATION=600 VIRTUAL_USERS_COUNT=5000 yarn load-test-custom:wallet-init @@ -73,29 +73,26 @@ const getKeyAgent = async (accountIndex: number) => { logger ); const bip32Ed25519 = await bip32Ed25519Factory.create(env.KEY_MANAGEMENT_PARAMS.bip32Ed25519, null, logger); - const walletUtil = createLazyWalletUtil(); - const keyAgent = await createKeyAgent({ bip32Ed25519, inputResolver: walletUtil, logger }); - return { keyAgent, walletUtil }; + const keyAgent = await createKeyAgent({ bip32Ed25519, logger }); + return { keyAgent }; }; const createWallet = async (accountIndex: number): Promise => { measurementUtil.addStartMarker(MeasureTarget.keyAgent, accountIndex); const providers = await getProviders(); - const { keyAgent, walletUtil } = await getKeyAgent(accountIndex); + const { keyAgent } = await getKeyAgent(accountIndex); measurementUtil.addMeasureMarker(MeasureTarget.keyAgent, accountIndex); measurementUtil.addStartMarker(MeasureTarget.wallet, accountIndex); - const wallet = new PersonalWallet( + return new PersonalWallet( { name: `Wallet ${accountIndex}` }, { ...providers, - addressManager: util.createBip32Ed25519AddressManager(keyAgent), + bip32Account: await Bip32Account.fromAsyncKeyAgent(keyAgent), logger, witnesser: util.createBip32Ed25519Witnesser(keyAgent) } ); - walletUtil.initialize(wallet); - return wallet; }; const initWallet = async (idx: number) => { diff --git a/packages/e2e/test/load-test-custom/wallet-restoration/wallet-restoration.test.ts b/packages/e2e/test/load-test-custom/wallet-restoration/wallet-restoration.test.ts index ccad9050718..b804b56722d 100644 --- a/packages/e2e/test/load-test-custom/wallet-restoration/wallet-restoration.test.ts +++ b/packages/e2e/test/load-test-custom/wallet-restoration/wallet-restoration.test.ts @@ -6,7 +6,7 @@ dotenv.config({ path: path.join(__dirname, '../../../.env') }); import { Cardano } from '@cardano-sdk/core'; import { GroupedAddress, util } from '@cardano-sdk/key-management'; import { Logger } from 'ts-log'; -import { MINUTE, StubKeyAgent, getEnv, getWallet, waitForWalletStateSettle, walletVariables } from '../../../src'; +import { MINUTE, createMockKeyAgent, getEnv, getWallet, waitForWalletStateSettle, walletVariables } from '../../../src'; import { PersonalWallet } from '@cardano-sdk/wallet'; import { logger } from '@cardano-sdk/util-dev'; import { mapToGroupedAddress } from '../../artillery/wallet-restoration/WalletRestoration'; @@ -41,7 +41,7 @@ const initWallets = async (walletsNum: number, addresses: GroupedAddress[]): Pro for (let i = 0; i < walletsNum; i++) { currentAddress = addresses[i]; testLogger.info(' address:', currentAddress.address); - const keyAgent = util.createAsyncKeyAgent(new StubKeyAgent(currentAddress)); + const keyAgent = util.createAsyncKeyAgent(createMockKeyAgent([currentAddress])); const { wallet } = await getWallet({ env, idx: 0, diff --git a/packages/e2e/test/local-network/register-pool.test.ts b/packages/e2e/test/local-network/register-pool.test.ts index 7ed9befa675..0b0d3591702 100644 --- a/packages/e2e/test/local-network/register-pool.test.ts +++ b/packages/e2e/test/local-network/register-pool.test.ts @@ -79,17 +79,15 @@ describe('local-network/register-pool', () => { await walletReady(wallet); - const poolAddressManager = wallet.addressManager; - - const poolPubKey = await poolAddressManager.derivePublicKey({ + const poolPubKey = await wallet1.bip32Account.derivePublicKey({ index: 0, role: KeyRole.External }); - const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey); + const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey.hex()); const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash); const poolRewardAccount = ( - await poolAddressManager.deriveAddress( + await wallet1.bip32Account.deriveAddress( { index: 0, type: AddressType.External @@ -167,17 +165,15 @@ describe('local-network/register-pool', () => { await walletReady(wallet); - const poolAddressManager = wallet.addressManager; - - const poolPubKey = await poolAddressManager.derivePublicKey({ + const poolPubKey = await wallet2.bip32Account.derivePublicKey({ index: 0, role: KeyRole.External }); - const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey); + const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey.hex()); const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash); const poolRewardAccount = ( - await poolAddressManager.deriveAddress( + await wallet2.bip32Account.deriveAddress( { index: 0, type: AddressType.External diff --git a/packages/e2e/test/long-running/cache-invalidation.test.ts b/packages/e2e/test/long-running/cache-invalidation.test.ts index 5ba31439c97..26d7d0fd6fa 100644 --- a/packages/e2e/test/long-running/cache-invalidation.test.ts +++ b/packages/e2e/test/long-running/cache-invalidation.test.ts @@ -107,17 +107,15 @@ describe('cache invalidation', () => { await walletReady(wallet); - const poolAddressManager = wallet.addressManager; - - const poolPubKey = await poolAddressManager.derivePublicKey({ + const poolPubKey = await wallet1.bip32Account.derivePublicKey({ index: 0, role: KeyRole.External }); - const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey); + const poolKeyHash = await bip32Ed25519.getPubKeyHash(poolPubKey.hex()); const poolId = Cardano.PoolId.fromKeyHash(poolKeyHash); const poolRewardAccount = ( - await poolAddressManager.deriveAddress( + await wallet1.bip32Account.deriveAddress( { index: 0, type: AddressType.External diff --git a/packages/e2e/test/long-running/multisig-wallet/MultiSigWallet.ts b/packages/e2e/test/long-running/multisig-wallet/MultiSigWallet.ts index c9f60bf1d6d..5f1e1c56de2 100644 --- a/packages/e2e/test/long-running/multisig-wallet/MultiSigWallet.ts +++ b/packages/e2e/test/long-running/multisig-wallet/MultiSigWallet.ts @@ -1,5 +1,11 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, GroupedAddress, InMemoryKeyAgent, KeyRole } from '@cardano-sdk/key-management'; +import { + AccountKeyDerivationPath, + AddressType, + GroupedAddress, + InMemoryKeyAgent, + KeyRole +} from '@cardano-sdk/key-management'; import { Cardano, ChainHistoryProvider, @@ -25,7 +31,7 @@ const randomPublicKey = () => Crypto.Ed25519PublicKeyHex(Array.from({ length: 64 const DUMMY_HEX_BYTES = 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'; -const DERIVATION_PATH = { +const DERIVATION_PATH: AccountKeyDerivationPath = { index: 0, role: KeyRole.External }; @@ -209,6 +215,7 @@ export class MultiSigWallet { body: multiSigTx.getTransaction().body, hash: multiSigTx.getTransaction().id }, + { knownAddresses: [this.#address], txInKeyPathMap: {} }, { additionalKeyPaths: [DERIVATION_PATH] } ); diff --git a/packages/e2e/test/web-extension/extension/ui.ts b/packages/e2e/test/web-extension/extension/ui.ts index 182dbe6b893..38027a513cc 100644 --- a/packages/e2e/test/web-extension/extension/ui.ts +++ b/packages/e2e/test/web-extension/extension/ui.ts @@ -21,13 +21,13 @@ import { userPromptServiceChannel, walletName } from './const'; -import { bip32Ed25519Factory, keyManagementFactory } from '../../../src'; +import { keyManagementFactory } from '../../../src'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; +import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto'; import { combineLatest, firstValueFrom, of } from 'rxjs'; import { runtime } from 'webextension-polyfill'; -import { setupWallet } from '@cardano-sdk/wallet'; const delegationConfig = { count: 3, @@ -223,23 +223,16 @@ wallet.delegation.distribution$.subscribe((delegationDistrib) => { const createWallet = async (accountIndex: number) => { clearWalletValues(); - // setupWallet call is required to provide context (InputResolver) to the key agent - const { keyAgent } = await setupWallet({ - bip32Ed25519: await bip32Ed25519Factory.create(env.KEY_MANAGEMENT_PARAMS.bip32Ed25519, null, logger), - createKeyAgent: async (dependencies) => - ( - await keyManagementFactory.create( - env.KEY_MANAGEMENT_PROVIDER, - { - ...env.KEY_MANAGEMENT_PARAMS, - accountIndex - }, - logger - ) - )(dependencies), - createWallet: async () => wallet, - logger - }); + const keyAgent = await ( + await keyManagementFactory.create( + env.KEY_MANAGEMENT_PROVIDER, + { + ...env.KEY_MANAGEMENT_PARAMS, + accountIndex + }, + logger + ) + )({ bip32Ed25519: new SodiumBip32Ed25519(), logger }); await walletManager.destroy(); await walletManager.activate({ keyAgent, observableWalletName: getObservableWalletName(accountIndex) }); diff --git a/packages/governance/test/integration/cip36KeyAgents.test.ts b/packages/governance/test/integration/cip36KeyAgents.test.ts index 2a7db718fbe..b3a4c320d75 100644 --- a/packages/governance/test/integration/cip36KeyAgents.test.ts +++ b/packages/governance/test/integration/cip36KeyAgents.test.ts @@ -37,8 +37,8 @@ describe('cip36', () => { it('can create cip36 voting registration metadata', async () => { // Just ensuring we have some address. PersonalWallet already does this internally. - await walletKeyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - const paymentAddress = walletKeyAgent.knownAddresses[0].address; + const groupedAddress = await walletKeyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); + const paymentAddress = groupedAddress.address; // InMemoryKeyAgent uses this derivation path for stake key. const stakeKey = await walletKeyAgent.derivePublicKey(util.STAKE_KEY_DERIVATION_PATH); // "Delegating" voting power to your own vote key diff --git a/packages/hardware-ledger/src/LedgerKeyAgent.ts b/packages/hardware-ledger/src/LedgerKeyAgent.ts index 689ff63ada8..50293639429 100644 --- a/packages/hardware-ledger/src/LedgerKeyAgent.ts +++ b/packages/hardware-ledger/src/LedgerKeyAgent.ts @@ -10,10 +10,10 @@ import { KeyAgentType, SerializableLedgerKeyAgentData, SignBlobResult, + SignTransactionContext, errors } from '@cardano-sdk/key-management'; import { LedgerTransportType } from './types'; -import { ManagedFreeableScope } from '@cardano-sdk/util'; import { str_to_path } from '@cardano-foundation/ledgerjs-hw-app-cardano/dist/utils/address'; import { toLedgerTx } from './transformers'; import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-noevents'; @@ -316,8 +316,7 @@ export class LedgerKeyAgent extends KeyAgentBase { chainId, communicationType, deviceConnection: activeDeviceConnection, - extendedAccountPublicKey, - knownAddresses: [] + extendedAccountPublicKey }, dependencies ); @@ -365,13 +364,16 @@ export class LedgerKeyAgent extends KeyAgentBase { } // TODO: Allow additional key paths - async signTransaction({ body, hash }: Cardano.TxBodyWithHash): Promise { - const scope = new ManagedFreeableScope(); + async signTransaction( + { body, hash }: Cardano.TxBodyWithHash, + { knownAddresses, txInKeyPathMap }: SignTransactionContext + ): Promise { try { const ledgerTxData = await toLedgerTx(body, { + accountIndex: this.accountIndex, chainId: this.chainId, - inputResolver: this.inputResolver, - knownAddresses: this.knownAddresses + knownAddresses, + txInKeyPathMap }); const deviceConnection = await LedgerKeyAgent.checkDeviceConnection( @@ -407,8 +409,6 @@ export class LedgerKeyAgent extends KeyAgentBase { throw new errors.AuthenticationError('Transaction signing aborted', error); } throw transportTypedError(error); - } finally { - scope.dispose(); } } diff --git a/packages/hardware-ledger/src/transformers/collateralInputs.ts b/packages/hardware-ledger/src/transformers/collateralInputs.ts index 2cfa23ab3a2..0e710576fe2 100644 --- a/packages/hardware-ledger/src/transformers/collateralInputs.ts +++ b/packages/hardware-ledger/src/transformers/collateralInputs.ts @@ -3,7 +3,7 @@ import { Cardano } from '@cardano-sdk/core'; import { LedgerTxTransformerContext } from '../types'; import { mapTxIns } from './txIn'; -export const mapCollateralTxIns = async ( +export const mapCollateralTxIns = ( collateralTxIns: Cardano.TxIn[] | undefined, context: LedgerTxTransformerContext -): Promise => (collateralTxIns ? await mapTxIns(collateralTxIns, context) : null); +): Ledger.TxInput[] | null => (collateralTxIns ? mapTxIns(collateralTxIns, context) : null); diff --git a/packages/hardware-ledger/src/transformers/tx.ts b/packages/hardware-ledger/src/transformers/tx.ts index 8be523bc099..ea93117e976 100644 --- a/packages/hardware-ledger/src/transformers/tx.ts +++ b/packages/hardware-ledger/src/transformers/tx.ts @@ -16,18 +16,18 @@ import { mapWithdrawals } from './withdrawals'; export const LedgerTxTransformer: Transformer = { auxiliaryData: ({ auxiliaryDataHash }) => mapAuxiliaryData(auxiliaryDataHash), certificates: ({ certificates }, context) => mapCerts(certificates, context!), - collateralInputs: async ({ collaterals }, context) => mapCollateralTxIns(collaterals, context!), + collateralInputs: ({ collaterals }, context) => mapCollateralTxIns(collaterals, context!), collateralOutput: ({ collateralReturn }, context) => mapCollateralTxOut(collateralReturn, context!), fee: ({ fee }) => fee, includeNetworkId: ({ networkId }) => !!networkId, - inputs: async ({ inputs }, context) => await mapTxIns(inputs, context!), + inputs: ({ inputs }, context) => mapTxIns(inputs, context!), mint: ({ mint }) => mapTokenMap(mint), network: (_, context) => ({ networkId: context!.chainId.networkId, protocolMagic: context!.chainId.networkMagic }), - outputs: async ({ outputs }, context) => mapTxOuts(outputs, context!), - referenceInputs: async ({ referenceInputs }) => mapReferenceInputs(referenceInputs), + outputs: ({ outputs }, context) => mapTxOuts(outputs, context!), + referenceInputs: ({ referenceInputs }) => mapReferenceInputs(referenceInputs), requiredSigners: ({ requiredExtraSignatures }, context) => mapRequiredSigners(requiredExtraSignatures, context!), scriptDataHashHex: ({ scriptIntegrityHash }) => scriptIntegrityHash?.toString(), totalCollateral: ({ totalCollateral }) => totalCollateral, diff --git a/packages/hardware-ledger/src/transformers/txIn.ts b/packages/hardware-ledger/src/transformers/txIn.ts index d536febcc00..e64cab74d2e 100644 --- a/packages/hardware-ledger/src/transformers/txIn.ts +++ b/packages/hardware-ledger/src/transformers/txIn.ts @@ -2,40 +2,25 @@ import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { Cardano } from '@cardano-sdk/core'; import { LedgerTxTransformerContext } from '../types'; import { Transform } from '@cardano-sdk/util'; -import { util } from '@cardano-sdk/key-management'; +import { TxInId, util } from '@cardano-sdk/key-management'; -const resolveKeyPath = async ( +const resolveKeyPath = ( txIn: Cardano.TxIn, - context: LedgerTxTransformerContext -): Promise => { - const txOut = await context.inputResolver.resolveInput(txIn); - - let paymentKeyPath = null; - - if (txOut) { - const knownAddress = context.knownAddresses.find(({ address }) => address === txOut.address); - - if (knownAddress) { - paymentKeyPath = util.paymentKeyPathFromGroupedAddress(knownAddress); - } + { accountIndex, txInKeyPathMap }: LedgerTxTransformerContext +): Ledger.BIP32Path | null => { + const utxoKeyPath = txInKeyPathMap[TxInId(txIn)]; + if (utxoKeyPath) { + return util.accountKeyDerivationPathToBip32Path(accountIndex, utxoKeyPath); } - return paymentKeyPath; + return null; }; -export const toTxIn: Transform, LedgerTxTransformerContext> = async ( - txIn, - context -) => ({ +export const toTxIn: Transform = (txIn, context) => ({ outputIndex: txIn.index, - path: await resolveKeyPath(txIn, context!), + path: resolveKeyPath(txIn, context!), txHashHex: txIn.txId }); -export const mapTxIns = async ( - txIns: Cardano.TxIn[], - context: LedgerTxTransformerContext -): Promise => { - const result = txIns.map((txIn) => toTxIn(txIn, context)); - return await Promise.all(result); -}; +export const mapTxIns = (txIns: Cardano.TxIn[], context: LedgerTxTransformerContext): Ledger.TxInput[] => + txIns.map((txIn) => toTxIn(txIn, context)); diff --git a/packages/hardware-ledger/src/types.ts b/packages/hardware-ledger/src/types.ts index 4b887d277e8..90a57a40565 100644 --- a/packages/hardware-ledger/src/types.ts +++ b/packages/hardware-ledger/src/types.ts @@ -1,5 +1,5 @@ import { Cardano } from '@cardano-sdk/core'; -import { GroupedAddress } from '@cardano-sdk/key-management'; +import { SignTransactionContext } from '@cardano-sdk/key-management'; import TransportNodeHid from '@ledgerhq/hw-transport-node-hid-noevents'; import TransportWebHID from '@ledgerhq/hw-transport-webhid'; @@ -12,13 +12,10 @@ export type LedgerTransportType = TransportWebHID | TransportNodeHid; /** * The LedgerTxTransformerContext type represents the additional context necessary for * transforming a Core transaction into a Ledger device compatible transaction. - * - * @property {Cardano.ChainId} chainId - The Cardano blockchain's network identifier (e.g., mainnet or testnet). - * @property {Cardano.InputResolver} inputResolver - A function that resolves transaction txOut from the given txIn. - * @property {GroupedAddress[]} knownAddresses - An array of grouped known addresses by wallet. */ export type LedgerTxTransformerContext = { + /** The Cardano blockchain's network identifier (e.g., mainnet or testnet). */ chainId: Cardano.ChainId; - inputResolver: Cardano.InputResolver; - knownAddresses: GroupedAddress[]; -}; + /** Non-hardened account in cip1852 */ + accountIndex: number; +} & SignTransactionContext; diff --git a/packages/hardware-ledger/test/LedgerKeyAgent.test.ts b/packages/hardware-ledger/test/LedgerKeyAgent.test.ts index 67304e356df..757b86c4c9a 100644 --- a/packages/hardware-ledger/test/LedgerKeyAgent.test.ts +++ b/packages/hardware-ledger/test/LedgerKeyAgent.test.ts @@ -364,6 +364,10 @@ describe('LedgerKeyAgent', () => { describe('Unsupported Transaction Errors', () => { let keyAgentMock: LedgerKeyAgent; const txId = '0000000000000000000000000000000000000000000000000000000000000000' as unknown as Cardano.TransactionId; + const noAddressesOptions = { + knownAddresses: [], + txInKeyPathMap: {} + }; beforeAll(() => { LedgerKeyAgent.checkDeviceConnection = async () => new Ada(new Transport()); @@ -373,15 +377,12 @@ describe('LedgerKeyAgent', () => { accountIndex: 0, chainId: Cardano.ChainIds.Preview, communicationType: CommunicationType.Node, - extendedAccountPublicKey: - '0000000000000000000000000000000000000000000000000000000000000000' as unknown as Crypto.Bip32PublicKeyHex, - knownAddresses: [] + extendedAccountPublicKey: Crypto.Bip32PublicKeyHex( + '00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000' + ) }, { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - inputResolver: { - resolveInput: jest.fn().mockResolvedValue(null) - }, logger: dummyLogger } ); @@ -392,20 +393,23 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__POOL_REGISTRATION_NOT_ALLOWED); }); @@ -414,24 +418,27 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.StakeDelegation, - poolId, - stakeCredential: { - hash: '00000000000000000000000000000000000000000000000000000000' as unknown as Crypto.Hash28ByteBase16, - type: Cardano.CredentialType.KeyHash + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.StakeDelegation, + poolId, + stakeCredential: { + hash: '00000000000000000000000000000000000000000000000000000000' as unknown as Crypto.Hash28ByteBase16, + type: Cardano.CredentialType.KeyHash + } } - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__CERTIFICATE_STAKE_CREDENTIAL_ONLY_AS_PATH); }); @@ -440,22 +447,25 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut], - withdrawals: [ - { - quantity: 5n, - stakeAddress: Cardano.RewardAccount( - 'stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27' - ) - } - ] + await keyAgentMock.signTransaction( + { + body: { + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut], + withdrawals: [ + { + quantity: 5n, + stakeAddress: Cardano.RewardAccount( + 'stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27' + ) + } + ] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__WITHDRAWAL_ONLY_AS_PATH); }); @@ -464,15 +474,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - collaterals: [{ ...txIn, index: txIn.index + 1 }], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + collaterals: [{ ...txIn, index: txIn.index + 1 }], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__COLLATERAL_INPUTS_NOT_ALLOWED); }); @@ -481,15 +494,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - collateralReturn: pureAdaTxOut, - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + collateralReturn: pureAdaTxOut, + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__COLLATERAL_OUTPUT_NOT_ALLOWED); }); @@ -498,15 +514,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut], - totalCollateral: 10n + await keyAgentMock.signTransaction( + { + body: { + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut], + totalCollateral: 10n + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__TOTAL_COLLATERAL_NOT_ALLOWED); }); @@ -515,15 +534,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut], - referenceInputs: [txIn] + await keyAgentMock.signTransaction( + { + body: { + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut], + referenceInputs: [txIn] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_ORDINARY__REFERENCE_INPUTS_NOT_ALLOWED); }); @@ -532,20 +554,23 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_MULTISIG__POOL_REGISTRATION_NOT_ALLOWED); }); @@ -554,15 +579,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - collaterals: [{ ...txIn, index: txIn.index + 1 }], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + collaterals: [{ ...txIn, index: txIn.index + 1 }], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_MULTISIG__COLLATERAL_INPUTS_NOT_ALLOWED); }); @@ -571,15 +599,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - collateralReturn: pureAdaTxOut, - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + collateralReturn: pureAdaTxOut, + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_MULTISIG__COLLATERAL_OUTPUT_NOT_ALLOWED); }); @@ -588,15 +619,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut], - totalCollateral: 10n + await keyAgentMock.signTransaction( + { + body: { + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut], + totalCollateral: 10n + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_MULTISIG__TOTAL_COLLATERAL_NOT_ALLOWED); }); @@ -605,15 +639,18 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut], - referenceInputs: [txIn] + await keyAgentMock.signTransaction( + { + body: { + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut], + referenceInputs: [txIn] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_MULTISIG__REFERENCE_INPUTS_NOT_ALLOWED); }); @@ -622,20 +659,23 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_PLUTUS__POOL_REGISTRATION_NOT_ALLOWED); }); @@ -644,20 +684,23 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [txOutWithDatum] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [txOutWithDatum] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_POOL_OWNER__DATUM_NOT_ALLOWED); }); @@ -666,21 +709,24 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut], - referenceInputs: [txIn] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut], + referenceInputs: [txIn] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_POOL_OWNER__SINGLE_DEVICE_OWNER_REQUIRED); }); @@ -689,24 +735,27 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - }, - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + }, + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_POOL_OWNER__SINGLE_POOL_REG_CERTIFICATE_REQUIRED); }); @@ -715,20 +764,23 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [txOutWithDatum] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [txOutWithDatum] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_POOL_OPERATOR__DATUM_NOT_ALLOWED); }); @@ -737,24 +789,27 @@ describe('LedgerKeyAgent', () => { await expect( async () => - await keyAgentMock.signTransaction({ - body: { - certificates: [ - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - }, - { - __typename: Cardano.CertificateType.PoolRegistration, - poolParameters - } - ], - fee: 10n, - inputs: [txIn], - outputs: [pureAdaTxOut] + await keyAgentMock.signTransaction( + { + body: { + certificates: [ + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + }, + { + __typename: Cardano.CertificateType.PoolRegistration, + poolParameters + } + ], + fee: 10n, + inputs: [txIn], + outputs: [pureAdaTxOut] + }, + hash: txId }, - hash: txId - }) + noAddressesOptions + ) ).rejects.toThrow(InvalidDataReason.SIGN_MODE_POOL_OPERATOR__SINGLE_POOL_REG_CERTIFICATE_REQUIRED); }); }); diff --git a/packages/hardware-ledger/test/testData.ts b/packages/hardware-ledger/test/testData.ts index 614195542a8..40f59485037 100644 --- a/packages/hardware-ledger/test/testData.ts +++ b/packages/hardware-ledger/test/testData.ts @@ -2,6 +2,7 @@ import * as Crypto from '@cardano-sdk/crypto'; import { AddressType, KeyRole } from '@cardano-sdk/key-management'; import { Base64Blob, HexBlob } from '@cardano-sdk/util'; import { Cardano, Serialization } from '@cardano-sdk/core'; +import { LedgerTxTransformerContext } from '../src'; export const rewardAccount = Cardano.RewardAccount('stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr'); export const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount); export const stakeCredential = { @@ -330,12 +331,12 @@ export const babbageTxWithoutScript: Cardano.Tx = { } }; -export const CONTEXT_WITH_KNOWN_ADDRESSES = { +export const CONTEXT_WITH_KNOWN_ADDRESSES: LedgerTxTransformerContext = { + accountIndex: 0, chainId: { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, - inputResolver: { resolveInput: () => Promise.resolve(txOutToOwnedAddress) }, knownAddresses: [ { accountIndex: 0, @@ -349,14 +350,16 @@ export const CONTEXT_WITH_KNOWN_ADDRESSES = { }, type: AddressType.Internal } - ] + ], + txInKeyPathMap: {} }; -export const CONTEXT_WITHOUT_KNOWN_ADDRESSES = { +export const CONTEXT_WITHOUT_KNOWN_ADDRESSES: LedgerTxTransformerContext = { + accountIndex: 0, chainId: { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, - inputResolver: { resolveInput: () => Promise.resolve(null) }, - knownAddresses: [] + knownAddresses: [], + txInKeyPathMap: {} }; diff --git a/packages/hardware-ledger/test/transformers/collateralInputs.test.ts b/packages/hardware-ledger/test/transformers/collateralInputs.test.ts index b3541833e4f..54ceff38e9a 100644 --- a/packages/hardware-ledger/test/transformers/collateralInputs.test.ts +++ b/packages/hardware-ledger/test/transformers/collateralInputs.test.ts @@ -1,23 +1,38 @@ import { CONTEXT_WITH_KNOWN_ADDRESSES, txIn } from '../testData'; -import { CardanoKeyConst, util } from '@cardano-sdk/key-management'; +import { CardanoKeyConst, TxInId, util } from '@cardano-sdk/key-management'; import { mapCollateralTxIns } from '../../src/transformers'; describe('collateralInputs', () => { describe('mapCollateralTxIns', () => { it('return null if given an undefined object as collateral txIns', async () => { - const txIns = await mapCollateralTxIns(undefined, CONTEXT_WITH_KNOWN_ADDRESSES); + const txIns = mapCollateralTxIns(undefined, CONTEXT_WITH_KNOWN_ADDRESSES); expect(txIns).toEqual(null); }); it('can map a a set of collateral inputs', async () => { - const txIns = await mapCollateralTxIns([txIn, txIn, txIn], CONTEXT_WITH_KNOWN_ADDRESSES); + const keyPath = { + index: 0, + role: 1 + }; + const txIns = mapCollateralTxIns([txIn, txIn, txIn], { + ...CONTEXT_WITH_KNOWN_ADDRESSES, + txInKeyPathMap: { + [TxInId(txIn)]: keyPath + } + }); expect(txIns!.length).toEqual(3); for (const input of txIns!) { expect(input).toEqual({ outputIndex: 0, - path: [util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), util.harden(0), 1, 0], + path: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + keyPath.role, + keyPath.index + ], txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' }); } diff --git a/packages/hardware-ledger/test/transformers/tx.test.ts b/packages/hardware-ledger/test/transformers/tx.test.ts index 668c210c6fc..1fa3eb7e25a 100644 --- a/packages/hardware-ledger/test/transformers/tx.test.ts +++ b/packages/hardware-ledger/test/transformers/tx.test.ts @@ -1,12 +1,22 @@ import * as Ledger from '@cardano-foundation/ledgerjs-hw-app-cardano'; import { CONTEXT_WITH_KNOWN_ADDRESSES, babbageTxWithoutScript, tx } from '../testData'; -import { CardanoKeyConst, util } from '@cardano-sdk/key-management'; +import { CardanoKeyConst, TxInId, util } from '@cardano-sdk/key-management'; import { toLedgerTx } from '../../src'; describe('tx', () => { describe('toLedgerTx', () => { test('can map a transaction with scripts', async () => { - expect(await toLedgerTx(tx.body, CONTEXT_WITH_KNOWN_ADDRESSES)).toEqual({ + const paymentKeyPath = { index: 0, role: 1 }; + const stakeKeyPath = { index: 0, role: 2 }; + expect( + await toLedgerTx(tx.body, { + ...CONTEXT_WITH_KNOWN_ADDRESSES, + txInKeyPathMap: { + [TxInId(tx.body.inputs[0])]: paymentKeyPath, + [TxInId(tx.body.collaterals![0])]: paymentKeyPath + } + }) + ).toEqual({ auxiliaryData: { params: { hashHex: '2ceb364d93225b4a0f004a0975a13eb50c3cc6348474b4fe9121f8dc72ca0cfa' @@ -19,9 +29,9 @@ describe('tx', () => { poolKeyPath: [ util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), - util.harden(0), - 2, - 0 + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + stakeKeyPath.role, + stakeKeyPath.index ], retirementEpoch: 500 }, @@ -31,7 +41,13 @@ describe('tx', () => { collateralInputs: [ { outputIndex: 1, - path: [util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), util.harden(0), 1, 0], + path: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + paymentKeyPath.role, + paymentKeyPath.index + ], txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' } ], @@ -41,7 +57,13 @@ describe('tx', () => { inputs: [ { outputIndex: 0, - path: [util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), util.harden(0), 1, 0], + path: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + paymentKeyPath.role, + paymentKeyPath.index + ], txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' } ], @@ -152,7 +174,15 @@ describe('tx', () => { }); test('can map a transaction without scripts', async () => { - expect(await toLedgerTx(babbageTxWithoutScript.body, CONTEXT_WITH_KNOWN_ADDRESSES)).toEqual({ + const paymentKeyPath = { index: 0, role: 1 }; + expect( + await toLedgerTx(babbageTxWithoutScript.body, { + ...CONTEXT_WITH_KNOWN_ADDRESSES, + txInKeyPathMap: { + [TxInId(babbageTxWithoutScript.body.inputs[0])]: paymentKeyPath + } + }) + ).toEqual({ auxiliaryData: null, certificates: null, collateralInputs: null, @@ -162,7 +192,13 @@ describe('tx', () => { inputs: [ { outputIndex: 0, - path: [util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), util.harden(0), 1, 0], + path: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + paymentKeyPath.role, + paymentKeyPath.index + ], txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' } ], diff --git a/packages/hardware-ledger/test/transformers/txIn.test.ts b/packages/hardware-ledger/test/transformers/txIn.test.ts index e59999b1f83..0d9fbc58449 100644 --- a/packages/hardware-ledger/test/transformers/txIn.test.ts +++ b/packages/hardware-ledger/test/transformers/txIn.test.ts @@ -1,19 +1,30 @@ import { CONTEXT_WITHOUT_KNOWN_ADDRESSES, CONTEXT_WITH_KNOWN_ADDRESSES, txIn } from '../testData'; -import { CardanoKeyConst, util } from '@cardano-sdk/key-management'; +import { CardanoKeyConst, TxInId, util } from '@cardano-sdk/key-management'; import { mapTxIns, toTxIn } from '../../src/transformers'; describe('txIn', () => { + const paymentKeyPath = { index: 0, role: 1 }; + describe('mapTxIns', () => { it('can map a a set of TxIns', async () => { - const txIns = await mapTxIns([txIn, txIn, txIn], CONTEXT_WITH_KNOWN_ADDRESSES); + const txIns = mapTxIns([txIn, txIn, txIn], { + ...CONTEXT_WITH_KNOWN_ADDRESSES, + txInKeyPathMap: { [TxInId(txIn)]: paymentKeyPath } + }); expect(txIns.length).toEqual(3); for (const input of txIns) { expect(input).toEqual({ - outputIndex: 0, - path: [util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), util.harden(0), 1, 0], - txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' + outputIndex: txIn.index, + path: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + paymentKeyPath.role, + paymentKeyPath.index + ], + txHashHex: txIn.txId }); } @@ -23,22 +34,31 @@ describe('txIn', () => { describe('toTxIn', () => { it('can map a simple txIn from third party address', async () => { - const ledgerTxIn = await toTxIn(txIn, CONTEXT_WITHOUT_KNOWN_ADDRESSES); + const ledgerTxIn = toTxIn(txIn, CONTEXT_WITHOUT_KNOWN_ADDRESSES); expect(ledgerTxIn).toEqual({ - outputIndex: 0, + outputIndex: txIn.index, path: null, - txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' + txHashHex: txIn.txId }); }); it('can map a simple txIn from own address', async () => { - const ledgerTxIn = await toTxIn(txIn, CONTEXT_WITH_KNOWN_ADDRESSES); + const ledgerTxIn = toTxIn(txIn, { + ...CONTEXT_WITH_KNOWN_ADDRESSES, + txInKeyPathMap: { [TxInId(txIn)]: paymentKeyPath } + }); expect(ledgerTxIn).toEqual({ - outputIndex: 0, - path: [util.harden(CardanoKeyConst.PURPOSE), util.harden(CardanoKeyConst.COIN_TYPE), util.harden(0), 1, 0], - txHashHex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5' + outputIndex: txIn.index, + path: [ + util.harden(CardanoKeyConst.PURPOSE), + util.harden(CardanoKeyConst.COIN_TYPE), + util.harden(CONTEXT_WITH_KNOWN_ADDRESSES.accountIndex), + paymentKeyPath.role, + paymentKeyPath.index + ], + txHashHex: txIn.txId }); }); }); diff --git a/packages/hardware-trezor/src/TrezorKeyAgent.ts b/packages/hardware-trezor/src/TrezorKeyAgent.ts index 43b21669509..0d02e4f5920 100644 --- a/packages/hardware-trezor/src/TrezorKeyAgent.ts +++ b/packages/hardware-trezor/src/TrezorKeyAgent.ts @@ -10,6 +10,7 @@ import { KeyAgentType, SerializableTrezorKeyAgentData, SignBlobResult, + SignTransactionContext, TrezorConfig, errors, util @@ -133,7 +134,6 @@ export class TrezorKeyAgent extends KeyAgentBase { chainId, extendedAccountPublicKey, isTrezorInitialized, - knownAddresses: [], trezorConfig }, dependencies @@ -164,14 +164,18 @@ export class TrezorKeyAgent extends KeyAgentBase { return Trezor.PROTO.CardanoTxSigningMode.ORDINARY_TRANSACTION; } - async signTransaction(tx: Cardano.TxBodyWithHash): Promise { + async signTransaction( + tx: Cardano.TxBodyWithHash, + { knownAddresses, txInKeyPathMap }: SignTransactionContext + ): Promise { try { await this.isTrezorInitialized; - const trezorTxData = await txToTrezor({ + const trezorTxData = txToTrezor({ + accountIndex: this.accountIndex, cardanoTxBody: tx.body, chainId: this.chainId, - inputResolver: this.inputResolver, - knownAddresses: this.knownAddresses + knownAddresses, + txInKeyPathMap }); const signingMode = TrezorKeyAgent.getSigningMode(trezorTxData); @@ -183,9 +187,9 @@ export class TrezorKeyAgent extends KeyAgentBase { }); const expectedPublicKeys = await Promise.all( - ( - await util.ownSignatureKeyPaths(tx.body, this.knownAddresses, this.inputResolver) - ).map((derivationPath) => this.derivePublicKey(derivationPath)) + util + .ownSignatureKeyPaths(tx.body, knownAddresses, txInKeyPathMap) + .map((derivationPath) => this.derivePublicKey(derivationPath)) ); if (!result.success) { diff --git a/packages/hardware-trezor/src/transformers/keyPaths.ts b/packages/hardware-trezor/src/transformers/keyPaths.ts index bc46267ff5a..0e6d7c32cd1 100644 --- a/packages/hardware-trezor/src/transformers/keyPaths.ts +++ b/packages/hardware-trezor/src/transformers/keyPaths.ts @@ -1,17 +1,18 @@ import { BIP32Path } from '@cardano-sdk/crypto'; import { Cardano } from '@cardano-sdk/core'; import { TrezorTxTransformerContext } from '../types'; -import { util } from '@cardano-sdk/key-management'; +import { TxInId, util } from '@cardano-sdk/key-management'; /** Uses the given Trezor input resolver to resolve the payment key path for known addresses for given input transaction. */ -export const resolvePaymentKeyPathForTxIn = async ( +export const resolvePaymentKeyPathForTxIn = ( txIn: Cardano.TxIn, context?: TrezorTxTransformerContext -): Promise => { +): BIP32Path | undefined => { if (!context) return; - const txOut = await context.inputResolver.resolveInput(txIn); - const knownAddress = context.knownAddresses.find(({ address }) => address === txOut?.address); - return knownAddress ? util.paymentKeyPathFromGroupedAddress(knownAddress) : undefined; + const txInKeyPath = context?.txInKeyPathMap[TxInId(txIn)]; + if (txInKeyPath) { + return util.accountKeyDerivationPathToBip32Path(context.accountIndex, txInKeyPath); + } }; // Resolves the stake key path for known addresses for the given reward address. diff --git a/packages/hardware-trezor/src/transformers/tx.ts b/packages/hardware-trezor/src/transformers/tx.ts index 4132f5d17e6..84eef1cdea7 100644 --- a/packages/hardware-trezor/src/transformers/tx.ts +++ b/packages/hardware-trezor/src/transformers/tx.ts @@ -1,6 +1,5 @@ import * as Trezor from '@trezor/connect'; import { Cardano } from '@cardano-sdk/core'; -import { GroupedAddress } from '@cardano-sdk/key-management'; import { TrezorTxTransformerContext } from '../types'; import { mapAdditionalWitnessRequests } from './additionalWitnessRequests'; import { mapAuxiliaryData, mapCerts, mapRequiredSigners, mapTxIns, mapTxOuts, mapWithdrawals, toTxOut } from './'; @@ -13,16 +12,16 @@ import { mapTokenMap } from './assets'; * this function to the Transformer interface like in the * hardware-ledger package) */ -const trezorTxTransformer = async ( +const trezorTxTransformer = ( body: Cardano.TxBody, context: TrezorTxTransformerContext -): Promise> => { - const inputs = await mapTxIns(body.inputs, context); +): Omit => { + const inputs = mapTxIns(body.inputs, context); return { additionalWitnessRequests: mapAdditionalWitnessRequests(inputs, context), auxiliaryData: body.auxiliaryDataHash ? mapAuxiliaryData(body.auxiliaryDataHash) : undefined, certificates: mapCerts(body.certificates ?? [], context), - collateralInputs: body.collaterals ? await mapTxIns(body.collaterals, context) : undefined, + collateralInputs: body.collaterals ? mapTxIns(body.collaterals, context) : undefined, collateralReturn: body.collateralReturn ? toTxOut(body.collateralReturn, context) : undefined, fee: body.fee.toString(), inputs, @@ -30,7 +29,7 @@ const trezorTxTransformer = async ( networkId: context.chainId.networkId, outputs: mapTxOuts(body.outputs, context), protocolMagic: context.chainId.networkMagic, - referenceInputs: body.referenceInputs ? await mapTxIns(body.referenceInputs, context) : undefined, + referenceInputs: body.referenceInputs ? mapTxIns(body.referenceInputs, context) : undefined, requiredSigners: body.requiredExtraSignatures ? mapRequiredSigners(body.requiredExtraSignatures, context) : undefined, @@ -44,17 +43,8 @@ const trezorTxTransformer = async ( /** Takes a core transaction and context data necessary to transform it into a trezor.CardanoSignTransaction */ export const txToTrezor = ({ cardanoTxBody, - chainId, - inputResolver, - knownAddresses + ...context }: { - chainId: Cardano.ChainId; - inputResolver: Cardano.InputResolver; - knownAddresses: GroupedAddress[]; cardanoTxBody: Cardano.TxBody; -}): Promise> => - trezorTxTransformer(cardanoTxBody, { - chainId, - inputResolver, - knownAddresses - }); +} & TrezorTxTransformerContext): Omit => + trezorTxTransformer(cardanoTxBody, context); diff --git a/packages/hardware-trezor/src/transformers/txIn.ts b/packages/hardware-trezor/src/transformers/txIn.ts index 04d7a83b260..c74918e26ca 100644 --- a/packages/hardware-trezor/src/transformers/txIn.ts +++ b/packages/hardware-trezor/src/transformers/txIn.ts @@ -8,15 +8,15 @@ import { resolvePaymentKeyPathForTxIn } from './keyPaths'; * Transforms the given Cardano input transaction to the Trezor * input transaction format using the given trezor input resolver. */ -export const toTrezorTxIn: Transform, TrezorTxTransformerContext> = async ( +export const toTrezorTxIn: Transform = ( txIn, context? ) => { - const path = await resolvePaymentKeyPathForTxIn(txIn, context); + const path = resolvePaymentKeyPathForTxIn(txIn, context); return { + path, prev_hash: txIn.txId, - prev_index: txIn.index, - ...(path ? { path } : {}) // optional destructuring of path + prev_index: txIn.index }; }; @@ -25,7 +25,5 @@ export const toTrezorTxIn: Transform, * an array of trezor Cardano transaction inputs using the * given context. */ -export const mapTxIns = async ( - txIns: Cardano.TxIn[], - context: TrezorTxTransformerContext -): Promise => Promise.all(txIns.map((txIn) => toTrezorTxIn(txIn, context))); +export const mapTxIns = (txIns: Cardano.TxIn[], context: TrezorTxTransformerContext): Trezor.CardanoInput[] => + txIns.map((txIn) => toTrezorTxIn(txIn, context)); diff --git a/packages/hardware-trezor/src/types.ts b/packages/hardware-trezor/src/types.ts index 8c7e1c74479..4bc0571640b 100644 --- a/packages/hardware-trezor/src/types.ts +++ b/packages/hardware-trezor/src/types.ts @@ -1,20 +1,17 @@ import * as Trezor from '@trezor/connect'; import { Cardano } from '@cardano-sdk/core'; -import { GroupedAddress } from '@cardano-sdk/key-management'; +import { SignTransactionContext } from '@cardano-sdk/key-management'; /** * The TrezorTxTransformerContext type represents the additional context necessary for * transforming a core transaction into a Trezor device compatible transaction. - * - * @property {Cardano.ChainId} chainId - The Cardano blockchain's network identifier (e.g., mainnet or testnet). - * @property {Cardano.InputResolver} inputResolver - A function that resolves transaction txOut from the given txIn. - * @property {GroupedAddress[]} knownAddresses - An array of grouped known addresses by wallet. */ export type TrezorTxTransformerContext = { + /** The Cardano blockchain's network identifier (e.g., mainnet or testnet). */ chainId: Cardano.ChainId; - inputResolver: Cardano.InputResolver; - knownAddresses: GroupedAddress[]; -}; + /** Non-hardened account in cip1852 */ + accountIndex: number; +} & SignTransactionContext; export type TrezorTxOutputDestination = | { diff --git a/packages/hardware-trezor/test/testData.ts b/packages/hardware-trezor/test/testData.ts index d8db762bf86..f01c0667370 100644 --- a/packages/hardware-trezor/test/testData.ts +++ b/packages/hardware-trezor/test/testData.ts @@ -2,6 +2,7 @@ import * as Crypto from '@cardano-sdk/crypto'; import { AddressType, GroupedAddress, KeyRole } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; +import { TrezorTxTransformerContext } from '../src'; export const mintTokenMap = new Map([ [Cardano.AssetId('2a286ad895d091f2b3d168a6091ad2627d30a72761a5bc36eef00740'), 20n], @@ -151,6 +152,8 @@ export const knownAddress: GroupedAddress = { type: AddressType.Internal }; +export const knownAddressPaymentKeyPath = { index: knownAddress.index, role: Number(knownAddress.type) }; + export const knownAddressWithoutStakingPath: GroupedAddress = { accountIndex: 0, address: paymentAddress, @@ -160,31 +163,34 @@ export const knownAddressWithoutStakingPath: GroupedAddress = { type: AddressType.Internal }; -export const contextWithKnownAddresses = { +export const contextWithKnownAddresses: TrezorTxTransformerContext = { + accountIndex: 0, chainId: { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, - inputResolver: { resolveInput: () => Promise.resolve(txOutToOwnedAddress) }, - knownAddresses: [knownAddress] + knownAddresses: [knownAddress], + txInKeyPathMap: {} }; -export const contextWithKnownAddressesWithoutStakingCredentials = { +export const contextWithKnownAddressesWithoutStakingCredentials: TrezorTxTransformerContext = { + accountIndex: 0, chainId: { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, - inputResolver: { resolveInput: () => Promise.resolve(txOutToOwnedAddress) }, - knownAddresses: [knownAddressWithoutStakingPath] + knownAddresses: [knownAddressWithoutStakingPath], + txInKeyPathMap: {} }; -export const contextWithoutKnownAddresses = { +export const contextWithoutKnownAddresses: TrezorTxTransformerContext = { + accountIndex: 0, chainId: { networkId: Cardano.NetworkId.Testnet, networkMagic: 999 }, - inputResolver: { resolveInput: () => Promise.resolve(null) }, - knownAddresses: [] + knownAddresses: [], + txInKeyPathMap: {} }; export const coreWithdrawalWithKeyHashCredential = { diff --git a/packages/hardware-trezor/test/transformers/additionalWitnessRequests.test.ts b/packages/hardware-trezor/test/transformers/additionalWitnessRequests.test.ts index c2483dde5e6..49aba2aa782 100644 --- a/packages/hardware-trezor/test/transformers/additionalWitnessRequests.test.ts +++ b/packages/hardware-trezor/test/transformers/additionalWitnessRequests.test.ts @@ -1,13 +1,18 @@ +import { TxInId } from '@cardano-sdk/key-management'; import { contextWithKnownAddresses, txIn } from '../testData'; import { mapAdditionalWitnessRequests } from '../../src/transformers/additionalWitnessRequests'; import { toTrezorTxIn } from '../../src'; describe('additionalWitnessRequests', () => { it('should include payment key paths and reward account key path from given inputs', async () => { - const mappedTrezorTxIn = await toTrezorTxIn(txIn, contextWithKnownAddresses); + const paymentKeyPath = { index: 0, role: 1 }; + const mappedTrezorTxIn = toTrezorTxIn(txIn, { + ...contextWithKnownAddresses, + txInKeyPathMap: { [TxInId(txIn)]: paymentKeyPath } + }); const result = mapAdditionalWitnessRequests([mappedTrezorTxIn], contextWithKnownAddresses); expect(result).toEqual([ - [2_147_485_500, 2_147_485_463, 2_147_483_648, 1, 0], // payment key path + [2_147_485_500, 2_147_485_463, 2_147_483_648, paymentKeyPath.role, paymentKeyPath.index], [2_147_485_500, 2_147_485_463, 2_147_483_648, 2, 0] // reward account key path ]); }); diff --git a/packages/hardware-trezor/test/transformers/keyPaths.test.ts b/packages/hardware-trezor/test/transformers/keyPaths.test.ts index b21a23edd2a..4c1928e9177 100644 --- a/packages/hardware-trezor/test/transformers/keyPaths.test.ts +++ b/packages/hardware-trezor/test/transformers/keyPaths.test.ts @@ -1,6 +1,8 @@ +import { TxInId } from '@cardano-sdk/key-management'; import { contextWithKnownAddresses, knownAddressKeyPath, + knownAddressPaymentKeyPath, knownAddressStakeKeyPath, rewardAddress, txIn @@ -10,7 +12,12 @@ import { resolvePaymentKeyPathForTxIn, resolveStakeKeyPath } from '../../src'; describe('key-paths', () => { describe('resolvePaymentKeyPathForTxIn', () => { it('returns the payment key path for a known address', async () => { - expect(await resolvePaymentKeyPathForTxIn(txIn, contextWithKnownAddresses)).toEqual(knownAddressKeyPath); + expect( + resolvePaymentKeyPathForTxIn(txIn, { + ...contextWithKnownAddresses, + txInKeyPathMap: { [TxInId(txIn)]: knownAddressPaymentKeyPath } + }) + ).toEqual(knownAddressKeyPath); }); }); describe('resolveStakeKeyPath', () => { diff --git a/packages/hardware-trezor/test/transformers/tx.test.ts b/packages/hardware-trezor/test/transformers/tx.test.ts index 9c846abafe4..b07f6c0e065 100644 --- a/packages/hardware-trezor/test/transformers/tx.test.ts +++ b/packages/hardware-trezor/test/transformers/tx.test.ts @@ -1,10 +1,11 @@ import * as Trezor from '@trezor/connect'; -import { CardanoKeyConst, util } from '@cardano-sdk/key-management'; +import { CardanoKeyConst, TxInId, util } from '@cardano-sdk/key-management'; import { babbageTxBodyWithScripts, contextWithKnownAddresses, contextWithoutKnownAddresses, knownAddressKeyPath, + knownAddressPaymentKeyPath, knownAddressStakeKeyPath, minValidTxBody, plutusTxWithBabbage, @@ -16,9 +17,9 @@ import { txToTrezor } from '../../src/transformers/tx'; describe('tx', () => { describe('txToTrezor', () => { - test('can map min valid transaction', async () => { + test('can map min valid transaction', () => { expect( - await txToTrezor({ + txToTrezor({ ...contextWithoutKnownAddresses, cardanoTxBody: minValidTxBody }) @@ -50,11 +51,12 @@ describe('tx', () => { }); }); - test('can map transaction without scripts', async () => { + test('can map transaction without scripts', () => { expect( - await txToTrezor({ + txToTrezor({ ...contextWithKnownAddresses, - cardanoTxBody: txBody + cardanoTxBody: txBody, + txInKeyPathMap: { [TxInId(txBody.inputs[0])]: knownAddressPaymentKeyPath } }) ).toEqual({ additionalWitnessRequests: [ @@ -206,11 +208,14 @@ describe('tx', () => { }); }); - test('can map babbage transaction with scripts', async () => { + test('can map babbage transaction with scripts', () => { expect( - await txToTrezor({ + txToTrezor({ ...contextWithKnownAddresses, - cardanoTxBody: babbageTxBodyWithScripts + cardanoTxBody: babbageTxBodyWithScripts, + txInKeyPathMap: { + [TxInId(babbageTxBodyWithScripts.inputs[0])]: knownAddressPaymentKeyPath + } }) ).toEqual({ additionalWitnessRequests: [ @@ -289,9 +294,9 @@ describe('tx', () => { }); }); - test('can map transaction with collaterals', async () => { + test('can map transaction with collaterals', () => { expect( - await txToTrezor({ + txToTrezor({ ...contextWithoutKnownAddresses, cardanoTxBody: txBodyWithCollaterals }) @@ -335,15 +340,25 @@ describe('tx', () => { }); }); - test('can map plutus transaction with babbage elements', async () => { + test('can map plutus transaction with babbage elements', () => { expect( - await txToTrezor({ + txToTrezor({ ...contextWithKnownAddresses, - cardanoTxBody: plutusTxWithBabbage + cardanoTxBody: plutusTxWithBabbage, + txInKeyPathMap: { + [TxInId(plutusTxWithBabbage.inputs[0])]: knownAddressPaymentKeyPath, + [TxInId(plutusTxWithBabbage.collaterals[0])]: knownAddressPaymentKeyPath + } }) ).toEqual({ additionalWitnessRequests: [ - [2_147_485_500, 2_147_485_463, 2_147_483_648, 1, 0], // payment key path + [ + 2_147_485_500, + 2_147_485_463, + 2_147_483_648, + knownAddressPaymentKeyPath.role, + knownAddressPaymentKeyPath.index + ], [2_147_485_500, 2_147_485_463, 2_147_483_648, 2, 0] // reward account key path ], auxiliaryData: { diff --git a/packages/hardware-trezor/test/transformers/txIn.test.ts b/packages/hardware-trezor/test/transformers/txIn.test.ts index 70d3e3c06ca..d87f4b85a0f 100644 --- a/packages/hardware-trezor/test/transformers/txIn.test.ts +++ b/packages/hardware-trezor/test/transformers/txIn.test.ts @@ -1,4 +1,11 @@ -import { contextWithKnownAddresses, contextWithoutKnownAddresses, knownAddressKeyPath, txIn } from '../testData'; +import { TxInId } from '@cardano-sdk/key-management'; +import { + contextWithKnownAddresses, + contextWithoutKnownAddresses, + knownAddressKeyPath, + knownAddressPaymentKeyPath, + txIn +} from '../testData'; import { mapTxIns, toTrezorTxIn } from '../../src'; const expectedTrezorTxInWithKnownAddress = { @@ -15,17 +22,23 @@ const expectedTrezorTxInWithoutKnownAddress = { describe('tx-inputs', () => { describe('toTrezorTxIn', () => { it('maps a simple tx input from an unknown third party address', async () => { - const mappedTrezorTxIn = await toTrezorTxIn(txIn, contextWithoutKnownAddresses); + const mappedTrezorTxIn = toTrezorTxIn(txIn, contextWithoutKnownAddresses); expect(mappedTrezorTxIn).toEqual(expectedTrezorTxInWithoutKnownAddress); }); it('maps a simple tx input from a known address', async () => { - const mappedTrezorTxIn = await toTrezorTxIn(txIn, contextWithKnownAddresses); + const mappedTrezorTxIn = toTrezorTxIn(txIn, { + ...contextWithKnownAddresses, + txInKeyPathMap: { [TxInId(txIn)]: knownAddressPaymentKeyPath } + }); expect(mappedTrezorTxIn).toEqual(expectedTrezorTxInWithKnownAddress); }); }); describe('mapTxIns', () => { it('can map a a set of TxIns', async () => { - const txIns = await mapTxIns([txIn, txIn, txIn], contextWithKnownAddresses); + const txIns = mapTxIns([txIn, txIn, txIn], { + ...contextWithKnownAddresses, + txInKeyPathMap: { [TxInId(txIn)]: knownAddressPaymentKeyPath } + }); expect(txIns).toEqual([ expectedTrezorTxInWithKnownAddress, expectedTrezorTxInWithKnownAddress, diff --git a/packages/key-management/src/Bip32Account.ts b/packages/key-management/src/Bip32Account.ts new file mode 100644 index 00000000000..154bfb147a2 --- /dev/null +++ b/packages/key-management/src/Bip32Account.ts @@ -0,0 +1,87 @@ +import * as Crypto from '@cardano-sdk/crypto'; +import { + AccountAddressDerivationPath, + AccountKeyDerivationPath, + AsyncKeyAgent, + GroupedAddress, + KeyRole +} from './types'; +import { Cardano } from '@cardano-sdk/core'; +import { Hash28ByteBase16 } from '@cardano-sdk/crypto'; + +type Bip32AccountProps = { + extendedAccountPublicKey: Crypto.Bip32PublicKeyHex; + accountIndex: number; + chainId: Cardano.ChainId; +}; + +/** Derives public keys and addresses from a BIP32-ED25519 public key */ +export class Bip32Account { + readonly extendedAccountPublicKey: Crypto.Bip32PublicKey; + readonly chainId: Cardano.ChainId; + readonly accountIndex: number; + + /** Initializes a new instance of the Bip32Ed25519AddressManager class. */ + constructor({ extendedAccountPublicKey, chainId, accountIndex }: Bip32AccountProps) { + this.extendedAccountPublicKey = Crypto.Bip32PublicKey.fromHex(extendedAccountPublicKey); + this.chainId = chainId; + this.accountIndex = accountIndex; + } + + async derivePublicKey(derivationPath: AccountKeyDerivationPath) { + const key = await this.extendedAccountPublicKey.derive([derivationPath.role, derivationPath.index]); + return key.toRawKey(); + } + + async deriveAddress( + paymentKeyDerivationPath: AccountAddressDerivationPath, + stakeKeyDerivationIndex: number + ): Promise { + const stakeKeyDerivationPath = { + index: stakeKeyDerivationIndex, + role: KeyRole.Stake + }; + + const derivedPublicPaymentKey = await this.derivePublicKey({ + index: paymentKeyDerivationPath.index, + role: Number(paymentKeyDerivationPath.type) + }); + + const derivedPublicPaymentKeyHash = await derivedPublicPaymentKey.hash(); + + const publicStakeKey = await this.derivePublicKey(stakeKeyDerivationPath); + const publicStakeKeyHash = await publicStakeKey.hash(); + + const stakeCredential = { hash: Hash28ByteBase16(publicStakeKeyHash.hex()), type: Cardano.CredentialType.KeyHash }; + + const address = Cardano.BaseAddress.fromCredentials( + this.chainId.networkId, + { hash: Hash28ByteBase16(derivedPublicPaymentKeyHash.hex()), type: Cardano.CredentialType.KeyHash }, + stakeCredential + ).toAddress(); + + const rewardAccount = Cardano.RewardAddress.fromCredentials(this.chainId.networkId, stakeCredential).toAddress(); + + return { + accountIndex: this.accountIndex, + address: Cardano.PaymentAddress(address.toBech32()), + networkId: this.chainId.networkId, + rewardAccount: Cardano.RewardAccount(rewardAccount.toBech32()), + stakeKeyDerivationPath, + ...paymentKeyDerivationPath + }; + } + + /** + * Creates a new instance of the Bip32Ed25519AddressManager class. + * + * @param keyAgent The key agent that will be used to derive addresses. + */ + static async fromAsyncKeyAgent(keyAgent: AsyncKeyAgent): Promise { + return new Bip32Account({ + accountIndex: await keyAgent.getAccountIndex(), + chainId: await keyAgent.getChainId(), + extendedAccountPublicKey: await keyAgent.getExtendedAccountPublicKey() + }); + } +} diff --git a/packages/key-management/src/InMemoryKeyAgent.ts b/packages/key-management/src/InMemoryKeyAgent.ts index 6cd43271cb5..827a46561df 100644 --- a/packages/key-management/src/InMemoryKeyAgent.ts +++ b/packages/key-management/src/InMemoryKeyAgent.ts @@ -9,6 +9,7 @@ import { KeyPair, SerializableInMemoryKeyAgentData, SignBlobResult, + SignTransactionContext, SignTransactionOptions } from './types'; import { Cardano } from '@cardano-sdk/core'; @@ -113,8 +114,7 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent { chainId, encryptedRootPrivateKeyBytes: [...encryptedRootPrivateKey], extendedAccountPublicKey, - getPassphrase, - knownAddresses: [] + getPassphrase }, dependencies ); @@ -122,14 +122,15 @@ export class InMemoryKeyAgent extends KeyAgentBase implements KeyAgent { async signTransaction( { body, hash }: Cardano.TxBodyWithHash, - { additionalKeyPaths = [] }: SignTransactionOptions | undefined = {} + { txInKeyPathMap, knownAddresses }: SignTransactionContext, + { additionalKeyPaths = [] }: SignTransactionOptions = {} ): Promise { // Possible optimization is casting strings to OpaqueString types directly and skipping validation const blob = HexBlob(hash); const dRepKeyHash = ( await Crypto.Ed25519PublicKey.fromHex(await this.derivePublicKey(DREP_KEY_DERIVATION_PATH)).hash() ).hex(); - const derivationPaths = await ownSignatureKeyPaths(body, this.knownAddresses, this.inputResolver, dRepKeyHash); + const derivationPaths = ownSignatureKeyPaths(body, knownAddresses, txInKeyPathMap, dRepKeyHash); const keyPaths = uniqBy([...derivationPaths, ...additionalKeyPaths], ({ role, index }) => `${role}.${index}`); // TODO: // if (keyPaths.length === 0) { diff --git a/packages/key-management/src/KeyAgentBase.ts b/packages/key-management/src/KeyAgentBase.ts index 6f22b03e3be..8e761a3afed 100644 --- a/packages/key-management/src/KeyAgentBase.ts +++ b/packages/key-management/src/KeyAgentBase.ts @@ -5,26 +5,20 @@ import { GroupedAddress, KeyAgent, KeyAgentDependencies, - KeyRole, SerializableKeyAgentData, SignBlobResult, + SignTransactionContext, SignTransactionOptions } from './types'; +import { Bip32Account } from './Bip32Account'; import { Cardano } from '@cardano-sdk/core'; -import { Hash28ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob } from '@cardano-sdk/util'; export abstract class KeyAgentBase implements KeyAgent { readonly #serializableData: SerializableKeyAgentData; readonly #bip32Ed25519: Crypto.Bip32Ed25519; - protected readonly inputResolver: Cardano.InputResolver; + readonly #account: Bip32Account; - get knownAddresses(): GroupedAddress[] { - return this.#serializableData.knownAddresses; - } - set knownAddresses(addresses: GroupedAddress[]) { - this.#serializableData.knownAddresses = addresses; - } get serializableData(): SerializableKeyAgentData { return this.#serializableData; } @@ -45,75 +39,25 @@ export abstract class KeyAgentBase implements KeyAgent { abstract exportRootPrivateKey(): Promise; abstract signTransaction( txInternals: Cardano.TxBodyWithHash, + context: SignTransactionContext, signTransactionOptions?: SignTransactionOptions ): Promise; - constructor(serializableData: SerializableKeyAgentData, { inputResolver, bip32Ed25519 }: KeyAgentDependencies) { + constructor(serializableData: SerializableKeyAgentData, { bip32Ed25519 }: KeyAgentDependencies) { this.#serializableData = serializableData; - this.inputResolver = inputResolver; this.#bip32Ed25519 = bip32Ed25519; + this.#account = new Bip32Account(serializableData); } /** See https://github.com/cardano-foundation/CIPs/tree/master/CIP-1852#specification */ async deriveAddress( - { index, type }: AccountAddressDerivationPath, - stakeKeyDerivationIndex: number, - pure?: boolean + paymentKeyDerivationPath: AccountAddressDerivationPath, + stakeKeyDerivationIndex: number ): Promise { - const stakeKeyDerivationPath = { - index: stakeKeyDerivationIndex, - role: KeyRole.Stake - }; - - const knownAddress = this.knownAddresses.find( - (addr) => - addr.type === type && - addr.index === index && - addr.stakeKeyDerivationPath?.index === stakeKeyDerivationPath.index - ); - - if (knownAddress) return knownAddress; - const derivedPublicPaymentKey = await this.derivePublicKey({ - index, - role: type as unknown as KeyRole - }); - - const derivedPublicPaymentKeyHash = await this.#bip32Ed25519.getPubKeyHash(derivedPublicPaymentKey); - - const publicStakeKey = await this.derivePublicKey(stakeKeyDerivationPath); - const publicStakeKeyHash = await this.#bip32Ed25519.getPubKeyHash(publicStakeKey); - - const stakeCredential = { hash: Hash28ByteBase16(publicStakeKeyHash), type: Cardano.CredentialType.KeyHash }; - - const address = Cardano.BaseAddress.fromCredentials( - this.chainId.networkId, - { hash: Hash28ByteBase16(derivedPublicPaymentKeyHash), type: Cardano.CredentialType.KeyHash }, - stakeCredential - ).toAddress(); - - const rewardAccount = Cardano.RewardAddress.fromCredentials(this.chainId.networkId, stakeCredential).toAddress(); - - const groupedAddress = { - accountIndex: this.accountIndex, - address: Cardano.PaymentAddress(address.toBech32()), - index, - networkId: this.chainId.networkId, - rewardAccount: Cardano.RewardAccount(rewardAccount.toBech32()), - stakeKeyDerivationPath, - type - }; - - if (!pure) this.knownAddresses = [...this.knownAddresses, groupedAddress]; - - return groupedAddress; + return this.#account.deriveAddress(paymentKeyDerivationPath, stakeKeyDerivationIndex); } async derivePublicKey(derivationPath: AccountKeyDerivationPath): Promise { - const childKey = await this.#bip32Ed25519.derivePublicKey(this.extendedAccountPublicKey, [ - derivationPath.role, - derivationPath.index - ]); - - return await this.#bip32Ed25519.getRawPublicKey(childKey); + return (await this.#account.derivePublicKey(derivationPath)).hex(); } } diff --git a/packages/key-management/src/cip8/cip30signData.ts b/packages/key-management/src/cip8/cip30signData.ts index 696bcca43c8..b0b1a59e1be 100644 --- a/packages/key-management/src/cip8/cip30signData.ts +++ b/packages/key-management/src/cip8/cip30signData.ts @@ -1,5 +1,5 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AccountKeyDerivationPath } from '..'; +import { AccountKeyDerivationPath, GroupedAddress, KeyRole } from '../types'; import { AlgorithmId, CBORValue, @@ -13,21 +13,14 @@ import { ProtectedHeaderMap, SigStructure } from '@emurgo/cardano-message-signing-nodejs'; -import { - Bip32Ed25519AddressManager, - Bip32Ed25519Witnesser, - DREP_KEY_DERIVATION_PATH, - STAKE_KEY_DERIVATION_PATH -} from '../util'; +import { Bip32Ed25519Witnesser, DREP_KEY_DERIVATION_PATH, STAKE_KEY_DERIVATION_PATH } from '../util'; 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 { KeyRole } from '../types'; -import { filter, firstValueFrom } from 'rxjs'; export interface Cip30SignDataRequest { - addressManager: Bip32Ed25519AddressManager; + knownAddresses: GroupedAddress[]; witnesser: Bip32Ed25519Witnesser; signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID; payload: HexBlob; @@ -57,7 +50,7 @@ const getAddressBytes = (signWith: Cardano.PaymentAddress | Cardano.RewardAccoun const getDerivationPath = async ( signWith: Cardano.PaymentAddress | Cardano.RewardAccount | Cardano.DRepID, - addressManager: Bip32Ed25519AddressManager + knownAddresses: GroupedAddress[] ) => { if (Cardano.DRepID.isValid(signWith)) { return DREP_KEY_DERIVATION_PATH; @@ -65,10 +58,6 @@ const getDerivationPath = async ( const isRewardAccount = signWith.startsWith('stake'); - const knownAddresses = await firstValueFrom( - addressManager.knownAddresses$.pipe(filter((addresses) => addresses.length > 0)) - ); - if (isRewardAccount) { const knownRewardAddress = knownAddresses.find(({ rewardAccount }) => rewardAccount === signWith); @@ -123,7 +112,7 @@ const createCoseKey = (addressBytes: Uint8Array, publicKey: Crypto.Ed25519Public * @throws {Cip30DataSignError} */ export const cip30signData = async ({ - addressManager, + knownAddresses, witnesser, signWith, payload @@ -132,7 +121,7 @@ export const cip30signData = async ({ throw new Cip30DataSignError(Cip30DataSignErrorCode.AddressNotPK, 'Invalid address'); } const addressBytes = getAddressBytes(signWith); - const derivationPath = await getDerivationPath(signWith, addressManager); + const derivationPath = await getDerivationPath(signWith, knownAddresses); const builder = COSESign1Builder.new( Headers.new(ProtectedHeaderMap.new(createSigStructureHeaders(addressBytes)), HeaderMap.new()), diff --git a/packages/key-management/src/index.ts b/packages/key-management/src/index.ts index e4f3883fb08..f3bcb3200c9 100644 --- a/packages/key-management/src/index.ts +++ b/packages/key-management/src/index.ts @@ -5,3 +5,4 @@ export * as util from './util'; export * from './emip3'; export * from './types'; export * as cip8 from './cip8'; +export * from './Bip32Account'; diff --git a/packages/key-management/src/types.ts b/packages/key-management/src/types.ts index 67c18259467..c2127d91b17 100644 --- a/packages/key-management/src/types.ts +++ b/packages/key-management/src/types.ts @@ -1,8 +1,7 @@ import * as Crypto from '@cardano-sdk/crypto'; import { Cardano } from '@cardano-sdk/core'; -import { HexBlob, Shutdown } from '@cardano-sdk/util'; +import { HexBlob, OpaqueString, Shutdown } from '@cardano-sdk/util'; import { Logger } from 'ts-log'; -import { Observable } from 'rxjs'; export interface SignBlobResult { publicKey: Crypto.Ed25519PublicKeyHex; @@ -58,7 +57,6 @@ export enum CommunicationType { } export interface KeyAgentDependencies { - inputResolver: Cardano.InputResolver; logger: Logger; bip32Ed25519: Crypto.Bip32Ed25519; } @@ -91,7 +89,6 @@ export interface TrezorConfig { export interface SerializableKeyAgentDataBase { chainId: Cardano.ChainId; accountIndex: number; - knownAddresses: GroupedAddress[]; extendedAccountPublicKey: Crypto.Bip32PublicKeyHex; } @@ -130,16 +127,26 @@ export interface Ed25519KeyPair { */ export type GetPassphrase = (noCache?: true) => Promise; +export type TxInId = OpaqueString<'TxInId'>; +export const TxInId = ({ txId, index }: Cardano.TxIn) => `${txId}_${index}` as TxInId; + +export type TxInKeyPathMap = Partial>; +export type RewardAccountKeyPathMap = Partial>; +export type KeyHashKeyPathMap = Partial>; + export interface SignTransactionOptions { additionalKeyPaths?: AccountKeyDerivationPath[]; } +export interface SignTransactionContext { + txInKeyPathMap: TxInKeyPathMap; + knownAddresses: GroupedAddress[]; +} + export interface KeyAgent { get chainId(): Cardano.ChainId; get accountIndex(): number; get serializableData(): SerializableKeyAgentData; - get knownAddresses(): GroupedAddress[]; - set knownAddresses(addresses: GroupedAddress[]); get extendedAccountPublicKey(): Crypto.Bip32PublicKeyHex; get bip32Ed25519(): Crypto.Bip32Ed25519; @@ -150,7 +157,11 @@ export interface KeyAgent { /** * @throws AuthenticationError */ - signTransaction(txInternals: Cardano.TxBodyWithHash, options?: SignTransactionOptions): Promise; + signTransaction( + txInternals: Cardano.TxBodyWithHash, + context: SignTransactionContext, + options?: SignTransactionOptions + ): Promise; /** * @throws AuthenticationError */ @@ -165,8 +176,7 @@ export interface KeyAgent { */ deriveAddress( paymentKeyDerivationPath: AccountAddressDerivationPath, - stakeKeyDerivationIndex: number, - pure?: boolean + stakeKeyDerivationIndex: number ): Promise; /** * @throws AuthenticationError @@ -175,11 +185,10 @@ export interface KeyAgent { } export type AsyncKeyAgent = Pick & { - knownAddresses$: Observable; getChainId(): Promise; getBip32Ed25519(): Promise; getExtendedAccountPublicKey(): Promise; - setKnownAddresses(addresses: GroupedAddress[]): Promise; + getAccountIndex(): Promise; } & Shutdown; /** The result of the transaction signer signing operation. */ @@ -213,22 +222,14 @@ export interface Witnesser { * @param options Optional additional parameters that may influence how the witness data is generated. * @returns A promise that resolves to the generated witness data for the transaction. */ - witness(txInternals: Cardano.TxBodyWithHash, options?: WitnessOptions): Promise; + witness( + txInternals: Cardano.TxBodyWithHash, + context: SignTransactionContext, + options?: WitnessOptions + ): Promise; /** * @throws AuthenticationError */ signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob): Promise; } - -/** Interface for managing blockchain addresses. */ -export interface AddressManager { - knownAddresses$: Observable; - - /** - * Sets or updates the list of known addresses managed by this instance. - * - * @param addresses An array of grouped addresses to be managed. - */ - setKnownAddresses(addresses: GroupedAddress[]): Promise; -} diff --git a/packages/key-management/src/util/KeyAgentTransactionSigner.ts b/packages/key-management/src/util/KeyAgentTransactionSigner.ts index 69634dc83cf..c8ea903cd6c 100644 --- a/packages/key-management/src/util/KeyAgentTransactionSigner.ts +++ b/packages/key-management/src/util/KeyAgentTransactionSigner.ts @@ -28,9 +28,16 @@ export class KeyAgentTransactionSigner implements TransactionSigner { * @returns A Ed25519 transaction signature. */ async sign(tx: Cardano.TxBodyWithHash): Promise { - const signatures: Cardano.Signatures = await this.#keyAgent.signTransaction(tx, { - additionalKeyPaths: [this.#account] - }); + const signatures: Cardano.Signatures = await this.#keyAgent.signTransaction( + tx, + { + knownAddresses: [], + txInKeyPathMap: {} + }, + { + additionalKeyPaths: [this.#account] + } + ); if (signatures.size !== EXPECTED_SIG_NUM) throw new ProofGenerationError( diff --git a/packages/key-management/src/util/createAddressManager.ts b/packages/key-management/src/util/createAddressManager.ts deleted file mode 100644 index 2eb7d7fe0a6..00000000000 --- a/packages/key-management/src/util/createAddressManager.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { - AccountAddressDerivationPath, - AccountKeyDerivationPath, - AddressManager, - AsyncKeyAgent, - GroupedAddress -} from '../types'; -import { Observable } from 'rxjs'; - -/** An address manager that uses a {@link AsyncKeyAgent} to derive addresses. */ -export class Bip32Ed25519AddressManager implements AddressManager { - knownAddresses$: Observable; - #keyAgent: AsyncKeyAgent; - - /** - * Initializes a new instance of the Bip32Ed25519AddressManager class. - * - * @param keyAgent The key agent that will be used to derive addresses. - */ - constructor(keyAgent: AsyncKeyAgent) { - this.#keyAgent = keyAgent; - this.knownAddresses$ = keyAgent.knownAddresses$; - } - - async setKnownAddresses(addresses: GroupedAddress[]): Promise { - return this.#keyAgent.setKnownAddresses(addresses); - } - - async derivePublicKey(derivationPath: AccountKeyDerivationPath): Promise { - return this.#keyAgent.derivePublicKey(derivationPath); - } - - async deriveAddress( - paymentKeyDerivationPath: AccountAddressDerivationPath, - stakeKeyDerivationIndex: number, - pure?: boolean - ): Promise { - return this.#keyAgent.deriveAddress(paymentKeyDerivationPath, stakeKeyDerivationIndex, pure); - } - - shutdown(): void { - this.#keyAgent.shutdown(); - } -} - -/** - * Creates a new instance of the Bip32Ed25519AddressManager class. - * - * @param keyAgent The key agent that will be used to derive addresses. - */ -export const createBip32Ed25519AddressManager = (keyAgent: AsyncKeyAgent): Bip32Ed25519AddressManager => - new Bip32Ed25519AddressManager(keyAgent); diff --git a/packages/key-management/src/util/createAsyncKeyAgent.ts b/packages/key-management/src/util/createAsyncKeyAgent.ts index c3c1e630f12..71a3f5e2470 100644 --- a/packages/key-management/src/util/createAsyncKeyAgent.ts +++ b/packages/key-management/src/util/createAsyncKeyAgent.ts @@ -1,33 +1,17 @@ -import { AsyncKeyAgent, GroupedAddress, KeyAgent } from '../'; -import { BehaviorSubject } from 'rxjs'; +import { AsyncKeyAgent, KeyAgent } from '../'; -export const createAsyncKeyAgent = (keyAgent: KeyAgent, onShutdown?: () => void): AsyncKeyAgent => { - const knownAddresses$ = new BehaviorSubject(keyAgent.knownAddresses); - return { - async deriveAddress(derivationPath, stakeKeyDerivationIndex: number, pure?: boolean) { - const numAddresses = keyAgent.knownAddresses.length; - const address = await keyAgent.deriveAddress(derivationPath, stakeKeyDerivationIndex, pure); - - if (keyAgent.knownAddresses.length > numAddresses && !pure) { - knownAddresses$.next(keyAgent.knownAddresses); - } - - return address; - }, - derivePublicKey: keyAgent.derivePublicKey.bind(keyAgent), - getBip32Ed25519: () => Promise.resolve(keyAgent.bip32Ed25519), - getChainId: () => Promise.resolve(keyAgent.chainId), - getExtendedAccountPublicKey: () => Promise.resolve(keyAgent.extendedAccountPublicKey), - knownAddresses$, - setKnownAddresses: async (addresses: GroupedAddress[]): Promise => { - keyAgent.knownAddresses = addresses; - knownAddresses$.next(keyAgent.knownAddresses); - }, - shutdown() { - knownAddresses$.complete(); - onShutdown?.(); - }, - signBlob: keyAgent.signBlob.bind(keyAgent), - signTransaction: keyAgent.signTransaction.bind(keyAgent) - }; -}; +export const createAsyncKeyAgent = (keyAgent: KeyAgent, onShutdown?: () => void): AsyncKeyAgent => ({ + deriveAddress(derivationPath, stakeKeyDerivationIndex: number) { + return keyAgent.deriveAddress(derivationPath, stakeKeyDerivationIndex); + }, + derivePublicKey: keyAgent.derivePublicKey.bind(keyAgent), + getAccountIndex: () => Promise.resolve(keyAgent.accountIndex), + getBip32Ed25519: () => Promise.resolve(keyAgent.bip32Ed25519), + getChainId: () => Promise.resolve(keyAgent.chainId), + getExtendedAccountPublicKey: () => Promise.resolve(keyAgent.extendedAccountPublicKey), + shutdown() { + onShutdown?.(); + }, + signBlob: keyAgent.signBlob.bind(keyAgent), + signTransaction: keyAgent.signTransaction.bind(keyAgent) +}); diff --git a/packages/key-management/src/util/createWitnesser.ts b/packages/key-management/src/util/createWitnesser.ts index 863b4ed76d0..e424c37710c 100644 --- a/packages/key-management/src/util/createWitnesser.ts +++ b/packages/key-management/src/util/createWitnesser.ts @@ -1,4 +1,11 @@ -import { AccountKeyDerivationPath, AsyncKeyAgent, SignBlobResult, WitnessOptions, Witnesser } from '../types'; +import { + AccountKeyDerivationPath, + AsyncKeyAgent, + SignBlobResult, + SignTransactionContext, + WitnessOptions, + Witnesser +} from '../types'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; @@ -10,8 +17,12 @@ export class Bip32Ed25519Witnesser implements Witnesser { this.#keyAgent = keyAgent; } - async witness(txInternals: Cardano.TxBodyWithHash, options?: WitnessOptions): Promise { - return { signatures: await this.#keyAgent.signTransaction(txInternals, options) }; + async witness( + txInternals: Cardano.TxBodyWithHash, + context: SignTransactionContext, + options: WitnessOptions + ): Promise { + return { signatures: await this.#keyAgent.signTransaction(txInternals, context, options) }; } async signBlob(derivationPath: AccountKeyDerivationPath, blob: HexBlob): Promise { diff --git a/packages/key-management/src/util/ensureStakeKeys.ts b/packages/key-management/src/util/ensureStakeKeys.ts index 50dd1796f75..ef0ed3f54c5 100644 --- a/packages/key-management/src/util/ensureStakeKeys.ts +++ b/packages/key-management/src/util/ensureStakeKeys.ts @@ -1,12 +1,13 @@ -import { AddressType, KeyRole } from '../types'; -import { Bip32Ed25519AddressManager } from './createAddressManager'; +import { AddressType, GroupedAddress, KeyRole } from '../types'; +import { Bip32Account } from '../Bip32Account'; import { Cardano } from '@cardano-sdk/core'; import { Logger } from 'ts-log'; -import { firstValueFrom } from 'rxjs'; export interface EnsureStakeKeysParams { /** Key agent to use */ - addressManager: Bip32Ed25519AddressManager; + bip32Account: Bip32Account; + /** Look for existing stake keys used in those addresses */ + knownAddresses: GroupedAddress[]; /** Requested number of stake keys */ count: number; /** The payment key index to use when more stake keys are needed */ @@ -14,14 +15,19 @@ export interface EnsureStakeKeysParams { logger: Logger; } -/** Given a count, checks if enough stake keys exist and derives more if needed. Returns all reward accounts */ +export type EnsureStakeKeysResult = { + rewardAccounts: Cardano.RewardAccount[]; + newAddresses: GroupedAddress[]; +}; + +/** Given a count, checks if enough stake keys exist and derives more if needed. Returns all reward accounts and new addresses */ export const ensureStakeKeys = async ({ - addressManager, + bip32Account, + knownAddresses, count, logger, paymentKeyIndex: index = 0 -}: EnsureStakeKeysParams): Promise => { - const knownAddresses = await firstValueFrom(addressManager.knownAddresses$); +}: EnsureStakeKeysParams): Promise => { const stakeKeys = new Map( knownAddresses .filter( @@ -34,9 +40,11 @@ export const ensureStakeKeys = async ({ logger.debug(`Stake keys requested: ${count}; got ${stakeKeys.size}`); // Need more stake keys for the portfolio + const newAddresses: GroupedAddress[] = []; for (let stakeKeyIdx = 0; stakeKeys.size < count; stakeKeyIdx++) { if (!stakeKeys.has(stakeKeyIdx)) { - const address = await addressManager.deriveAddress({ index, type: AddressType.External }, stakeKeyIdx); + const address = await bip32Account.deriveAddress({ index, type: AddressType.External }, stakeKeyIdx); + newAddresses.push(address); logger.debug( `No derivation with stake key index ${stakeKeyIdx} exists. Derived a new stake key ${address.rewardAccount}.` ); @@ -44,5 +52,8 @@ export const ensureStakeKeys = async ({ } } - return [...stakeKeys.values()]; + return { + newAddresses, + rewardAccounts: [...stakeKeys.values()] + }; }; diff --git a/packages/key-management/src/util/index.ts b/packages/key-management/src/util/index.ts index a7421c31f6f..70f246ed1ce 100644 --- a/packages/key-management/src/util/index.ts +++ b/packages/key-management/src/util/index.ts @@ -5,5 +5,4 @@ export * from './ownSignatureKeyPaths'; export * from './stubSignTransaction'; export * from './KeyAgentTransactionSigner'; export * from './ensureStakeKeys'; -export * from './createAddressManager'; export * from './createWitnesser'; diff --git a/packages/key-management/src/util/key.ts b/packages/key-management/src/util/key.ts index af379a714ab..9e1b8ebb47c 100644 --- a/packages/key-management/src/util/key.ts +++ b/packages/key-management/src/util/key.ts @@ -39,6 +39,17 @@ export const deriveAccountPrivateKey = async ({ harden(accountIndex) ]); +// TODO: test +/** + * Constructs the hardened derivation path for the specified + * account key of an HD wallet as specified in CIP 1852 + * https://cips.cardano.org/cips/cip1852/ + */ +export const accountKeyDerivationPathToBip32Path = ( + accountIndex: number, + { index, role }: AccountKeyDerivationPath +): BIP32Path => [harden(CardanoKeyConst.PURPOSE), harden(CardanoKeyConst.COIN_TYPE), harden(accountIndex), role, index]; + /** * Constructs the hardened derivation path of the payment key for the * given grouped address of an HD wallet as specified in CIP 1852 diff --git a/packages/key-management/src/util/ownSignatureKeyPaths.ts b/packages/key-management/src/util/ownSignatureKeyPaths.ts index 8d2fe0d7836..f46df17e76a 100644 --- a/packages/key-management/src/util/ownSignatureKeyPaths.ts +++ b/packages/key-management/src/util/ownSignatureKeyPaths.ts @@ -1,10 +1,9 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AccountKeyDerivationPath, GroupedAddress } from '../types'; +import { AccountKeyDerivationPath, GroupedAddress, TxInId, TxInKeyPathMap } from '../types'; import { Cardano } from '@cardano-sdk/core'; import { DREP_KEY_DERIVATION_PATH } from './key'; import { isNotNil } from '@cardano-sdk/util'; import isEqual from 'lodash/isEqual'; -import uniq from 'lodash/uniq'; import uniqBy from 'lodash/uniqBy'; import uniqWith from 'lodash/uniqWith'; @@ -327,32 +326,41 @@ export const getDRepCredentialKeyPaths = ({ return signature; }; +// TODO: test +export const createTxInKeyPathMap = async ( + txBody: Cardano.TxBody, + knownAddresses: GroupedAddress[], + inputResolver: Cardano.InputResolver +) => { + const result: TxInKeyPathMap = {}; + const txInputs = [...txBody.inputs, ...(txBody.collaterals ? txBody.collaterals : [])]; + await Promise.all( + txInputs.map(async (txIn) => { + const resolution = await inputResolver.resolveInput(txIn); + if (!resolution) return; + const ownAddress = knownAddresses.find(({ address }) => address === resolution.address); + if (!ownAddress) return; + result[TxInId(txIn)] = { index: ownAddress.index, role: Number(ownAddress.type) }; + }) + ); + + return result; +}; + /** * Assumes that a single stake key is used for all addresses (index=0) * * @returns {AccountKeyDerivationPath[]} derivation paths for keys to sign transaction with */ -export const ownSignatureKeyPaths = async ( +export const ownSignatureKeyPaths = ( txBody: Cardano.TxBody, knownAddresses: GroupedAddress[], - inputResolver: Cardano.InputResolver, + txInKeyPathMap: TxInKeyPathMap, dRepKeyHash?: Crypto.Ed25519KeyHashHex -): Promise => { - const txInputs = [...txBody.inputs, ...(txBody.collaterals ? txBody.collaterals : [])]; - const paymentKeyPaths = uniq( - ( - await Promise.all( - txInputs.map(async (input) => { - const resolution = await inputResolver.resolveInput(input); - if (!resolution) return null; - return knownAddresses.find(({ address }) => address === resolution.address); - }) - ) - ).filter(isNotNil) - ).map(({ type, index }) => ({ index, role: Number(type) })); - +): AccountKeyDerivationPath[] => { // TODO: add `proposal_procedure` witnesses. + const paymentKeyPaths = Object.values(txInKeyPathMap).filter(isNotNil); return uniqWith( [ ...paymentKeyPaths, diff --git a/packages/key-management/src/util/stubSignTransaction.ts b/packages/key-management/src/util/stubSignTransaction.ts index ebfe706bd54..8a67d91ba6f 100644 --- a/packages/key-management/src/util/stubSignTransaction.ts +++ b/packages/key-management/src/util/stubSignTransaction.ts @@ -1,6 +1,6 @@ import * as Crypto from '@cardano-sdk/crypto'; import { Cardano } from '@cardano-sdk/core'; -import { GroupedAddress, SignTransactionOptions, TransactionSigner } from '../types'; +import { SignTransactionContext, SignTransactionOptions, TransactionSigner } from '../types'; import { deepEquals } from '@cardano-sdk/util'; import { ownSignatureKeyPaths } from './ownSignatureKeyPaths'; @@ -11,29 +11,26 @@ const randomPublicKey = () => Crypto.Ed25519PublicKeyHex(Array.from({ length: 64 export interface StubSignTransactionProps { txBody: Cardano.TxBody; - knownAddresses: GroupedAddress[]; - inputResolver: Cardano.InputResolver; extraSigners?: TransactionSigner[]; dRepPublicKey: Crypto.Ed25519PublicKeyHex; + context: SignTransactionContext; signTransactionOptions?: SignTransactionOptions; } export const stubSignTransaction = async ({ txBody, - knownAddresses, - inputResolver, extraSigners, dRepPublicKey, - signTransactionOptions = { additionalKeyPaths: [] } + context: { knownAddresses, txInKeyPathMap }, + signTransactionOptions: { additionalKeyPaths = [] } = {} }: StubSignTransactionProps): Promise => { - const { additionalKeyPaths = [] } = signTransactionOptions; const mockSignature = Crypto.Ed25519SignatureHex( // eslint-disable-next-line max-len 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff' ); const dRepKeyHash = (await Crypto.Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); const signatureKeyPaths = uniqWith( - [...(await ownSignatureKeyPaths(txBody, knownAddresses, inputResolver, dRepKeyHash)), ...additionalKeyPaths], + [...ownSignatureKeyPaths(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash), ...additionalKeyPaths], deepEquals ); diff --git a/packages/key-management/test/Bip32Account.test.ts b/packages/key-management/test/Bip32Account.test.ts new file mode 100644 index 00000000000..59dffb70469 --- /dev/null +++ b/packages/key-management/test/Bip32Account.test.ts @@ -0,0 +1,124 @@ +import * as Crypto from '@cardano-sdk/crypto'; +import { AccountKeyDerivationPath, AddressType, Bip32Account, InMemoryKeyAgent, KeyRole, util } from '../src'; +import { Cardano } from '@cardano-sdk/core'; +import { HexBlob } from '@cardano-sdk/util'; +import { dummyLogger } from 'ts-log'; + +describe('Bip32Account', () => { + const accountIndex = 1; + const testnetChainId = Cardano.ChainIds.Preview; + let testnetAccount: Bip32Account; + let mainnetAccount: Bip32Account; + + beforeEach(async () => { + const mnemonicWords = util.generateMnemonicWords(); + const getPassphrase = jest.fn().mockResolvedValue(Buffer.from('password')); + const keyAgentDependencies = { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger: dummyLogger }; + const testnetKeyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( + { + accountIndex, + chainId: testnetChainId, + getPassphrase, + mnemonicWords + }, + keyAgentDependencies + ); + const mainnetKeyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( + { + accountIndex, + chainId: Cardano.ChainIds.Mainnet, + getPassphrase, + mnemonicWords + }, + keyAgentDependencies + ); + + testnetAccount = new Bip32Account(testnetKeyAgent.serializableData); + mainnetAccount = new Bip32Account(mainnetKeyAgent.serializableData); + }); + + it('derivePublicKey resolves with ed25519 public key', async () => { + const derivedKeys = [ + await testnetAccount.derivePublicKey({ index: 0, role: KeyRole.DRep }), + await testnetAccount.derivePublicKey({ index: 1, role: KeyRole.External }), + await mainnetAccount.derivePublicKey({ index: 2, role: KeyRole.Internal }), + await mainnetAccount.derivePublicKey({ index: 3, role: KeyRole.Stake }) + ]; + for (const key of derivedKeys) { + const hexKey = key.hex(); + expect(typeof hexKey).toBe('string'); + expect(hexKey).toHaveLength(64); + expect(() => HexBlob(hexKey)).not.toThrow(); + } + }); + + describe('deriveAddress', () => { + it('derives valid mainnet address', async () => { + const externalAddress = await mainnetAccount.deriveAddress({ index: 1, type: AddressType.External }, 1); + expect(externalAddress.address.startsWith('addr')).toBe(true); + expect(externalAddress.address.startsWith('addr_test')).toBe(false); + expect(externalAddress.rewardAccount.startsWith('stake')).toBe(true); + expect(externalAddress.rewardAccount.startsWith('stake_test')).toBe(false); + }); + + it('resolves with GroupedAddress object', async () => { + const stakeKeyDerivationIndex = 1; + const paymentKeyDerivationPath = { index: 0, type: AddressType.Internal }; + const internalAddress = await testnetAccount.deriveAddress(paymentKeyDerivationPath, stakeKeyDerivationIndex); + expect(internalAddress.address.startsWith('addr_test')).toBe(true); + expect(internalAddress.accountIndex).toBe(accountIndex); + expect(internalAddress.index).toBe(paymentKeyDerivationPath.index); + expect(internalAddress.type).toBe(paymentKeyDerivationPath.type); + expect(internalAddress.networkId).toBe(testnetChainId.networkId); + expect(internalAddress.rewardAccount.startsWith('stake_test')).toBe(true); + expect(internalAddress.stakeKeyDerivationPath).toEqual({ index: stakeKeyDerivationIndex, role: 2 }); + }); + + it('derives the address with stake key of the given index', async () => { + const keyMap = new Map([ + [0, '0000000000000000000000000000000000000000000000000000000000000000'], + [1, '1111111111111111111111111111111111111111111111111111111111111111'], + [2, '2222222222222222222222222222222222222222222222222222222222222222'], + [3, '3333333333333333333333333333333333333333333333333333333333333333'], + [4, '4444444444444444444444444444444444444444444444444444444444444444'] + ]); + + testnetAccount.derivePublicKey = jest.fn((x: AccountKeyDerivationPath) => + Promise.resolve(Crypto.Ed25519PublicKey.fromHex(Crypto.Ed25519PublicKeyHex(keyMap.get(x.index)!))) + ); + + const index = 0; + const type = AddressType.External; + const addresses = [ + await testnetAccount.deriveAddress({ index, type }, 0), + await testnetAccount.deriveAddress({ index, type }, 1), + await testnetAccount.deriveAddress({ index, type }, 2), + await testnetAccount.deriveAddress({ index, type }, 3) + ]; + + expect(addresses[0].stakeKeyDerivationPath).toEqual({ index: 0, role: KeyRole.Stake }); + expect(addresses[0].rewardAccount).toEqual('stake_test1uruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhc8cxn3h'); + expect(addresses[0].address).toEqual( + 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhlemj3p5myzdmy2edx089wtcfp4rymmlejkpvng82utg90s4cadlm' + ); + + expect(addresses[1].stakeKeyDerivationPath).toEqual({ index: 1, role: KeyRole.Stake }); + expect(addresses[1].rewardAccount).toEqual('stake_test1uzx0qqs06evy77cnpk6u5q3fc50exjpp5t4s0swl2ykc4jsmh8tej'); + expect(addresses[1].address).toEqual( + 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhuv7qpql4jcfaa3xrd4egpzn3gljdyzrghtqlqa75fd3t9qqnvgeq' + ); + + expect(addresses[2].stakeKeyDerivationPath).toEqual({ index: 2, role: KeyRole.Stake }); + expect(addresses[2].rewardAccount).toEqual('stake_test1uqcnxxxatdgmqdmz0rhg72kn3n0egek5s0nqcvfy9ztyltc9cpuz4'); + expect(addresses[2].address).toEqual( + 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhe3xvvd6k63kqmky78w3u4d8rxlj3ndfqlxpscjg2ykf7hs8qc48l' + ); + + expect(addresses[3].stakeKeyDerivationPath).toEqual({ index: 3, role: KeyRole.Stake }); + expect(addresses[3].rewardAccount).toEqual('stake_test1urj8hvwxxz0t6pnfttj9ne5leu74shjlg83a8kxww9ft2fqtdhssu'); + expect(addresses[3].address).toEqual( + 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhly0wcuvvy7h5rxjkhyt8nflneatp097s0r60vvuu2jk5jq73efq0' + ); + }); + }); +}); diff --git a/packages/key-management/test/InMemoryKeyAgent.test.ts b/packages/key-management/test/InMemoryKeyAgent.test.ts index bfa0f88a76a..a965d1b1b91 100644 --- a/packages/key-management/test/InMemoryKeyAgent.test.ts +++ b/packages/key-management/test/InMemoryKeyAgent.test.ts @@ -1,5 +1,5 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, InMemoryKeyAgent, KeyRole, SerializableInMemoryKeyAgentData, util } from '../src'; +import { AddressType, GroupedAddress, InMemoryKeyAgent, KeyRole, SerializableInMemoryKeyAgentData, util } from '../src'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; import { dummyLogger } from 'ts-log'; @@ -10,21 +10,19 @@ const { ownSignatureKeyPaths } = jest.requireMock('../src/util/ownSignatureKeyPa describe('InMemoryKeyAgent', () => { let keyAgent: InMemoryKeyAgent; let getPassphrase: jest.Mock; - let inputResolver: jest.Mocked; let mnemonicWords: string[]; const bip32Ed25519 = new Crypto.SodiumBip32Ed25519(); beforeEach(async () => { mnemonicWords = util.generateMnemonicWords(); getPassphrase = jest.fn().mockResolvedValue(Buffer.from('password')); - inputResolver = { resolveInput: jest.fn() }; keyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( { chainId: Cardano.ChainIds.Preview, getPassphrase, mnemonicWords }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); }); @@ -50,7 +48,7 @@ describe('InMemoryKeyAgent', () => { mnemonic2ndFactorPassphrase: 'passphrase', mnemonicWords }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); expect(await saferKeyAgent.exportRootPrivateKey()).not.toEqual(await keyAgent.exportRootPrivateKey()); }); @@ -66,7 +64,6 @@ describe('InMemoryKeyAgent', () => { expect(typeof serializableData.__typename).toBe('string'); expect(typeof serializableData.accountIndex).toBe('number'); expect(typeof serializableData.chainId).toBe('object'); - expect(Array.isArray(serializableData.knownAddresses)).toBe(true); expect(serializableData.encryptedRootPrivateKeyBytes.length > 0).toBe(true); }); @@ -91,11 +88,19 @@ describe('InMemoryKeyAgent', () => { { index: 0, role: 2 } ]); const body = {} as unknown as Cardano.HydratedTxBody; - const witnessSet = await keyAgent.signTransaction({ - body, - hash: Cardano.TransactionId('8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec') - }); - expect(ownSignatureKeyPaths).toBeCalledWith(body, keyAgent.knownAddresses, inputResolver, expect.anything()); + const knownAddresses: GroupedAddress[] = []; + const txInKeyPathMap = {}; + const witnessSet = await keyAgent.signTransaction( + { + body, + hash: Cardano.TransactionId('8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec') + }, + { + knownAddresses, + txInKeyPathMap + } + ); + expect(ownSignatureKeyPaths).toBeCalledWith(body, knownAddresses, txInKeyPathMap, expect.anything()); expect(witnessSet.size).toBe(2); expect(typeof [...witnessSet.values()][0]).toBe('string'); }); @@ -156,10 +161,9 @@ describe('InMemoryKeyAgent', () => { rootPrivateKey: Crypto.Bip32PrivateKeyHex(yoroiRootPrivateKeyHex) }) ), - getPassphrase, - knownAddresses: [] + getPassphrase }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); const exportedPrivateKeyHex = await keyAgentFromEncryptedKey.exportRootPrivateKey(); expect(exportedPrivateKeyHex).toEqual(yoroiRootPrivateKeyHex); @@ -172,7 +176,7 @@ describe('InMemoryKeyAgent', () => { getPassphrase, mnemonicWords: yoroiMnemonic }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); const exportedPrivateKeyHex = await keyAgentFromMnemonic.exportRootPrivateKey(); expect(exportedPrivateKeyHex).toEqual(yoroiRootPrivateKeyHex); @@ -190,14 +194,17 @@ describe('InMemoryKeyAgent', () => { getPassphrase, mnemonicWords: michaelMnemonic }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); - ownSignatureKeyPaths.mockResolvedValue([{ index: 0, type: KeyRole.External }]); - const signature = await keyAgentFromMnemonic.signTransaction({ - body: { fee: 10n, inputs: [], outputs: [], validityInterval: {} }, - hash: Cardano.TransactionId('0000000000000000000000000000000000000000000000000000000000000000') - }); + ownSignatureKeyPaths.mockReturnValue([{ index: 0, type: KeyRole.External }]); + const signature = await keyAgentFromMnemonic.signTransaction( + { + body: { fee: 10n, inputs: [], outputs: [], validityInterval: {} }, + hash: Cardano.TransactionId('0000000000000000000000000000000000000000000000000000000000000000') + }, + { knownAddresses: [], txInKeyPathMap: {} } + ); expect( signature.has(Crypto.Ed25519PublicKeyHex('0b1c96fad4179d7910bd9485ac28c4c11368c83d18d01b29d4cf84d8ff6a06c4')) ).toBe(true); @@ -252,10 +259,9 @@ describe('InMemoryKeyAgent', () => { }) ), // daedelus enforces min length of 10 - getPassphrase: jest.fn().mockResolvedValue(Buffer.from('nMmys*X002')), - knownAddresses: [] + getPassphrase: jest.fn().mockResolvedValue(Buffer.from('nMmys*X002')) }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); const derivedAddress = await keyAgentFromEncryptedKey.deriveAddress( { @@ -274,7 +280,7 @@ describe('InMemoryKeyAgent', () => { getPassphrase, mnemonicWords: daedelusMnemonic24 }, - { bip32Ed25519, inputResolver, logger: dummyLogger } + { bip32Ed25519, logger: dummyLogger } ); const derivedAddress = await keyAgentFromMnemonic.deriveAddress( { diff --git a/packages/key-management/test/KeyAgentBase.test.ts b/packages/key-management/test/KeyAgentBase.test.ts index 279ca3304fb..b30ac3dfc9f 100644 --- a/packages/key-management/test/KeyAgentBase.test.ts +++ b/packages/key-management/test/KeyAgentBase.test.ts @@ -1,13 +1,6 @@ /* eslint-disable sonarjs/no-duplicate-string */ import * as Crypto from '@cardano-sdk/crypto'; -import { - AccountKeyDerivationPath, - AddressType, - KeyAgentBase, - KeyAgentType, - KeyRole, - SerializableInMemoryKeyAgentData -} from '../src'; +import { AddressType, KeyAgentBase, KeyAgentType, KeyRole, SerializableInMemoryKeyAgentData } from '../src'; import { Cardano } from '@cardano-sdk/core'; import { dummyLogger } from 'ts-log'; @@ -18,7 +11,6 @@ class MockKeyAgent extends KeyAgentBase { constructor(data: SerializableInMemoryKeyAgentData) { super(data, { bip32Ed25519, - inputResolver: { resolveInput: () => Promise.resolve(null) }, logger: dummyLogger }); } @@ -43,20 +35,14 @@ describe('KeyAgentBase', () => { extendedAccountPublicKey: Crypto.Bip32PublicKeyHex( // eslint-disable-next-line max-len 'fc5ab25e830b67c47d0a17411bf7fdabf711a597fb6cf04102734b0a2934ceaaa65ff5e7c52498d52c07b8ddfcd436fc2b4d2775e2984a49d0c79f65ceee4779' - ), - knownAddresses: [] + ) }); }); // eslint-disable-next-line max-len - // extpubkey: return '781ad7d97e043e3790e6a94111e2e65b5a5e584a3e542f4655f7794f80d2a081ee571e58e8982b6a549d9090df6d86b6bb2afc69a226eee44ac7f3e3e1da9a14'; - - test('deriveAddress either derives a new address, or returns existing with matching type and index', async () => { - const paymentKey = 'b524f4627318819891efe52da641e05604168e508c3cc9f3e13945f21b69afa0'; - const stakeKey = '6a27d881ef58bd3816f60c05a5fbe872726e76fc239985fde9dcb9a8d7e582e8'; - keyAgent.derivePublicKey = jest.fn().mockResolvedValueOnce(paymentKey).mockResolvedValueOnce(stakeKey); - const initialAddresses = keyAgent.knownAddresses; + // extpubkey: return ''; + test('deriveAddress derives a new address', async () => { const index = 1; const type = AddressType.External; const address = await keyAgent.deriveAddress({ index, type }, 0); @@ -67,60 +53,6 @@ describe('KeyAgentBase', () => { expect(address.networkId).toBe(Cardano.ChainIds.Preview.networkId); expect(address.address.startsWith('addr_test')).toBe(true); expect(address.rewardAccount.startsWith('stake_test')).toBe(true); - expect(keyAgent.knownAddresses).toHaveLength(1); - // creates a new array obj - expect(keyAgent.knownAddresses).not.toBe(initialAddresses); - - const sameAddress = await keyAgent.deriveAddress({ index, type }, 0); - expect(sameAddress.address).toEqual(address.address); - expect(keyAgent.knownAddresses.length).toEqual(1); - }); - - test('deriveAddress derives the address with stake key of the given index', async () => { - const keyMap = new Map([ - [0, '0000000000000000000000000000000000000000000000000000000000000000'], - [1, '1111111111111111111111111111111111111111111111111111111111111111'], - [2, '2222222222222222222222222222222222222222222222222222222222222222'], - [3, '3333333333333333333333333333333333333333333333333333333333333333'], - [4, '4444444444444444444444444444444444444444444444444444444444444444'] - ]); - - keyAgent.derivePublicKey = jest.fn((x: AccountKeyDerivationPath) => - Promise.resolve(Crypto.Ed25519PublicKeyHex(keyMap.get(x.index)!)) - ); - - const index = 0; - const type = AddressType.External; - const addresses = [ - await keyAgent.deriveAddress({ index, type }, 0), - await keyAgent.deriveAddress({ index, type }, 1), - await keyAgent.deriveAddress({ index, type }, 2), - await keyAgent.deriveAddress({ index, type }, 3) - ]; - - expect(addresses[0].stakeKeyDerivationPath).toEqual({ index: 0, role: KeyRole.Stake }); - expect(addresses[0].rewardAccount).toEqual('stake_test1uruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhc8cxn3h'); - expect(addresses[0].address).toEqual( - 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhlemj3p5myzdmy2edx089wtcfp4rymmlejkpvng82utg90s4cadlm' - ); - - expect(addresses[1].stakeKeyDerivationPath).toEqual({ index: 1, role: KeyRole.Stake }); - expect(addresses[1].rewardAccount).toEqual('stake_test1uzx0qqs06evy77cnpk6u5q3fc50exjpp5t4s0swl2ykc4jsmh8tej'); - expect(addresses[1].address).toEqual( - 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhuv7qpql4jcfaa3xrd4egpzn3gljdyzrghtqlqa75fd3t9qqnvgeq' - ); - - expect(addresses[2].stakeKeyDerivationPath).toEqual({ index: 2, role: KeyRole.Stake }); - expect(addresses[2].rewardAccount).toEqual('stake_test1uqcnxxxatdgmqdmz0rhg72kn3n0egek5s0nqcvfy9ztyltc9cpuz4'); - expect(addresses[2].address).toEqual( - 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhe3xvvd6k63kqmky78w3u4d8rxlj3ndfqlxpscjg2ykf7hs8qc48l' - ); - - expect(addresses[3].stakeKeyDerivationPath).toEqual({ index: 3, role: KeyRole.Stake }); - expect(addresses[3].rewardAccount).toEqual('stake_test1urj8hvwxxz0t6pnfttj9ne5leu74shjlg83a8kxww9ft2fqtdhssu'); - expect(addresses[3].address).toEqual( - 'addr_test1qruaegs6djpxaj9vkn8njh9uys63jdaluetqkf5r4w95zhly0wcuvvy7h5rxjkhyt8nflneatp097s0r60vvuu2jk5jq73efq0' - ); }); test('derivePublicKey', async () => { diff --git a/packages/key-management/test/cip8/cip30signData.test.ts b/packages/key-management/test/cip8/cip30signData.test.ts index ca049c6c04a..b5b3ab09199 100644 --- a/packages/key-management/test/cip8/cip30signData.test.ts +++ b/packages/key-management/test/cip8/cip30signData.test.ts @@ -24,13 +24,16 @@ describe('cip30signData', () => { beforeAll(async () => { const keyAgentReady = testKeyAgent(); keyAgent = await keyAgentReady; - asyncKeyAgent = await testAsyncKeyAgent(undefined, undefined, keyAgentReady); + asyncKeyAgent = await testAsyncKeyAgent(undefined, keyAgentReady); address = await asyncKeyAgent.deriveAddress(addressDerivationPath, 0); }); - const signAndDecode = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount) => { + const signAndDecode = async ( + signWith: Cardano.PaymentAddress | Cardano.RewardAccount, + knownAddresses: GroupedAddress[] + ) => { const dataSignature = await cip8.cip30signData({ - addressManager: KeyManagementUtil.createBip32Ed25519AddressManager(asyncKeyAgent), + knownAddresses, payload: HexBlob('abc123'), signWith, witnesser: KeyManagementUtil.createBip32Ed25519Witnesser(asyncKeyAgent) @@ -60,7 +63,7 @@ describe('cip30signData', () => { it('supports sign with payment address', async () => { const signWith = address.address; - const { signedData, publicKeyHex } = await signAndDecode(signWith); + const { signedData, publicKeyHex } = await signAndDecode(signWith, [address]); testAddressHeader(signedData, signWith); @@ -74,7 +77,7 @@ describe('cip30signData', () => { it('supports signing with reward account', async () => { const signWith = address.rewardAccount; - const { signedData, publicKeyHex } = await signAndDecode(signWith); + const { signedData, publicKeyHex } = await signAndDecode(signWith, [address]); testAddressHeader(signedData, signWith); @@ -88,7 +91,7 @@ describe('cip30signData', () => { it('signature can be verified', async () => { const signWith = address.address; - const { coseSign1, publicKeyHex, signedData } = await signAndDecode(signWith); + const { coseSign1, publicKeyHex, signedData } = await signAndDecode(signWith, [address]); const signedDataBytes = HexBlob.fromBytes(signedData.to_bytes()); const signatureBytes = HexBlob.fromBytes(coseSign1.signature()) as unknown as Crypto.Ed25519SignatureHex; expect( diff --git a/packages/key-management/test/mocks/mockKeyAgentDependencies.ts b/packages/key-management/test/mocks/mockKeyAgentDependencies.ts index 81b9ee603d0..53debfddb06 100644 --- a/packages/key-management/test/mocks/mockKeyAgentDependencies.ts +++ b/packages/key-management/test/mocks/mockKeyAgentDependencies.ts @@ -4,8 +4,5 @@ import { dummyLogger } from 'ts-log'; export const mockKeyAgentDependencies = (): jest.Mocked => ({ bip32Ed25519: new SodiumBip32Ed25519(), - inputResolver: { - resolveInput: jest.fn().mockResolvedValue(null) - }, logger: dummyLogger }); diff --git a/packages/key-management/test/mocks/testKeyAgent.ts b/packages/key-management/test/mocks/testKeyAgent.ts index 09fde978f1a..b2f7ba00741 100644 --- a/packages/key-management/test/mocks/testKeyAgent.ts +++ b/packages/key-management/test/mocks/testKeyAgent.ts @@ -1,14 +1,11 @@ import { Cardano } from '@cardano-sdk/core'; -import { GroupedAddress, InMemoryKeyAgent, KeyAgentDependencies, util } from '../../src'; +import { InMemoryKeyAgent, KeyAgentDependencies, util } from '../../src'; import { mockKeyAgentDependencies } from './mockKeyAgentDependencies'; export const getPassphrase = jest.fn(async () => Buffer.from('password')); -export const testKeyAgent = async ( - addresses?: GroupedAddress[], - dependencies: KeyAgentDependencies | undefined = mockKeyAgentDependencies() -) => { - const keyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( +export const testKeyAgent = async (dependencies: KeyAgentDependencies | undefined = mockKeyAgentDependencies()) => + InMemoryKeyAgent.fromBip39MnemonicWords( { chainId: Cardano.ChainIds.Preview, getPassphrase, @@ -16,15 +13,9 @@ export const testKeyAgent = async ( }, dependencies ); - if (addresses) { - keyAgent.knownAddresses.push(...addresses); - } - return keyAgent; -}; export const testAsyncKeyAgent = async ( - addresses?: GroupedAddress[], dependencies: KeyAgentDependencies | undefined = mockKeyAgentDependencies(), - keyAgentReady = testKeyAgent(addresses, dependencies), + keyAgentReady = testKeyAgent(dependencies), shutdownSpy?: () => void ) => util.createAsyncKeyAgent(await keyAgentReady, shutdownSpy); diff --git a/packages/key-management/test/util/createAddressManager.test.ts b/packages/key-management/test/util/createAddressManager.test.ts deleted file mode 100644 index 1e8b2ae32e8..00000000000 --- a/packages/key-management/test/util/createAddressManager.test.ts +++ /dev/null @@ -1,50 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { AsyncKeyAgent, InMemoryKeyAgent, KeyRole, util } from '../../src'; -import { Cardano } from '@cardano-sdk/core'; -import { dummyLogger } from 'ts-log'; - -describe('createBip32Ed25519AddressManager', () => { - let asyncKeyAgent: AsyncKeyAgent; - let addressManager: util.Bip32Ed25519AddressManager; - let inputResolver: jest.Mocked; - const addressDerivationPath = { index: 0, type: 0 }; - - beforeEach(async () => { - const mnemonicWords = util.generateMnemonicWords(); - const getPassphrase = jest.fn().mockResolvedValue(Buffer.from('password')); - inputResolver = { resolveInput: jest.fn() }; - const keyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( - { - chainId: Cardano.ChainIds.Preview, - getPassphrase, - mnemonicWords - }, - { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), inputResolver, logger: dummyLogger } - ); - asyncKeyAgent = util.createAsyncKeyAgent(keyAgent); - addressManager = util.createBip32Ed25519AddressManager(asyncKeyAgent); - }); - - it('deriveAddress is unchanged', async () => { - await expect(asyncKeyAgent.deriveAddress(addressDerivationPath, 0)).resolves.toEqual( - await addressManager.deriveAddress(addressDerivationPath, 0) - ); - }); - - it('derivePublicKey is unchanged', async () => { - await expect(asyncKeyAgent.derivePublicKey({ index: 0, role: KeyRole.External })).resolves.toEqual( - await addressManager.derivePublicKey({ index: 0, role: KeyRole.External }) - ); - }); - - it('stops emitting addresses$ after shutdown', (done) => { - addressManager.shutdown(); - addressManager.knownAddresses$.subscribe({ - complete: done, - next: () => { - throw new Error('Should not emit'); - } - }); - void addressManager.deriveAddress(addressDerivationPath, 0); - }); -}); diff --git a/packages/key-management/test/util/createAsyncKeyAgent.test.ts b/packages/key-management/test/util/createAsyncKeyAgent.test.ts deleted file mode 100644 index f4062411f44..00000000000 --- a/packages/key-management/test/util/createAsyncKeyAgent.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { AsyncKeyAgent, InMemoryKeyAgent, KeyAgent, util } from '../../src'; -import { Cardano } from '@cardano-sdk/core'; -import { HexBlob } from '@cardano-sdk/util'; -import { dummyLogger } from 'ts-log'; -import { firstValueFrom } from 'rxjs'; - -describe('createAsyncKeyAgent maps KeyAgent to AsyncKeyAgent', () => { - let keyAgent: KeyAgent; - let asyncKeyAgent: AsyncKeyAgent; - let inputResolver: jest.Mocked; - const addressDerivationPath = { index: 0, type: 0 }; - - beforeEach(async () => { - const mnemonicWords = util.generateMnemonicWords(); - const getPassphrase = jest.fn().mockResolvedValue(Buffer.from('password')); - inputResolver = { resolveInput: jest.fn() }; - keyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( - { - chainId: Cardano.ChainIds.Preview, - getPassphrase, - mnemonicWords - }, - { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), inputResolver, logger: dummyLogger } - ); - asyncKeyAgent = util.createAsyncKeyAgent(keyAgent); - }); - it('getChainId resolves with chainId', async () => { - expect(await asyncKeyAgent.getChainId()).toEqual(keyAgent.chainId); - }); - it('deriveAddress/signBlob/signTransaction are unchanged', async () => { - await expect(asyncKeyAgent.deriveAddress(addressDerivationPath, 0)).resolves.toEqual( - await keyAgent.deriveAddress(addressDerivationPath, 0) - ); - const keyDerivationPath = { index: 0, role: 0 }; - const blob = HexBlob('abc123'); - await expect(asyncKeyAgent.signBlob(keyDerivationPath, blob)).resolves.toEqual( - await keyAgent.signBlob(keyDerivationPath, blob) - ); - inputResolver.resolveInput.mockResolvedValue(null); - const txInternals = { - body: { fee: 20_000n, inputs: [], outputs: [], validityInterval: {} } as Cardano.HydratedTxBody, - hash: Cardano.TransactionId('8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec') - }; - await expect(asyncKeyAgent.signTransaction(txInternals)).resolves.toEqual( - await keyAgent.signTransaction(txInternals) - ); - }); - it('knownAddresses$ is emits initial addresses and after new address derivation', async () => { - await expect(firstValueFrom(asyncKeyAgent.knownAddresses$)).resolves.toEqual(keyAgent.knownAddresses); - await asyncKeyAgent.deriveAddress(addressDerivationPath, 0); - await expect(firstValueFrom(asyncKeyAgent.knownAddresses$)).resolves.toEqual(keyAgent.knownAddresses); - }); - it('stops emitting addresses$ after shutdown', (done) => { - asyncKeyAgent.shutdown(); - asyncKeyAgent.knownAddresses$.subscribe({ - complete: done, - next: () => { - throw new Error('Should not emit'); - } - }); - void asyncKeyAgent.deriveAddress(addressDerivationPath, 0); - }); -}); diff --git a/packages/key-management/test/util/createWitnesser.test.ts b/packages/key-management/test/util/createWitnesser.test.ts index 62e1dc1fdfe..e214c60450b 100644 --- a/packages/key-management/test/util/createWitnesser.test.ts +++ b/packages/key-management/test/util/createWitnesser.test.ts @@ -1,51 +1,37 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { AsyncKeyAgent, InMemoryKeyAgent, Witnesser, util } from '../../src'; +import { AsyncKeyAgent, SignBlobResult, Witnesser, util } from '../../src'; import { Cardano } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; -import { dummyLogger } from 'ts-log'; describe('createBip32Ed25519Witnesser', () => { - let asyncKeyAgent: AsyncKeyAgent; + let asyncKeyAgent: jest.Mocked; let witnesser: Witnesser; - let inputResolver: jest.Mocked; beforeEach(async () => { - const mnemonicWords = util.generateMnemonicWords(); - const getPassphrase = jest.fn().mockResolvedValue(Buffer.from('password')); - inputResolver = { resolveInput: jest.fn() }; - const keyAgent = await InMemoryKeyAgent.fromBip39MnemonicWords( - { - chainId: Cardano.ChainIds.Preview, - getPassphrase, - mnemonicWords - }, - { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), inputResolver, logger: dummyLogger } - ); - asyncKeyAgent = util.createAsyncKeyAgent(keyAgent); + asyncKeyAgent = { + signBlob: jest.fn(), + signTransaction: jest.fn() + } as unknown as jest.Mocked; witnesser = util.createBip32Ed25519Witnesser(asyncKeyAgent); }); it('signBlob is unchanged', async () => { const keyDerivationPath = { index: 0, role: 0 }; const blob = HexBlob('abc123'); - - await expect(asyncKeyAgent.signBlob(keyDerivationPath, blob)).resolves.toEqual( - await witnesser.signBlob(keyDerivationPath, blob) - ); + const result = {} as SignBlobResult; + asyncKeyAgent.signBlob.mockResolvedValueOnce(result); + await expect(witnesser.signBlob(keyDerivationPath, blob)).resolves.toBe(result); + expect(asyncKeyAgent.signBlob).toBeCalledWith(keyDerivationPath, blob); }); it('signTransaction is unchanged', async () => { - inputResolver.resolveInput.mockResolvedValue(null); - const txInternals = { body: { fee: 20_000n, inputs: [], outputs: [], validityInterval: {} } as Cardano.HydratedTxBody, hash: Cardano.TransactionId('8561258e210352fba2ac0488afed67b3427a27ccf1d41ec030c98a8199bc22ec') }; - - await expect(asyncKeyAgent.signTransaction(txInternals)).resolves.toEqual( - ( - await witnesser.witness(txInternals) - ).signatures - ); + const options = { knownAddresses: [], txInKeyPathMap: {} }; + const result = {} as Cardano.Signatures; + asyncKeyAgent.signTransaction.mockResolvedValueOnce(result); + await expect(witnesser.witness(txInternals, options)).resolves.toEqual({ signatures: result }); + expect(asyncKeyAgent.signTransaction).toBeCalledWith(txInternals, options, void 0); }); }); diff --git a/packages/key-management/test/util/ensureStakeKeys.test.ts b/packages/key-management/test/util/ensureStakeKeys.test.ts index 0d45bb10c2d..7d74f8b8c57 100644 --- a/packages/key-management/test/util/ensureStakeKeys.test.ts +++ b/packages/key-management/test/util/ensureStakeKeys.test.ts @@ -1,116 +1,135 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, AsyncKeyAgent, InMemoryKeyAgent, util } from '../../src'; +import { AddressType, Bip32Account, util } from '../../src'; +import { Bip32PublicKeyHex } from '@cardano-sdk/crypto'; import { Cardano } from '@cardano-sdk/core'; import { Logger, dummyLogger } from 'ts-log'; -import { firstValueFrom } from 'rxjs'; describe('ensureStakeKeys', () => { - let keyAgent: AsyncKeyAgent; + let bip32Account: Bip32Account; let logger: Logger; beforeEach(async () => { logger = dummyLogger; - const mnemonicWords = util.generateMnemonicWords(); - const getPassphrase = jest.fn().mockResolvedValue(Buffer.from('password')); - const inputResolver = { resolveInput: jest.fn() }; - keyAgent = util.createAsyncKeyAgent( - await InMemoryKeyAgent.fromBip39MnemonicWords( - { - chainId: Cardano.ChainIds.Preview, - getPassphrase, - mnemonicWords - }, - { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), inputResolver, logger: dummyLogger } + bip32Account = new Bip32Account({ + accountIndex: 0, + chainId: Cardano.ChainIds.Preview, + extendedAccountPublicKey: Bip32PublicKeyHex( + 'fc5ab25e830b67c47d0a17411bf7fdabf711a597fb6cf04102734b0a2934ceaaa65ff5e7c52498d52c07b8ddfcd436fc2b4d2775e2984a49d0c79f65ceee4779' ) - ); + }); }); it('can derive one stake key', async () => { - const newRewardAccounts = await util.ensureStakeKeys({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), + const { rewardAccounts, newAddresses } = await util.ensureStakeKeys({ + bip32Account, count: 1, + knownAddresses: [], logger }); - const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$); - expect(knownAddresses.length).toBe(1); - expect(knownAddresses.map(({ rewardAccount }) => rewardAccount)).toEqual(newRewardAccounts); + expect(newAddresses).toHaveLength(1); + expect(rewardAccounts).toHaveLength(1); }); it('does not create more stake keys when sufficient exist', async () => { - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 1); + const knownAddresses = [ + await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 0), + await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 1) + ]; - await util.ensureStakeKeys({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), + const { newAddresses, rewardAccounts } = await util.ensureStakeKeys({ + bip32Account, count: 2, + knownAddresses, logger }); - const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$); - expect(knownAddresses.length).toBe(2); + expect(newAddresses).toHaveLength(0); + expect(rewardAccounts).toHaveLength(2); }); it('derives new stake keys filling any existing gaps', async () => { - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 2); + const knownAddresses = [ + await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 0), + await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 2) + ]; - await util.ensureStakeKeys({ addressManager: util.createBip32Ed25519AddressManager(keyAgent), count: 4, logger }); + const { newAddresses, rewardAccounts } = await util.ensureStakeKeys({ + bip32Account, + count: 4, + knownAddresses, + logger + }); - const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$); - const stakeKeyIndices = knownAddresses.map(({ stakeKeyDerivationPath }) => stakeKeyDerivationPath?.index).sort(); - expect(stakeKeyIndices).toEqual([0, 1, 2, 3]); + const stakeKeyIndices = newAddresses.map(({ stakeKeyDerivationPath }) => stakeKeyDerivationPath?.index).sort(); + expect(stakeKeyIndices).toEqual([1, 3]); + expect(rewardAccounts).toHaveLength(4); }); it('generates new known addresses using the requested payment key index', async () => { - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 2); + const knownAddresses = [ + await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 0), + await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 2) + ]; - await util.ensureStakeKeys({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), + const { newAddresses, rewardAccounts } = await util.ensureStakeKeys({ + bip32Account, count: 4, + knownAddresses, logger, paymentKeyIndex: 1 }); - const stakeKeyIndicesPaymentKey1 = (await firstValueFrom(keyAgent.knownAddresses$)) - .filter(({ index }) => index === 1) - .map(({ stakeKeyDerivationPath }) => stakeKeyDerivationPath?.index); - expect(stakeKeyIndicesPaymentKey1).toEqual(expect.arrayContaining([1, 3])); + expect(newAddresses).toHaveLength(2); + expect(newAddresses.every((acc) => acc.index === 1)).toBe(true); + const newStakeKeyIndices = newAddresses.map(({ stakeKeyDerivationPath }) => stakeKeyDerivationPath?.index); + expect(newStakeKeyIndices).toEqual([1, 3]); + expect(rewardAccounts).toHaveLength(4); }); it('can handle request of 0 stake keys', async () => { - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - await util.ensureStakeKeys({ addressManager: util.createBip32Ed25519AddressManager(keyAgent), count: 0, logger }); + const knownAddresses = [await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 0)]; + const { newAddresses, rewardAccounts } = await util.ensureStakeKeys({ + bip32Account, + count: 0, + knownAddresses, + logger + }); - const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$); - expect(knownAddresses.length).toBe(1); + expect(newAddresses).toHaveLength(0); + expect(rewardAccounts).toHaveLength(1); }); it('returns all reward accounts', async () => { - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - await expect( - util.ensureStakeKeys({ addressManager: util.createBip32Ed25519AddressManager(keyAgent), count: 2, logger }) - ).resolves.toHaveLength(2); + const knownAddresses = [await bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 0)]; + const { rewardAccounts } = await util.ensureStakeKeys({ bip32Account, count: 2, knownAddresses, logger }); + expect(rewardAccounts).toHaveLength(2); }); it('takes into account addresses with multiple stake keys and payment keys', async () => { - await Promise.all([ - keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0), - keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 3), - keyAgent.deriveAddress({ index: 1, type: AddressType.External }, 0), - keyAgent.deriveAddress({ index: 1, type: AddressType.External }, 2) + const knownAddresses = await Promise.all([ + bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 0), + bip32Account.deriveAddress({ index: 0, type: AddressType.External }, 3), + bip32Account.deriveAddress({ index: 1, type: AddressType.External }, 0), + bip32Account.deriveAddress({ index: 1, type: AddressType.External }, 2) ]); - await util.ensureStakeKeys({ addressManager: util.createBip32Ed25519AddressManager(keyAgent), count: 5, logger }); - const knownAddresses = await firstValueFrom(keyAgent.knownAddresses$); - expect(knownAddresses.length).toBe(6); + const { newAddresses, rewardAccounts } = await util.ensureStakeKeys({ + bip32Account, + count: 5, + knownAddresses, + logger + }); + expect(newAddresses).toHaveLength(2); + expect( + newAddresses.every(({ address: newAddress }) => !knownAddresses.some(({ address }) => address === newAddress)) + ).toBe(true); + expect(rewardAccounts).toHaveLength(5); - const stakeKeyIndicesPaymentKey0 = knownAddresses + const stakeKeyIndicesPaymentKey0 = [...knownAddresses, ...newAddresses] .filter(({ index }) => index === 0) .map(({ stakeKeyDerivationPath }) => stakeKeyDerivationPath?.index); expect(stakeKeyIndicesPaymentKey0).toEqual(expect.arrayContaining([0, 1, 3, 4])); - const stakeKeyIndicesPaymentKey1 = knownAddresses + const stakeKeyIndicesPaymentKey1 = [...knownAddresses, ...newAddresses] .filter(({ index }) => index === 1) .map(({ stakeKeyDerivationPath }) => stakeKeyDerivationPath?.index); expect(stakeKeyIndicesPaymentKey1).toEqual(expect.arrayContaining([0, 2])); diff --git a/packages/key-management/test/util/ownSignaturePaths.test.ts b/packages/key-management/test/util/ownSignaturePaths.test.ts index 9a73758edf3..669c9b33a58 100644 --- a/packages/key-management/test/util/ownSignaturePaths.test.ts +++ b/packages/key-management/test/util/ownSignaturePaths.test.ts @@ -1,14 +1,15 @@ /* eslint-disable sonarjs/no-duplicate-string */ import * as Crypto from '@cardano-sdk/crypto'; -import { AccountKeyDerivationPath, AddressType, GroupedAddress, KeyRole, util } from '../../src'; +import { AccountKeyDerivationPath, AddressType, GroupedAddress, KeyRole, TxInId, util } from '../../src'; import { Cardano } from '@cardano-sdk/core'; -import { txOut } from '../../../tx-construction/test/testData'; export const stakeKeyPath = { index: 0, role: KeyRole.Stake }; +const txId = (seed: number) => Cardano.TransactionId(Array.from({ length: 64 + 1 }).join(seed.toString())); + const toStakeCredential = (stakeKeyHash: Crypto.Ed25519KeyHashHex): Cardano.Credential => ({ hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(stakeKeyHash), type: Cardano.CredentialType.KeyHash @@ -64,18 +65,23 @@ describe('KeyManagement.util.ownSignaturePaths', () => { }); it('returns distinct derivation paths required to sign the transaction', async () => { - const txBody = { - inputs: [{}, {}, {}] - } as Cardano.TxBody; + const inputs: Cardano.TxIn[] = [ + { index: 0, txId: txId(0) }, + { index: 1, txId: txId(0) }, + { index: 2, txId: txId(1) } + ]; + const txBody = { inputs } as Cardano.TxBody; const knownAddresses = [address1, address2].map((address, index) => createGroupedAddress(address, ownRewardAccount, AddressType.External, index, stakeKeyPath) ); - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce({ ...txOut, address: address2 }) - .mockReturnValueOnce({ ...txOut, address: address1 }); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ + + expect( + util.ownSignatureKeyPaths(txBody, knownAddresses, { + [TxInId(inputs[0])]: { index: knownAddresses[0].index, role: Number(knownAddresses[0].type) }, + [TxInId(inputs[1])]: { index: knownAddresses[1].index, role: Number(knownAddresses[1].type) }, + [TxInId(inputs[2])]: { index: knownAddresses[0].index, role: Number(knownAddresses[0].type) } + }) + ).toEqual([ { index: 0, role: KeyRole.External @@ -95,16 +101,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { certificates: [{ __typename: Cardano.CertificateType.StakeRegistration, stakeCredential: ownStakeCredential }], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - } - ]); + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([]); } ); @@ -121,15 +119,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -148,15 +139,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -173,15 +157,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { certificates: [{ __typename: Cardano.CertificateType.StakeDelegation, stakeCredential: ownStakeCredential }], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -199,15 +176,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -229,15 +199,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -276,15 +239,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -304,15 +260,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -379,16 +328,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { } ] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput }, dRepKeyHash)).toEqual([ - { - index: 0, - role: KeyRole.External - } - ]); + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, dRepKeyHash)).toEqual([]); }); it('signs withdrawals for own reward account', async () => { @@ -397,12 +338,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => { withdrawals: [{ quantity: 1n, stakeAddress: ownRewardAccount }] } as Cardano.TxBody; const knownAddresses = [createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath)]; - const resolveInput = jest.fn().mockReturnValue({ ...txOut, address: address1 }); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - }, + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -416,13 +352,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => { withdrawals: [{ quantity: 1n, stakeAddress: otherRewardAccount }] } as Cardano.TxBody; const knownAddresses = [createGroupedAddress(address1, ownRewardAccount, AddressType.External, 0, stakeKeyPath)]; - const resolveInput = jest.fn().mockReturnValue({ ...txOut, address: address1 }); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ - { - index: 0, - role: KeyRole.External - } - ]); + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([]); }); it('returns the derivation path of a known payment credential key hash present in the requiredSigners field', async () => { @@ -439,8 +369,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => { createGroupedAddress(paymentAddress, ownRewardAccount, AddressType.External, 100, stakeKeyPath) ]; - const resolveInput = jest.fn().mockReturnValueOnce(null); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([ { index: 100, role: KeyRole.External @@ -457,9 +386,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { requiredExtraSignatures: [stakeKeyHash] } as Cardano.TxBody; const knownAddresses = [createGroupedAddress(address1, rewardAccount, AddressType.External, 0, stakeKeyPath)]; - const resolveInput = jest.fn().mockReturnValue(null); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([ { index: 0, role: KeyRole.Stake @@ -514,12 +442,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ - { index: 0, role: KeyRole.External }, + + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([ { index: 0, role: KeyRole.Stake }, { index: 1, role: KeyRole.Stake }, { index: 2, role: KeyRole.Stake }, @@ -543,14 +467,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ - { index: 0, role: KeyRole.External }, - { index: 0, role: KeyRole.Stake } - ]); + + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([{ index: 0, role: KeyRole.Stake }]); }); it('is returned for StakePool voter in voting procedures', async () => { @@ -572,14 +490,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, knownAddresses, { resolveInput })).toEqual([ - { index: 0, role: KeyRole.External }, - { index: 3, role: KeyRole.Stake } - ]); + expect(util.ownSignatureKeyPaths(txBody, knownAddresses, {})).toEqual([{ index: 3, role: KeyRole.Stake }]); }); }); @@ -603,12 +514,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput }, dRepKeyHash)).toEqual([ - { index: 0, role: KeyRole.External }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, dRepKeyHash)).toEqual([ { index: 0, role: KeyRole.DRep } ]); }); @@ -626,12 +533,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput }, dRepKeyHash)).toEqual([ - { index: 0, role: KeyRole.External }, + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, dRepKeyHash)).toEqual([ { index: 0, role: KeyRole.DRep } ]); }); @@ -650,13 +553,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ], inputs: [{}, {}, {}] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput }, dRepKeyHash)).toEqual([ - { index: 0, role: KeyRole.External } - ]); + + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, dRepKeyHash)).toEqual([]); }); it('is returned for DRep voter in voting procedures', async () => { @@ -675,12 +573,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => { ] } as Cardano.TxBody; - const resolveInput = jest - .fn() - .mockReturnValueOnce({ ...txOut, address: address1 }) - .mockReturnValueOnce(address1); - expect(await util.ownSignatureKeyPaths(txBody, [knownAddress1], { resolveInput }, dRepKeyHash)).toEqual([ - { index: 0, role: KeyRole.External }, + expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, dRepKeyHash)).toEqual([ { index: 0, role: KeyRole.DRep } ]); }); diff --git a/packages/key-management/test/util/stubSignTransaction.test.ts b/packages/key-management/test/util/stubSignTransaction.test.ts index d190f007abb..725024ec793 100644 --- a/packages/key-management/test/util/stubSignTransaction.test.ts +++ b/packages/key-management/test/util/stubSignTransaction.test.ts @@ -8,14 +8,30 @@ const { ownSignatureKeyPaths } = jest.requireMock('../../src/util/ownSignatureKe describe('KeyManagement.util.stubSignTransaction', () => { it('returns as many signatures as number of keys returned by ownSignaturePaths', async () => { - const inputResolver = {} as Cardano.InputResolver; // not called const txBody = {} as Cardano.HydratedTxBody; const knownAddresses = [{} as GroupedAddress]; const dRepPublicKey = Ed25519PublicKeyHex('0b1c96fad4179d7910bd9485ac28c4c11368c83d18d01b29d4cf84d8ff6a06c4'); const dRepKeyHash = (await Ed25519PublicKey.fromHex(dRepPublicKey).hash()).hex(); + const txInKeyPathMap = {}; ownSignatureKeyPaths.mockReturnValueOnce(['a']).mockReturnValueOnce(['a', 'b']); - expect((await util.stubSignTransaction({ dRepPublicKey, inputResolver, knownAddresses, txBody })).size).toBe(1); - expect((await util.stubSignTransaction({ dRepPublicKey, inputResolver, knownAddresses, txBody })).size).toBe(2); - expect(ownSignatureKeyPaths).toBeCalledWith(txBody, knownAddresses, inputResolver, dRepKeyHash); + expect( + ( + await util.stubSignTransaction({ + context: { knownAddresses, txInKeyPathMap }, + dRepPublicKey, + txBody + }) + ).size + ).toBe(1); + expect( + ( + await util.stubSignTransaction({ + context: { knownAddresses, txInKeyPathMap }, + dRepPublicKey, + txBody + }) + ).size + ).toBe(2); + expect(ownSignatureKeyPaths).toBeCalledWith(txBody, knownAddresses, txInKeyPathMap, dRepKeyHash); }); }); diff --git a/packages/tx-construction/src/tx-builder/TxBuilder.ts b/packages/tx-construction/src/tx-builder/TxBuilder.ts index 284de630f31..446acb70efb 100644 --- a/packages/tx-construction/src/tx-builder/TxBuilder.ts +++ b/packages/tx-construction/src/tx-builder/TxBuilder.ts @@ -29,11 +29,12 @@ import { RewardAccountWithPoolId } from '../types'; import { coldObservableProvider } from '@cardano-sdk/util-rxjs'; import { contextLogger, deepEquals } from '@cardano-sdk/util'; import { createOutputValidator } from '../output-validation'; -import { filter, firstValueFrom, lastValueFrom } from 'rxjs'; import { finalizeTx } from './finalizeTx'; import { initializeTx } from './initializeTx'; +import { lastValueFrom } from 'rxjs'; import minBy from 'lodash/minBy'; import omit from 'lodash/omit'; +import uniq from 'lodash/uniq'; type BuiltTx = { tx: Cardano.TxBodyWithHash; @@ -74,10 +75,14 @@ class LazyTxSigner implements UnsignedTx { async inspect(): Promise { const { tx, - ctx: { ownAddresses, auxiliaryData, handleResolutions }, + ctx: { + signingContext: { knownAddresses }, + auxiliaryData, + handleResolutions + }, inputSelection } = await this.#build(); - return { ...tx, auxiliaryData, handleResolutions, inputSelection, ownAddresses }; + return { ...tx, auxiliaryData, handleResolutions, inputSelection, ownAddresses: knownAddresses }; } async sign(): Promise { @@ -199,9 +204,7 @@ export class GenericTxBuilder implements TxBuilder { await this.#validateOutputs(); // Take a snapshot of returned properties, // so that they don't change while `initializeTx` is resolving - const ownAddresses = await firstValueFrom( - this.#dependencies.addressManager.knownAddresses$.pipe(filter((addresses) => addresses.length > 0)) - ); + const ownAddresses = await this.#dependencies.txBuilderProviders.addresses.get(); const registeredRewardAccounts = (await this.#dependencies.txBuilderProviders.rewardAccounts()).filter( (acct) => acct.keyStatus === Cardano.StakeKeyStatus.Registered || @@ -209,7 +212,7 @@ export class GenericTxBuilder implements TxBuilder { ); const auxiliaryData = this.partialAuxiliaryData && { ...this.partialAuxiliaryData }; const extraSigners = this.partialExtraSigners && [...this.partialExtraSigners]; - const signingOptions = this.partialSigningOptions && { ...this.partialSigningOptions }; + const partialSigningOptions = this.partialSigningOptions && { ...this.partialSigningOptions }; if (this.partialAuxiliaryData) { this.partialTxBody.auxiliaryDataHash = Cardano.computeAuxiliaryDataHash(this.partialAuxiliaryData); @@ -227,7 +230,7 @@ export class GenericTxBuilder implements TxBuilder { // If the wallet is currently delegating to several pools, and all delegations are being removed, // then the funds will be concentrated back into a single address. if (rewardAccountsWithWeights.size === 0) { - const firstAddress = await this.#dependencies.addressManager.deriveAddress( + const firstAddress = await this.#dependencies.bip32Account.deriveAddress( { index: 0, type: AddressType.External }, 0 ); @@ -249,7 +252,7 @@ export class GenericTxBuilder implements TxBuilder { certificates: this.partialTxBody.certificates, handleResolutions: this.#handleResolutions, outputs: new Set(this.partialTxBody.outputs || []), - signingOptions, + signingOptions: partialSigningOptions, witness: { extraSigners } }, dependencies @@ -259,7 +262,11 @@ export class GenericTxBuilder implements TxBuilder { auxiliaryData, handleResolutions: this.#handleResolutions, ownAddresses, - signingOptions, + signingContext: { + knownAddresses: ownAddresses, + txInKeyPathMap: await util.createTxInKeyPathMap(body, ownAddresses, this.#dependencies.inputResolver) + }, + signingOptions: partialSigningOptions, witness: { extraSigners } }, inputSelection, @@ -296,18 +303,22 @@ export class GenericTxBuilder implements TxBuilder { } async #getOrCreateRewardAccounts(): Promise { - let newRewardAccounts: Cardano.RewardAccount[] = []; + let allRewardAccounts: Cardano.RewardAccount[] = []; if (this.#requestedPortfolio) { - newRewardAccounts = await util.ensureStakeKeys({ - addressManager: this.#dependencies.addressManager, + const knownAddresses = await this.#dependencies.txBuilderProviders.addresses.get(); + const { newAddresses } = await util.ensureStakeKeys({ + bip32Account: this.#dependencies.bip32Account, count: this.#requestedPortfolio.length, + knownAddresses, logger: contextLogger(this.#logger, 'getOrCreateRewardAccounts') }); + await this.#dependencies.txBuilderProviders.addresses.add(...newAddresses); + allRewardAccounts = uniq([...knownAddresses, ...newAddresses]).map(({ rewardAccount }) => rewardAccount); } const rewardAccounts$ = coldObservableProvider({ pollUntil: (rewardAccounts) => - newRewardAccounts.every((newAccount) => rewardAccounts.some((acct) => acct.address === newAccount)), + allRewardAccounts.every((newAccount) => rewardAccounts.some((acct) => acct.address === newAccount)), provider: this.#dependencies.txBuilderProviders.rewardAccounts, retryBackoffConfig: { initialInterval: 10, maxInterval: 100, maxRetries: 10 } }); @@ -315,7 +326,7 @@ export class GenericTxBuilder implements TxBuilder { try { return await lastValueFrom(rewardAccounts$); } catch { - throw new OutOfSyncRewardAccounts(newRewardAccounts); + throw new OutOfSyncRewardAccounts(allRewardAccounts); } } @@ -431,7 +442,7 @@ export class GenericTxBuilder implements TxBuilder { ({ index }) => index ); if (!address) { - throw new Error(`Could not find any keyAgent address associated with ${rewardAccount}.`); + throw new Error(`Could not find any address associated with ${rewardAccount}.`); } return [address.address, weight]; }) diff --git a/packages/tx-construction/src/tx-builder/finalizeTx.ts b/packages/tx-construction/src/tx-builder/finalizeTx.ts index 7e18593c933..55a1cb834fa 100644 --- a/packages/tx-construction/src/tx-builder/finalizeTx.ts +++ b/packages/tx-construction/src/tx-builder/finalizeTx.ts @@ -1,23 +1,21 @@ import { Cardano, TxCBOR } from '@cardano-sdk/core'; import { FinalizeTxDependencies, SignedTx, TxContext } from './types'; import { + SignTransactionContext, SignTransactionOptions, TransactionSigner, Witnesser, util as keyManagementUtil } from '@cardano-sdk/key-management'; -import { filter, firstValueFrom } from 'rxjs'; const getSignatures = async ( - addressManager: keyManagementUtil.Bip32Ed25519AddressManager, witnesser: Witnesser, txInternals: Cardano.TxBodyWithHash, - extraSigners?: TransactionSigner[], - signingOptions?: SignTransactionOptions + signingContext: SignTransactionContext, + signingOptions?: SignTransactionOptions, + extraSigners?: TransactionSigner[] ) => { - // Wait until the async key agent has at least one known addresses. - await firstValueFrom(addressManager.knownAddresses$.pipe(filter((addresses) => addresses.length > 0))); - const { signatures } = await witnesser.witness(txInternals, signingOptions); + const { signatures } = await witnesser.witness(txInternals, signingContext, signingOptions); if (extraSigners) { for (const extraSigner of extraSigners) { @@ -31,20 +29,19 @@ const getSignatures = async ( export const finalizeTx = async ( tx: Cardano.TxBodyWithHash, - { ownAddresses, witness, signingOptions, auxiliaryData, isValid, handleResolutions }: TxContext, - { inputResolver, addressManager, witnesser }: FinalizeTxDependencies, + { witness, signingOptions, signingContext, auxiliaryData, isValid, handleResolutions }: TxContext, + { bip32Account: addressManager, witnesser }: FinalizeTxDependencies, stubSign = false ): Promise => { const signatures = stubSign ? await keyManagementUtil.stubSignTransaction({ - dRepPublicKey: await addressManager.derivePublicKey(keyManagementUtil.DREP_KEY_DERIVATION_PATH), + context: signingContext, + dRepPublicKey: (await addressManager.derivePublicKey(keyManagementUtil.DREP_KEY_DERIVATION_PATH)).hex(), extraSigners: witness?.extraSigners, - inputResolver, - knownAddresses: ownAddresses, signTransactionOptions: signingOptions, txBody: tx.body }) - : await getSignatures(addressManager, witnesser, tx, witness?.extraSigners, signingOptions); + : await getSignatures(witnesser, tx, signingContext, signingOptions, witness?.extraSigners); const transaction = { auxiliaryData, diff --git a/packages/tx-construction/src/tx-builder/initializeTx.ts b/packages/tx-construction/src/tx-builder/initializeTx.ts index 3895745c238..c0ab5c937a4 100644 --- a/packages/tx-construction/src/tx-builder/initializeTx.ts +++ b/packages/tx-construction/src/tx-builder/initializeTx.ts @@ -7,25 +7,32 @@ import { createTransactionInternals } from '../createTransactionInternals'; import { defaultSelectionConstraints } from '../input-selection'; import { ensureValidityInterval } from '../ensureValidityInterval'; import { finalizeTx } from './finalizeTx'; -import { firstValueFrom } from 'rxjs'; +import { util } from '@cardano-sdk/key-management'; export const initializeTx = async ( props: InitializeTxProps, - { txBuilderProviders, inputSelector, inputResolver, addressManager, witnesser, logger }: TxBuilderDependencies + { + txBuilderProviders, + inputSelector, + inputResolver, + bip32Account: addressManager, + witnesser, + logger + }: TxBuilderDependencies ): Promise => { - const [tip, genesisParameters, protocolParameters, addresses, rewardAccounts, utxo] = await Promise.all([ + const [tip, genesisParameters, protocolParameters, rewardAccounts, utxo, addresses] = await Promise.all([ txBuilderProviders.tip(), txBuilderProviders.genesisParameters(), txBuilderProviders.protocolParameters(), - firstValueFrom(addressManager.knownAddresses$), txBuilderProviders.rewardAccounts(), - txBuilderProviders.utxoAvailable() + txBuilderProviders.utxoAvailable(), + txBuilderProviders.addresses.get() ]); inputSelector = inputSelector ?? roundRobinRandomImprove({ - changeAddressResolver: new StaticChangeAddressResolver(() => firstValueFrom(addressManager.knownAddresses$)) + changeAddressResolver: new StaticChangeAddressResolver(async () => addresses) }); const validityInterval = ensureValidityInterval(tip.slot, genesisParameters, props.options?.validityInterval); @@ -59,11 +66,14 @@ export const initializeTx = async ( { auxiliaryData: props.auxiliaryData, handleResolutions: props.handleResolutions ?? [], - ownAddresses: addresses, + signingContext: { + knownAddresses: addresses, + txInKeyPathMap: await util.createTxInKeyPathMap(unsignedTx.body, addresses, inputResolver) + }, signingOptions: props.signingOptions, witness: props.witness }, - { addressManager, inputResolver, witnesser }, + { bip32Account: addressManager, witnesser }, true ); return tx; diff --git a/packages/tx-construction/src/tx-builder/types.ts b/packages/tx-construction/src/tx-builder/types.ts index 5d3a43f3dde..d9feb56b0b2 100644 --- a/packages/tx-construction/src/tx-builder/types.ts +++ b/packages/tx-construction/src/tx-builder/types.ts @@ -1,17 +1,16 @@ -import { Cardano, Handle, HandleProvider, HandleResolution, TxCBOR } from '@cardano-sdk/core'; -import { CustomError } from 'ts-custom-error'; - -import { InputSelectionError, InputSelector, SelectionSkeleton } from '@cardano-sdk/input-selection'; - import { + Bip32Account, GroupedAddress, + SignTransactionContext, SignTransactionOptions, TransactionSigner, - Witnesser, - util + Witnesser } from '@cardano-sdk/key-management'; +import { Cardano, Handle, HandleProvider, HandleResolution, TxCBOR } from '@cardano-sdk/core'; +import { CustomError } from 'ts-custom-error'; import { Hash32ByteBase16 } from '@cardano-sdk/crypto'; import { InitializeTxWitness, TxBuilderProviders } from '../types'; +import { InputSelectionError, InputSelector, SelectionSkeleton } from '@cardano-sdk/input-selection'; import { Logger } from 'ts-log'; import { OutputBuilderValidator } from './OutputBuilder'; import { OutputValidation } from '../output-validation'; @@ -137,8 +136,8 @@ export interface OutputBuilder { } export interface TxContext { - ownAddresses: GroupedAddress[]; signingOptions?: SignTransactionOptions; + signingContext: SignTransactionContext; auxiliaryData?: Cardano.AuxiliaryData; witness?: InitializeTxWitness; isValid?: boolean; @@ -146,8 +145,9 @@ export interface TxContext { } export type TxInspection = Cardano.TxBodyWithHash & - Pick & { + Pick & { inputSelection: SelectionSkeleton; + ownAddresses: GroupedAddress[]; }; export interface SignedTx { @@ -270,7 +270,7 @@ export interface TxBuilder { export interface TxBuilderDependencies { inputSelector?: InputSelector; inputResolver: Cardano.InputResolver; - addressManager: util.Bip32Ed25519AddressManager; + bip32Account: Bip32Account; witnesser: Witnesser; txBuilderProviders: TxBuilderProviders; logger: Logger; @@ -278,4 +278,4 @@ export interface TxBuilderDependencies { handleProvider?: HandleProvider; } -export type FinalizeTxDependencies = Pick; +export type FinalizeTxDependencies = Pick; diff --git a/packages/tx-construction/src/types.ts b/packages/tx-construction/src/types.ts index 73071f6db26..58a0c7c1357 100644 --- a/packages/tx-construction/src/types.ts +++ b/packages/tx-construction/src/types.ts @@ -1,7 +1,7 @@ import * as Crypto from '@cardano-sdk/crypto'; import { Cardano, HandleResolution } from '@cardano-sdk/core'; +import { GroupedAddress, SignTransactionOptions, TransactionSigner } from '@cardano-sdk/key-management'; import { SelectionSkeleton } from '@cardano-sdk/input-selection'; -import { SignTransactionOptions, TransactionSigner } from '@cardano-sdk/key-management'; import { MinimumCoinQuantityPerOutput } from './output-validation'; @@ -11,12 +11,23 @@ export type RewardAccountWithPoolId = Omit Promise; + /** + * When TxBuilder derives new addresses, it notifies the wallet via this method. + * + * @returns Updated wallet addresses (including old ones) + */ + add: (...addresses: GroupedAddress[]) => Promise; +} + export interface TxBuilderProviders { tip: () => Promise; protocolParameters: () => Promise; genesisParameters: () => Promise; rewardAccounts: () => Promise; utxoAvailable: () => Promise; + addresses: AddressesProvider; } export type InitializeTxWitness = Partial & { extraSigners?: TransactionSigner[] }; @@ -33,7 +44,7 @@ export interface InitializeTxProps { requiredExtraSignatures?: Crypto.Ed25519KeyHashHex[]; auxiliaryData?: Cardano.AuxiliaryData; witness?: InitializeTxWitness; - signingOptions?: SignTransactionOptions; + signingOptions?: Pick; handleResolutions?: HandleResolution[]; } diff --git a/packages/tx-construction/test/tx-builder/TxBuilder.bootstrap.test.ts b/packages/tx-construction/test/tx-builder/TxBuilder.bootstrap.test.ts index 31ac7e61de5..ef1d662792a 100644 --- a/packages/tx-construction/test/tx-builder/TxBuilder.bootstrap.test.ts +++ b/packages/tx-construction/test/tx-builder/TxBuilder.bootstrap.test.ts @@ -1,24 +1,46 @@ -/* eslint-disable sonarjs/no-duplicate-string */ -import { AddressType, util } from '@cardano-sdk/key-management'; +import { AddressType, Bip32Account, InMemoryKeyAgent, util } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; -import { GenericTxBuilder, OutputValidation } from '../../src'; -import { StubKeyAgent, mockProviders as mocks } from '@cardano-sdk/util-dev'; +import { GenericTxBuilder, OutputValidation, TxBuilderProviders } from '../../src'; +import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto'; import { dummyLogger } from 'ts-log'; -import delay from 'delay'; +import { logger, mockProviders as mocks } from '@cardano-sdk/util-dev'; describe('TxBuilder bootstrap', () => { - it('awaits for non-empty knownAddresses$', async () => { + it('awaits for non-empty knownAddresses', async () => { // Initialize the TxBuilder const output = mocks.utxo[0][1]; const rewardAccount = mocks.rewardAccount; + const knownAddresses = [ + { + accountIndex: 0, + address: mocks.utxo[0][1].address, + index: 0, + networkId: Cardano.NetworkId.Testnet, + rewardAccount, + type: AddressType.External + } + ]; const inputResolver: Cardano.InputResolver = { resolveInput: async (txIn) => mocks.utxo.find( ([hydratedTxIn]) => txIn.txId === hydratedTxIn.txId && txIn.index === hydratedTxIn.index )?.[1] || null }; - const keyAgent = new StubKeyAgent(inputResolver); - const txBuilderProviders = { + const keyAgent = util.createAsyncKeyAgent( + await InMemoryKeyAgent.fromBip39MnemonicWords( + { + chainId: Cardano.ChainIds.Preview, + getPassphrase: async () => Buffer.from([]), + mnemonicWords: util.generateMnemonicWords() + }, + { bip32Ed25519: new SodiumBip32Ed25519(), logger } + ) + ); + const txBuilderProviders: jest.Mocked = { + addresses: { + add: jest.fn(), + get: jest.fn().mockResolvedValue(knownAddresses) + }, genesisParameters: jest.fn().mockResolvedValue(mocks.genesisParameters), protocolParameters: jest.fn().mockResolvedValue(mocks.protocolParameters), rewardAccounts: jest.fn().mockResolvedValue([ @@ -36,7 +58,7 @@ describe('TxBuilder bootstrap', () => { }; const builderParams = { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), + bip32Account: await Bip32Account.fromAsyncKeyAgent(keyAgent), inputResolver, logger: dummyLogger, outputValidator, @@ -47,20 +69,6 @@ describe('TxBuilder bootstrap', () => { // Build and sign a tx const signedTxReady = txBuilder.addOutput(output).build().sign(); - await delay(1); - // keyAgent knownAddresses are initially [], - // but then eventually resolves to some addresses - // eslint-disable-next-line @typescript-eslint/no-floating-promises - keyAgent.setKnownAddresses([ - { - accountIndex: 0, - address: mocks.utxo[0][1].address, - index: 0, - networkId: Cardano.NetworkId.Testnet, - rewardAccount: mocks.rewardAccount, - type: AddressType.External - } - ]); const signedTx = await signedTxReady; expect(signedTx.tx.witness.signatures.size).toBe(1); }); diff --git a/packages/tx-construction/test/tx-builder/TxBuilder.test.ts b/packages/tx-construction/test/tx-builder/TxBuilder.test.ts index fa176e22a18..4a7d916fb1e 100644 --- a/packages/tx-construction/test/tx-builder/TxBuilder.test.ts +++ b/packages/tx-construction/test/tx-builder/TxBuilder.test.ts @@ -3,8 +3,9 @@ import * as Crypto from '@cardano-sdk/crypto'; import { AddressType, + Bip32Account, + GroupedAddress, InMemoryKeyAgent, - KeyRole, SignTransactionOptions, TransactionSigner, util @@ -51,6 +52,7 @@ describe('GenericTxBuilder', () => { let output: Cardano.TxOut; let output2: Cardano.TxOut; let inputResolver: Cardano.InputResolver; + let knownAddresses: GroupedAddress[]; beforeEach(async () => { output = mocks.utxo[0][1]; @@ -68,13 +70,22 @@ describe('GenericTxBuilder', () => { getPassphrase: async () => Buffer.from('passphrase'), mnemonicWords: util.generateMnemonicWords() }, - { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), inputResolver, logger: dummyLogger } + { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger: dummyLogger } ); - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); - keyAgent.knownAddresses[0].address = mocks.utxo[0][1].address; - keyAgent.knownAddresses[0].rewardAccount = rewardAccount; + + knownAddresses = [ + { + ...(await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0)), + address: mocks.utxo[0][1].address, + rewardAccount + } + ]; txBuilderProviders = { + addresses: { + add: jest.fn().mockImplementation(async (...newAddresses) => knownAddresses.push(...newAddresses)), + get: jest.fn().mockResolvedValue(knownAddresses) + }, genesisParameters: jest.fn().mockResolvedValue(mocks.genesisParameters), protocolParameters: jest.fn().mockResolvedValue(mocks.protocolParameters), rewardAccounts: jest.fn().mockResolvedValue([ @@ -93,7 +104,7 @@ describe('GenericTxBuilder', () => { const asyncKeyAgent = util.createAsyncKeyAgent(keyAgent); const builderParams = { - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), + bip32Account: await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent), inputResolver, logger: dummyLogger, outputValidator, @@ -239,7 +250,7 @@ describe('GenericTxBuilder', () => { let signingOptions: SignTransactionOptions; beforeEach(() => { - signingOptions = { additionalKeyPaths: [{ index: 0, role: KeyRole.Internal }] }; + signingOptions = { additionalKeyPaths: [{ index: 1, role: 1 }] }; }); it('can setSigningOptions without mutating signingOptions', () => { diff --git a/packages/tx-construction/test/tx-builder/TxBuilderDelegatePortfolio.test.ts b/packages/tx-construction/test/tx-builder/TxBuilderDelegatePortfolio.test.ts index 83b53c819ab..4feefbac554 100644 --- a/packages/tx-construction/test/tx-builder/TxBuilderDelegatePortfolio.test.ts +++ b/packages/tx-construction/test/tx-builder/TxBuilderDelegatePortfolio.test.ts @@ -1,6 +1,6 @@ /* eslint-disable sonarjs/no-duplicate-string */ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, GroupedAddress, InMemoryKeyAgent, util } from '@cardano-sdk/key-management'; +import { AddressType, Bip32Account, GroupedAddress, InMemoryKeyAgent, util } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; import { GenericTxBuilder, @@ -48,18 +48,22 @@ const inputResolver: Cardano.InputResolver = { const createTxBuilder = async ({ stakeKeyDelegations, + numAddresses = stakeKeyDelegations.length, useMultiplePaymentKeys = false, rewardAccounts, keyAgent }: { stakeKeyDelegations: { keyStatus: Cardano.StakeKeyStatus; poolId?: Cardano.PoolId }[]; + numAddresses?: number; useMultiplePaymentKeys?: boolean; // eslint-disable-next-line @typescript-eslint/no-explicit-any rewardAccounts?: any; keyAgent: InMemoryKeyAgent; }) => { let groupedAddresses = await Promise.all( - stakeKeyDelegations.map(async (_, idx) => keyAgent.deriveAddress({ index: 0, type: AddressType.External }, idx)) + Array.from({ length: numAddresses }).map(async (_, idx) => + keyAgent.deriveAddress({ index: 0, type: AddressType.External }, idx) + ) ); // Simulate an HD wallet where a each stake key partitions 2 payment keys (2 addresses per stake key) @@ -71,6 +75,10 @@ const createTxBuilder = async ({ } const txBuilderProviders: jest.Mocked = { + addresses: { + add: jest.fn().mockImplementation((...addreses) => groupedAddresses.push(...addreses)), + get: jest.fn().mockResolvedValue(groupedAddresses) + }, genesisParameters: jest.fn().mockResolvedValue(mocks.genesisParameters), protocolParameters: jest.fn().mockResolvedValue(mocks.protocolParameters), rewardAccounts: @@ -78,7 +86,7 @@ const createTxBuilder = async ({ jest.fn().mockImplementation(() => Promise.resolve( // There can be multiple addresses with the same reward account. Extract the uniq reward accounts - uniqBy(keyAgent.knownAddresses, ({ rewardAccount }) => rewardAccount) + uniqBy(groupedAddresses, ({ rewardAccount }) => rewardAccount) // Create mock stakeKey/delegation status for each reward account according to the requested stakeKeyDelegations. // This would normally be done by the wallet.delegation.rewardAccounts .map(({ rewardAccount: address }, index) => { @@ -102,7 +110,7 @@ const createTxBuilder = async ({ return { groupedAddresses, txBuilder: new GenericTxBuilder({ - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), + bip32Account: await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent), inputResolver, logger: dummyLogger, outputValidator, @@ -131,7 +139,7 @@ describe('TxBuilder/delegatePortfolio', () => { getPassphrase: async () => Buffer.from('passphrase'), mnemonicWords: util.generateMnemonicWords() }, - { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), inputResolver, logger: dummyLogger } + { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger: dummyLogger } ); }); @@ -666,23 +674,24 @@ describe('TxBuilder/delegatePortfolio', () => { describe('rewardAccount syncing', () => { const normalRewardAccountsCalls = 3; it('can wait for delayed key agent stake keys', async () => { - await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); + const address = await keyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); const rewardAccountsProvider = jest .fn() .mockResolvedValueOnce([]) .mockResolvedValueOnce([]) .mockResolvedValueOnce([]) .mockImplementation(() => - Promise.resolve( - keyAgent.knownAddresses.map(({ rewardAccount: address }) => ({ - address, + Promise.resolve([ + { + address: address.rewardAccount, keyStatus: Cardano.StakeKeyStatus.Unregistered, rewardBalance: mocks.rewardAccountBalance - })) - ) + } + ]) ); const txBuilderFactory = await createTxBuilder({ keyAgent, + numAddresses: 1, rewardAccounts: rewardAccountsProvider, stakeKeyDelegations: [] }); diff --git a/packages/util-dev/src/StubKeyAgent.ts b/packages/util-dev/src/StubKeyAgent.ts deleted file mode 100644 index bf7621a88ce..00000000000 --- a/packages/util-dev/src/StubKeyAgent.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { - AccountKeyDerivationPath, - AsyncKeyAgent, - GroupedAddress, - SignBlobResult, - util -} from '@cardano-sdk/key-management'; -import { BehaviorSubject, firstValueFrom } from 'rxjs'; -import { Cardano } from '@cardano-sdk/core'; -import { generateRandomHexString } from './dataGeneration'; - -const NOT_IMPLEMENTED = 'Method not implemented'; - -export class StubKeyAgent implements AsyncKeyAgent { - static readonly dRepPubKey = Crypto.Ed25519PublicKeyHex( - '0b1c96fad4179d7910bd9485ac28c4c11368c83d18d01b29d4cf84d8ff6a06c4' - ); - - knownAddresses$ = new BehaviorSubject([]); - - constructor(private inputResolver: Cardano.InputResolver) {} - - deriveAddress(): Promise { - throw new Error(NOT_IMPLEMENTED); - } - derivePublicKey(derivationPath: AccountKeyDerivationPath): Promise { - if (derivationPath.role === util.DREP_KEY_DERIVATION_PATH.role) { - return Promise.resolve(StubKeyAgent.dRepPubKey); - } - throw new Error(NOT_IMPLEMENTED); - } - signBlob(): Promise { - throw new Error(NOT_IMPLEMENTED); - } - async signTransaction(txInternals: Cardano.TxBodyWithHash): Promise { - const signatures = new Map(); - const knownAddresses = await firstValueFrom(this.knownAddresses$); - for (const _ of await util.ownSignatureKeyPaths( - txInternals.body, - knownAddresses, - this.inputResolver, - Crypto.Ed25519KeyHashHex('f15db05f56035465bf8900a09bdaa16c3d8b8244fea686524408dd80') - )) { - signatures.set( - Crypto.Ed25519PublicKeyHex(generateRandomHexString(64)), - Crypto.Ed25519SignatureHex(generateRandomHexString(128)) - ); - } - return signatures; - } - getChainId(): Promise { - throw new Error(NOT_IMPLEMENTED); - } - getBip32Ed25519(): Promise { - throw new Error(NOT_IMPLEMENTED); - } - getExtendedAccountPublicKey(): Promise { - throw new Error(NOT_IMPLEMENTED); - } - async setKnownAddresses(addresses: GroupedAddress[]): Promise { - this.knownAddresses$.next(addresses); - } - shutdown(): void { - throw new Error(NOT_IMPLEMENTED); - } -} diff --git a/packages/util-dev/src/index.ts b/packages/util-dev/src/index.ts index 9862d24a70b..3bfa3bcdaf6 100644 --- a/packages/util-dev/src/index.ts +++ b/packages/util-dev/src/index.ts @@ -11,7 +11,6 @@ export * from './createStubObservable'; export * from './createGenericMockServer'; export * from './dataGeneration'; export * from './eraSummaries'; -export * from './StubKeyAgent'; export * as mockProviders from './mockProviders'; export * as handleProviderMocks from './handleProvider'; export * as cip19TestVectors from './Cip19TestVectors'; diff --git a/packages/wallet/src/PersonalWallet/PersonalWallet.ts b/packages/wallet/src/PersonalWallet/PersonalWallet.ts index 2442d4f233b..5e6e522cad4 100644 --- a/packages/wallet/src/PersonalWallet/PersonalWallet.ts +++ b/packages/wallet/src/PersonalWallet/PersonalWallet.ts @@ -2,6 +2,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies import { AddressDiscovery, + AddressTracker, BalanceTracker, ConnectionStatus, ConnectionStatusTracker, @@ -25,6 +26,7 @@ import { TransactionsTracker, UtxoTracker, WalletUtil, + createAddressTracker, createAssetsTracker, createBalanceTracker, createDelegationTracker, @@ -37,8 +39,7 @@ import { createWalletUtil, currentEpochTracker, distinctBlock, - distinctEraSummaries, - groupedAddressesEquals + distinctEraSummaries } from '../services'; import { AssetProvider, @@ -73,17 +74,24 @@ import { Subject, Subscription, catchError, - concat, - distinctUntilChanged, filter, firstValueFrom, from, map, mergeMap, switchMap, + take, tap, throwError } from 'rxjs'; +import { + Bip32Account, + GroupedAddress, + Witnesser, + cip8, + util as keyManagementUtil, + util +} from '@cardano-sdk/key-management'; import { ChangeAddressResolver, InputSelector, roundRobinRandomImprove } from '@cardano-sdk/input-selection'; import { Cip30DataSignature } from '@cardano-sdk/dapp-connector'; import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto'; @@ -97,7 +105,6 @@ import { finalizeTx, initializeTx } from '@cardano-sdk/tx-construction'; -import { GroupedAddress, Witnesser, cip8, util as keyManagementUtil } from '@cardano-sdk/key-management'; import { Logger } from 'ts-log'; import { PubStakeKeyAndStatus, createPublicStakeKeysTracker } from '../services/PublicStakeKeysTracker'; import { RetryBackoffConfig } from 'backoff-rxjs'; @@ -115,7 +122,7 @@ export interface PersonalWalletProps { export interface PersonalWalletDependencies { readonly witnesser: Witnesser; - readonly addressManager: keyManagementUtil.Bip32Ed25519AddressManager; + readonly bip32Account: Bip32Account; readonly txSubmitProvider: TxSubmitProvider; readonly stakePoolProvider: StakePoolProvider; readonly assetProvider: AssetProvider; @@ -197,11 +204,11 @@ export class PersonalWallet implements ObservableWallet { #reemitSubscriptions: Subscription; #failedFromReemitter$: Subject; #trackedTxSubmitProvider: TrackedTxSubmitProvider; - #addressDiscovery: AddressDiscovery; + #addressTracker: AddressTracker; #submittingPromises: Partial>> = {}; readonly witnesser: Witnesser; - readonly addressManager: keyManagementUtil.Bip32Ed25519AddressManager; + readonly bip32Account: Bip32Account; readonly currentEpoch$: TrackerSubject; readonly txSubmitProvider: TxSubmitProvider; readonly utxoProvider: TrackedUtxoProvider; @@ -215,7 +222,7 @@ export class PersonalWallet implements ObservableWallet { readonly delegation: DelegationTracker & Shutdown; readonly tip$: BehaviorObservable; readonly eraSummaries$: TrackerSubject; - readonly addresses$: TrackerSubject; + readonly addresses$: Observable; readonly protocolParameters$: TrackerSubject; readonly genesisParameters$: TrackerSubject; readonly assetInfo$: TrackerSubject; @@ -248,7 +255,7 @@ export class PersonalWallet implements ObservableWallet { txSubmitProvider, stakePoolProvider, witnesser, - addressManager, + bip32Account: addressManager, assetProvider, handleProvider, networkInfoProvider, @@ -264,7 +271,6 @@ export class PersonalWallet implements ObservableWallet { ) { this.#logger = contextLogger(logger, name); - this.#addressDiscovery = addressDiscovery; this.#trackedTxSubmitProvider = new TrackedTxSubmitProvider(txSubmitProvider); this.utxoProvider = new TrackedUtxoProvider(utxoProvider); @@ -287,7 +293,7 @@ export class PersonalWallet implements ObservableWallet { { consideredOutOfSyncAfter } ); - this.addressManager = addressManager; + this.bip32Account = addressManager; this.witnesser = witnesser; this.fatalError$ = new Subject(); @@ -300,33 +306,23 @@ export class PersonalWallet implements ObservableWallet { filter((status) => status === ConnectionStatus.down) ); - this.addresses$ = new TrackerSubject( - concat( - stores.addresses.get(), - this.addressManager.knownAddresses$.pipe( - distinctUntilChanged(groupedAddressesEquals), - tap( - // derive addresses if none available - (addresses) => { - if (addresses.length === 0) { - this.#logger.debug('No addresses available; initiating address discovery process'); - - firstValueFrom( - coldObservableProvider({ - cancel$, - onFatalError, - provider: () => this.#addressDiscovery.discover(this.addressManager), - retryBackoffConfig - }) - ).catch(() => this.#logger.error('Failed to complete the address discovery process')); - } - } - ), - filter((addresses) => addresses.length > 0), - tap(stores.addresses.set.bind(stores.addresses)) - ) - ) - ); + this.#addressTracker = createAddressTracker({ + addressDiscovery$: coldObservableProvider({ + cancel$, + onFatalError, + provider: () => addressDiscovery.discover(this.bip32Account), + retryBackoffConfig + }).pipe( + take(1), + catchError((error) => { + this.#logger.error('Failed to complete the address discovery process', error); + throw error; + }) + ), + logger: this.#logger, + store: stores.addresses + }); + this.addresses$ = this.#addressTracker.addresses$; this.#tip$ = this.tip$ = new TipTracker({ connectionStatus$: connectionStatusTracker$, @@ -456,7 +452,7 @@ export class PersonalWallet implements ObservableWallet { this.delegation = createDelegationTracker({ epoch$, eraSummaries$, - knownAddresses$: this.addressManager.knownAddresses$, + knownAddresses$: this.addresses$, logger: contextLogger(this.#logger, 'delegation'), onFatalError, retryBackoffConfig, @@ -485,8 +481,8 @@ export class PersonalWallet implements ObservableWallet { }); this.publicStakeKeys$ = createPublicStakeKeysTracker({ - addressManager: this.addressManager, addresses$: this.addresses$, + bip32Account: this.bip32Account, rewardAccounts$: this.delegation.rewardAccounts$ }); @@ -536,10 +532,17 @@ export class PersonalWallet implements ObservableWallet { } async finalizeTx({ tx, ...rest }: FinalizeTxProps, stubSign = false): Promise { + const knownAddresses = await firstValueFrom(this.addresses$); const { tx: signedTx } = await finalizeTx( tx, - { ...rest, ownAddresses: await firstValueFrom(this.addresses$) }, - { addressManager: this.addressManager, inputResolver: this.util, witnesser: this.witnesser }, + { + ...rest, + signingContext: { + knownAddresses, + txInKeyPathMap: await util.createTxInKeyPathMap(tx.body, knownAddresses, this.util) + } + }, + { bip32Account: this.bip32Account, witnesser: this.witnesser }, stubSign ); return signedTx; @@ -624,12 +627,12 @@ export class PersonalWallet implements ObservableWallet { })()); } - signData(props: SignDataProps): Promise { + async signData(props: SignDataProps): Promise { return cip8.cip30signData({ // TODO: signData probably needs to be refactored out of the wallet and supported as a stand alone util // as this operation doesnt require any of the wallet state. Also this operation can only be performed // by Bip32Ed25519 type of wallets. - addressManager: this.addressManager, + knownAddresses: await firstValueFrom(this.addresses$), witnesser: this.witnesser as keyManagementUtil.Bip32Ed25519Witnesser, ...props }); @@ -644,7 +647,7 @@ export class PersonalWallet implements ObservableWallet { this.protocolParameters$.complete(); this.genesisParameters$.complete(); this.#tip$.complete(); - this.addresses$.complete(); + this.#addressTracker.shutdown(); this.assetProvider.stats.shutdown(); this.#trackedTxSubmitProvider.stats.shutdown(); this.networkInfoProvider.stats.shutdown(); @@ -652,7 +655,6 @@ export class PersonalWallet implements ObservableWallet { this.utxoProvider.stats.shutdown(); this.rewardsProvider.stats.shutdown(); this.chainHistoryProvider.stats.shutdown(); - this.addressManager.shutdown(); this.currentEpoch$.complete(); this.delegation.shutdown(); this.assetInfo$.complete(); @@ -687,13 +689,17 @@ export class PersonalWallet implements ObservableWallet { */ getTxBuilderDependencies(): TxBuilderDependencies { return { - addressManager: this.addressManager, + bip32Account: this.bip32Account, handleProvider: this.handleProvider, inputResolver: this.util, inputSelector: this.#inputSelector, logger: this.#logger, outputValidator: this.util, txBuilderProviders: { + addresses: { + add: (...newAddresses) => firstValueFrom(this.#addressTracker.addAddresses(newAddresses)), + get: () => firstValueFrom(this.addresses$) + }, genesisParameters: () => this.#firstValueFromSettled(this.genesisParameters$), protocolParameters: () => this.#firstValueFromSettled(this.protocolParameters$), rewardAccounts: () => this.#firstValueFromSettled(this.delegation.rewardAccounts$), @@ -721,7 +727,7 @@ export class PersonalWallet implements ObservableWallet { async getPubDRepKey(): Promise { if (!this.drepPubKey) { try { - this.drepPubKey = await this.addressManager.derivePublicKey(keyManagementUtil.DREP_KEY_DERIVATION_PATH); + this.drepPubKey = (await this.bip32Account.derivePublicKey(keyManagementUtil.DREP_KEY_DERIVATION_PATH)).hex(); } catch (error) { this.#logger.error(error); throw error; diff --git a/packages/wallet/src/index.ts b/packages/wallet/src/index.ts index 046972cb314..1976e35d58b 100644 --- a/packages/wallet/src/index.ts +++ b/packages/wallet/src/index.ts @@ -3,4 +3,3 @@ export * from './types'; export * from './services'; export * as storage from './persistence'; export * as cip30 from './cip30'; -export * from './setupWallet'; diff --git a/packages/wallet/src/services/AddressDiscovery/HDSequentialDiscovery.ts b/packages/wallet/src/services/AddressDiscovery/HDSequentialDiscovery.ts index 7a04540f522..6fb32f35982 100644 --- a/packages/wallet/src/services/AddressDiscovery/HDSequentialDiscovery.ts +++ b/packages/wallet/src/services/AddressDiscovery/HDSequentialDiscovery.ts @@ -1,4 +1,4 @@ -import { AccountAddressDerivationPath, AddressType, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AccountAddressDerivationPath, AddressType, Bip32Account, GroupedAddress } from '@cardano-sdk/key-management'; import { AddressDiscovery } from '../types'; import { ChainHistoryProvider } from '@cardano-sdk/core'; import uniqBy from 'lodash/uniqBy'; @@ -26,14 +26,14 @@ const addressHasTx = async (address: GroupedAddress, chainHistoryProvider: Chain /** * Search for all base addresses composed with the given payment and stake credentials. * - * @param manager The address manager to be used to derive the addresses to be discovered. + * @param account The bip32 account to be used to derive the addresses to be discovered. * @param chainHistoryProvider The chain history provider. * @param lookAheadCount Number down the derivation chain to be searched for. * @param getDeriveAddressArgs Callback that retrieves the derivation path arguments. * @returns A promise that will be resolved into a GroupedAddress list containing the discovered addresses. */ const discoverAddresses = async ( - manager: util.Bip32Ed25519AddressManager, + account: Bip32Account, chainHistoryProvider: ChainHistoryProvider, lookAheadCount: number, getDeriveAddressArgs: ( @@ -52,16 +52,14 @@ const discoverAddresses = async ( const externalAddressArgs = getDeriveAddressArgs(currentIndex, AddressType.External); const internalAddressArgs = getDeriveAddressArgs(currentIndex, AddressType.Internal); - const externalAddress = await manager.deriveAddress( + const externalAddress = await account.deriveAddress( externalAddressArgs.paymentKeyDerivationPath, - externalAddressArgs.stakeKeyDerivationIndex, - true + externalAddressArgs.stakeKeyDerivationIndex ); - const internalAddress = await manager.deriveAddress( + const internalAddress = await account.deriveAddress( internalAddressArgs.paymentKeyDerivationPath, - internalAddressArgs.stakeKeyDerivationIndex, - true + internalAddressArgs.stakeKeyDerivationIndex ); const externalHasTx = await addressHasTx(externalAddress, chainHistoryProvider); @@ -114,9 +112,9 @@ export class HDSequentialDiscovery implements AddressDiscovery { * @param manager The address manager be used to derive the addresses to be discovered. * @returns A promise that will be resolved into a GroupedAddress list containing the discovered addresses. */ - public async discover(manager: util.Bip32Ed25519AddressManager): Promise { - const firstAddresses = [await manager.deriveAddress({ index: 0, type: AddressType.External }, 0, true)]; - const firstInternalAddress = await manager.deriveAddress({ index: 0, type: AddressType.Internal }, 0, true); + public async discover(manager: Bip32Account): Promise { + const firstAddresses = [await manager.deriveAddress({ index: 0, type: AddressType.External }, 0)]; + const firstInternalAddress = await manager.deriveAddress({ index: 0, type: AddressType.Internal }, 0); if (await addressHasTx(firstInternalAddress, this.#chainHistoryProvider)) { firstAddresses.push(firstInternalAddress); } @@ -153,9 +151,8 @@ export class HDSequentialDiscovery implements AddressDiscovery { // We need to make sure the addresses are sorted since the wallet assumes that the first address // in the list is the change address (payment cred 0 and stake cred 0). - addresses.sort((a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index); - await manager.setKnownAddresses(addresses); - - return addresses; + return addresses.sort( + (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index + ); } } diff --git a/packages/wallet/src/services/AddressDiscovery/SingleAddressDiscovery.ts b/packages/wallet/src/services/AddressDiscovery/SingleAddressDiscovery.ts index 8748b631945..cec62a544f5 100644 --- a/packages/wallet/src/services/AddressDiscovery/SingleAddressDiscovery.ts +++ b/packages/wallet/src/services/AddressDiscovery/SingleAddressDiscovery.ts @@ -1,12 +1,12 @@ import { AddressDiscovery } from '../types'; -import { AddressType, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AddressType, Bip32Account, GroupedAddress } from '@cardano-sdk/key-management'; /** * Discovers the first address in the derivation chain (both payment and stake credentials) without looking at the * chain history. */ export class SingleAddressDiscovery implements AddressDiscovery { - public async discover(manager: util.Bip32Ed25519AddressManager): Promise { + public async discover(manager: Bip32Account): Promise { const address = await manager.deriveAddress({ index: 0, type: AddressType.External }, 0); return [address]; } diff --git a/packages/wallet/src/services/AddressTracker.ts b/packages/wallet/src/services/AddressTracker.ts new file mode 100644 index 00000000000..7c0bb5b36d1 --- /dev/null +++ b/packages/wallet/src/services/AddressTracker.ts @@ -0,0 +1,94 @@ +import { GroupedAddress } from '@cardano-sdk/key-management'; +import { Logger } from 'ts-log'; +import { + Observable, + Subject, + defaultIfEmpty, + distinctUntilChanged, + filter, + map, + merge, + mergeMap, + of, + shareReplay, + switchMap, + take, + tap +} from 'rxjs'; +import { WalletStores } from '../persistence'; +import { groupedAddressesEquals } from './util'; + +export type AddressTrackerDependencies = { + store: WalletStores['addresses']; + addressDiscovery$: Observable; + logger: Logger; +}; + +export const createAddressTracker = ({ addressDiscovery$, store, logger }: AddressTrackerDependencies) => { + // eslint-disable-next-line unicorn/consistent-function-scoping + const storeAddresses = () => (addresses$: Observable) => + addresses$.pipe(switchMap((addresses) => store.set(addresses).pipe(map(() => addresses)))); + + const newAddresses$ = new Subject(); + const addresses$ = store + .get() + .pipe( + defaultIfEmpty([]), + mergeMap( + // derive addresses if none available + (addresses) => { + if (addresses.length === 0) { + logger.debug('No addresses available; initiating address discovery process'); + return addressDiscovery$.pipe( + tap((derivedAddresses) => { + if (derivedAddresses.length === 0) { + throw new Error('Address discovery derived 0 addresses'); + } + }), + storeAddresses() + ); + } + + return of(addresses); + } + ), + switchMap((addresses) => { + const addressCache = [...addresses]; + return merge( + of(addresses), + newAddresses$.pipe( + map((newAddresses) => { + for (const newAddress of newAddresses) { + if (addressCache.some((addr) => addr.address === newAddress.address)) { + logger.warn('Address already exists', newAddress.address); + continue; + } + + addressCache.push(newAddress); + } + + return [...addressCache]; + }), + storeAddresses() + ) + ).pipe(distinctUntilChanged(groupedAddressesEquals)); + }) + ) + .pipe(shareReplay(1)); + + return { + addAddresses: (newAddresses: GroupedAddress[]) => { + newAddresses$.next(newAddresses); + return addresses$.pipe( + filter((addresses) => + newAddresses.every(({ address: newAddress }) => addresses.some(({ address }) => address === newAddress)) + ), + take(1) + ); + }, + addresses$, + shutdown: newAddresses$.complete.bind(newAddresses$) + }; +}; + +export type AddressTracker = ReturnType; diff --git a/packages/wallet/src/services/DelegationTracker/DelegationTracker.ts b/packages/wallet/src/services/DelegationTracker/DelegationTracker.ts index 76ceb0ba712..134d93adb35 100644 --- a/packages/wallet/src/services/DelegationTracker/DelegationTracker.ts +++ b/packages/wallet/src/services/DelegationTracker/DelegationTracker.ts @@ -1,6 +1,6 @@ -import { AsyncKeyAgent } from '@cardano-sdk/key-management'; import { Cardano, ChainHistoryProvider, EraSummary, SlotEpochCalc, createSlotEpochCalc } from '@cardano-sdk/core'; import { DelegationTracker, TransactionsTracker, UtxoTracker } from '../types'; +import { GroupedAddress } from '@cardano-sdk/key-management'; import { Logger } from 'ts-log'; import { Observable, combineLatest, map, tap } from 'rxjs'; import { @@ -44,7 +44,7 @@ export interface DelegationTrackerProps { transactionsTracker: TransactionsTracker; retryBackoffConfig: RetryBackoffConfig; utxoTracker: UtxoTracker; - knownAddresses$: AsyncKeyAgent['knownAddresses$']; + knownAddresses$: Observable; stores: WalletStores; internals?: { queryStakePoolsProvider?: ObservableStakePoolProvider; diff --git a/packages/wallet/src/services/KeyAgent/restoreKeyAgent.ts b/packages/wallet/src/services/KeyAgent/restoreKeyAgent.ts index f330bfca002..c7c70b4aa7f 100644 --- a/packages/wallet/src/services/KeyAgent/restoreKeyAgent.ts +++ b/packages/wallet/src/services/KeyAgent/restoreKeyAgent.ts @@ -3,7 +3,6 @@ import { Cardano } from '@cardano-sdk/core'; import { GetPassphrase, - GroupedAddress, InMemoryKeyAgent, KeyAgent, KeyAgentDependencies, @@ -12,8 +11,7 @@ import { SerializableKeyAgentData, SerializableLedgerKeyAgentData, SerializableTrezorKeyAgentData, - errors, - util + errors } from '@cardano-sdk/key-management'; import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger'; import { Logger } from 'ts-log'; @@ -42,11 +40,7 @@ const migrateSerializableData = (data: any, return Cardano.ChainIds.Preprod; })() }; - })(), - knownAddresses: data.knownAddresses.map((address: GroupedAddress) => ({ - ...address, - stakeKeyDerivationPath: address.stakeKeyDerivationPath || util.STAKE_KEY_DERIVATION_PATH - })) + })() }); export function restoreKeyAgent( diff --git a/packages/wallet/src/services/PublicStakeKeysTracker.ts b/packages/wallet/src/services/PublicStakeKeysTracker.ts index 0a7486c7688..5523a5819fe 100644 --- a/packages/wallet/src/services/PublicStakeKeysTracker.ts +++ b/packages/wallet/src/services/PublicStakeKeysTracker.ts @@ -1,4 +1,4 @@ -import { AccountKeyDerivationPath, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AccountKeyDerivationPath, Bip32Account, GroupedAddress } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto'; import { Observable, defaultIfEmpty, distinctUntilChanged, forkJoin, from, map, mergeMap, switchMap } from 'rxjs'; @@ -8,7 +8,7 @@ import { deepEquals } from '@cardano-sdk/util'; export interface CreatePubStakeKeysTrackerProps { addresses$: Observable; rewardAccounts$: Observable; - addressManager: util.Bip32Ed25519AddressManager; + bip32Account: Bip32Account; } export interface PubStakeKeyAndStatus { @@ -43,7 +43,7 @@ const withStakeKeyDerivationPaths = export const createPublicStakeKeysTracker = ({ addresses$, rewardAccounts$, - addressManager + bip32Account: addressManager }: CreatePubStakeKeysTrackerProps) => new TrackerSubject( rewardAccounts$.pipe( @@ -52,7 +52,7 @@ export const createPublicStakeKeysTracker = ({ forkJoin( derivationPathsAndStatus.map(({ stakeKeyDerivationPath, keyStatus }) => from(addressManager.derivePublicKey(stakeKeyDerivationPath)).pipe( - map((publicStakeKey) => ({ keyStatus, publicStakeKey })) + map((publicStakeKey) => ({ keyStatus, publicStakeKey: publicStakeKey.hex() })) ) ) ).pipe(defaultIfEmpty([])) diff --git a/packages/wallet/src/services/WalletUtil.ts b/packages/wallet/src/services/WalletUtil.ts index 8905798bddf..d76bdf8516a 100644 --- a/packages/wallet/src/services/WalletUtil.ts +++ b/packages/wallet/src/services/WalletUtil.ts @@ -41,42 +41,6 @@ export const createWalletUtil = (context: WalletUtilContext) => ({ export type WalletUtil = ReturnType; -type SetWalletUtilContext = (context: WalletUtilContext) => void; - -/** - * Creates a WalletUtil that has an additional function to `initialize` by setting the context. - * Calls to WalletUtil functions will only resolve after initializing. - * - * @returns common wallet utility functions that are aware of wallet state and computes useful things - */ -export const createLazyWalletUtil = (): WalletUtil & { initialize: SetWalletUtilContext } => { - let initialize: SetWalletUtilContext; - const resolverReady = new Promise((resolve: SetWalletUtilContext) => (initialize = resolve)).then(createWalletUtil); - return { - initialize: initialize!, - async resolveInput(input) { - const resolver = await resolverReady; - return resolver.resolveInput(input); - }, - async validateOutput(output) { - const resolver = await resolverReady; - return resolver.validateOutput(output); - }, - async validateOutputs(outputs) { - const resolver = await resolverReady; - return resolver.validateOutputs(outputs); - }, - async validateValue(value) { - const resolver = await resolverReady; - return resolver.validateValue(value); - }, - async validateValues(values) { - const resolver = await resolverReady; - return resolver.validateValues(values); - } - }; -}; - /** All transaction inputs and collaterals must come from our utxo set */ const hasForeignInputs = ( { body: { inputs, collaterals = [] } }: { body: Pick }, diff --git a/packages/wallet/src/services/index.ts b/packages/wallet/src/services/index.ts index f517f6beb5a..86475111e2d 100644 --- a/packages/wallet/src/services/index.ts +++ b/packages/wallet/src/services/index.ts @@ -16,3 +16,4 @@ export * from './KeyAgent'; export * from './AddressDiscovery'; export * from './HandlesTracker'; export * from './ChangeAddress'; +export * from './AddressTracker'; diff --git a/packages/wallet/src/services/types.ts b/packages/wallet/src/services/types.ts index 4c8643b04ad..bdbde26e445 100644 --- a/packages/wallet/src/services/types.ts +++ b/packages/wallet/src/services/types.ts @@ -1,5 +1,5 @@ +import { Bip32Account, GroupedAddress } from '@cardano-sdk/key-management'; import { Cardano, Reward, TxCBOR } from '@cardano-sdk/core'; -import { GroupedAddress, util } from '@cardano-sdk/key-management'; import { Observable } from 'rxjs'; import { Percent } from '@cardano-sdk/util'; import { SignedTx } from '@cardano-sdk/tx-construction'; @@ -44,7 +44,7 @@ export interface AddressDiscovery { * @param addressManager The address manager to be used to derive the addresses to be discovered. * @returns A promise that will be resolved into a GroupedAddress list containing the discovered addresses. */ - discover(addressManager: util.Bip32Ed25519AddressManager): Promise; + discover(addressManager: Bip32Account): Promise; } export type Milliseconds = number; diff --git a/packages/wallet/src/setupWallet.ts b/packages/wallet/src/setupWallet.ts deleted file mode 100644 index a292a731849..00000000000 --- a/packages/wallet/src/setupWallet.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { AsyncKeyAgent, KeyAgentDependencies } from '@cardano-sdk/key-management'; -import { Logger } from 'ts-log'; -import { ObservableWallet } from './types'; -import { WalletUtil, WalletUtilContext, createLazyWalletUtil } from './services'; - -export interface SetupWalletProps { - createKeyAgent: (dependencies: KeyAgentDependencies) => Promise; - createWallet: (keyAgent: TKeyAgent) => Promise; - logger: Logger; - bip32Ed25519: Crypto.Bip32Ed25519; -} - -/** - * Creates a wallet and a key agent that has the context of that wallet. - * - * Use this if you want to create a KeyAgent that uses wallet as InputResolver. - * - * Encapsulates the logic to resolve circular dependency of Wallet->KeyAgent->InputResolver(WalletUtil)->Wallet. - */ -export const setupWallet = async ({ - createKeyAgent, - createWallet, - logger, - bip32Ed25519 -}: SetupWalletProps) => { - const walletUtil = createLazyWalletUtil(); - const keyAgent = await createKeyAgent({ bip32Ed25519, inputResolver: walletUtil, logger }); - const wallet = await createWallet(keyAgent); - walletUtil.initialize(wallet); - return { keyAgent, wallet, walletUtil: walletUtil as WalletUtil }; -}; diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 450e6843f6c..784e429713c 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -10,7 +10,7 @@ import { import { BalanceTracker, DelegationTracker, TransactionsTracker, UtxoTracker } from './services'; import { Cip30DataSignature } from '@cardano-sdk/dapp-connector'; import { Ed25519PublicKeyHex } from '@cardano-sdk/crypto'; -import { GroupedAddress, cip8 } from '@cardano-sdk/key-management'; +import { GroupedAddress, SignTransactionOptions, cip8 } from '@cardano-sdk/key-management'; import { InitializeTxProps, InitializeTxResult, SignedTx, TxBuilder, TxContext } from '@cardano-sdk/tx-construction'; import { Observable } from 'rxjs'; import { PubStakeKeyAndStatus } from './services/PublicStakeKeysTracker'; @@ -18,7 +18,7 @@ import { Shutdown } from '@cardano-sdk/util'; export type Assets = Map; -export type SignDataProps = Omit; +export type SignDataProps = Omit; export interface SyncStatus extends Shutdown { /** @@ -43,8 +43,9 @@ export interface SyncStatus extends Shutdown { isSettled$: Observable; } -export type FinalizeTxProps = Omit & { +export type FinalizeTxProps = Omit & { tx: Cardano.TxBodyWithHash; + signingOptions?: SignTransactionOptions; }; export type HandleInfo = HandleResolution & Asset.AssetInfo; diff --git a/packages/wallet/test/PersonalWallet/load.test.ts b/packages/wallet/test/PersonalWallet/load.test.ts index 8701aac509e..9fe1a510cc9 100644 --- a/packages/wallet/test/PersonalWallet/load.test.ts +++ b/packages/wallet/test/PersonalWallet/load.test.ts @@ -1,7 +1,5 @@ /* eslint-disable unicorn/consistent-destructuring */ /* eslint-disable max-statements */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import * as Crypto from '@cardano-sdk/crypto'; import { AddressDiscovery, ConnectionStatus, @@ -9,9 +7,9 @@ import { ObservableWallet, PersonalWallet, PollingConfig, - setupWallet + SingleAddressDiscovery } from '../../src'; -import { AddressType, AsyncKeyAgent, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AddressType, AsyncKeyAgent, Bip32Account, GroupedAddress, util } from '@cardano-sdk/key-management'; import { AssetId, createStubStakePoolProvider, @@ -34,7 +32,6 @@ import { InvalidStringError } from '@cardano-sdk/util'; import { ReplaySubject, firstValueFrom } from 'rxjs'; import { WalletStores, createInMemoryWalletStores } from '../../src/persistence'; import { dummyLogger as logger } from 'ts-log'; -import { prepareMockKeyAgentWithData } from '../services/addressDiscovery/mockData'; import { stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; import { waitForWalletStateSettle } from '../util'; import delay from 'delay'; @@ -55,7 +52,6 @@ const name = 'Test Wallet'; const address = mocks.utxo[0][0].address!; const rewardAccount = mocks.rewardAccount; -const bip32Ed25519 = new Crypto.SodiumBip32Ed25519(); interface Providers { rewardsProvider: RewardsProvider; utxoProvider: UtxoProvider; @@ -75,13 +71,12 @@ export class MockAddressDiscovery implements AddressDiscovery { this.#resolveAfterAttempts = resolveAfterAttempts; } - public async discover(addressManager: util.Bip32Ed25519AddressManager): Promise { + public async discover(): Promise { if (this.#currentAttempt <= this.#resolveAfterAttempts) { ++this.#currentAttempt; throw new Error('An error occurred during the discovery process.'); } - await addressManager.setKnownAddresses(this.#addresses); return this.#addresses; } } @@ -96,61 +91,52 @@ type CreateWalletProps = { }; const createWallet = async (props: CreateWalletProps) => { - const { wallet } = await setupWallet({ - bip32Ed25519, - createKeyAgent: async (dependencies) => { - const groupedAddress: GroupedAddress = { - accountIndex: 0, - address, - index: 0, - networkId: Cardano.NetworkId.Testnet, - rewardAccount, - stakeKeyDerivationPath, - type: AddressType.External - }; - - if (props.asyncKeyAgent) return props.asyncKeyAgent; - - const asyncKeyAgent = await testAsyncKeyAgent([groupedAddress], dependencies); - asyncKeyAgent.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => { - const { - rewardsProvider, - utxoProvider, - chainHistoryProvider, - handleProvider, - networkInfoProvider, - connectionStatusTracker$ - } = props.providers; - const txSubmitProvider = mocks.mockTxSubmitProvider(); - const assetProvider = mocks.mockAssetProvider(); - const stakePoolProvider = createStubStakePoolProvider(); - - return new PersonalWallet( - { name, polling: props.pollingConfig }, - { - addressDiscovery: props?.addressDiscovery, - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider, - chainHistoryProvider, - connectionStatusTracker$, - handleProvider, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - stores: props.stores, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ); - }, - logger - }); - return wallet; + const groupedAddress: GroupedAddress = { + accountIndex: 0, + address, + index: 0, + networkId: Cardano.NetworkId.Testnet, + rewardAccount, + stakeKeyDerivationPath, + type: AddressType.External + }; + + const asyncKeyAgent = props.asyncKeyAgent || (await testAsyncKeyAgent()); + + const { + rewardsProvider, + utxoProvider, + chainHistoryProvider, + handleProvider, + networkInfoProvider, + connectionStatusTracker$ + } = props.providers; + const txSubmitProvider = mocks.mockTxSubmitProvider(); + const assetProvider = mocks.mockAssetProvider(); + const stakePoolProvider = createStubStakePoolProvider(); + + const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + bip32Account.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); + + return new PersonalWallet( + { name, polling: props.pollingConfig }, + { + addressDiscovery: props?.addressDiscovery, + assetProvider, + bip32Account, + chainHistoryProvider, + connectionStatusTracker$, + handleProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + stores: props.stores, + txSubmitProvider, + utxoProvider, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); }; const assertWalletProperties = async ( @@ -159,10 +145,14 @@ const assertWalletProperties = async ( expectedRewardsHistory = flatten([...mocks.rewardsHistory.values()]), expectHandles?: boolean ) => { - expect(wallet.addressManager).toBeTruthy(); + expect(wallet.bip32Account).toBeTruthy(); expect(wallet.witnesser).toBeTruthy(); // name expect(wallet.name).toBe(name); + // addresses$ + const addresses = await firstValueFrom(wallet.addresses$); + expect(addresses[0].address).toEqual(address); + expect(addresses[0].rewardAccount).toEqual(rewardAccount); // utxo const utxoAvailable = await firstValueFrom(wallet.utxo.available$); const utxoTotal = await firstValueFrom(wallet.utxo.total$); @@ -195,10 +185,6 @@ const assertWalletProperties = async ( expect(rewardAccounts[0].address).toBe(rewardAccount); expect(rewardAccounts[0].delegatee?.nextNextEpoch?.id).toEqual(expectedDelegateeId); expect(rewardAccounts[0].rewardBalance).toBe(mocks.rewardAccountBalance); - // addresses$ - const addresses = await firstValueFrom(wallet.addresses$); - expect(addresses[0].address).toEqual(address); - expect(addresses[0].rewardAccount).toEqual(rewardAccount); // assets$ expect(await firstValueFrom(wallet.assetInfo$)).toEqual( new Map([ @@ -347,9 +333,10 @@ describe('PersonalWallet load', () => { const txsWithNoCertificates = queryTransactionsResult.pageResults.filter((tx) => !tx.body.certificates); chainHistoryProvider.transactionsByAddresses = jest.fn().mockResolvedValueOnce({ pageResults: txsWithNoCertificates, - totalResultCount: 1 + totalResultCount: txsWithNoCertificates.length }); const wallet = await createWallet({ + addressDiscovery: new SingleAddressDiscovery(), providers: { chainHistoryProvider, networkInfoProvider, @@ -358,7 +345,6 @@ describe('PersonalWallet load', () => { }, stores }); - // eslint-disable-next-line unicorn/no-useless-undefined await assertWalletProperties(wallet, undefined, []); await waitForWalletStateSettle(wallet); wallet.shutdown(); @@ -502,7 +488,6 @@ describe('PersonalWallet.AddressDiscovery', () => { const wallet = await createWallet({ addressDiscovery: new MockAddressDiscovery([testValue], 2), - asyncKeyAgent: prepareMockKeyAgentWithData(), providers: { chainHistoryProvider: mocks.mockChainHistoryProvider(), networkInfoProvider: mocks.mockNetworkInfoProvider(), diff --git a/packages/wallet/test/PersonalWallet/methods.test.ts b/packages/wallet/test/PersonalWallet/methods.test.ts index 116da5948e3..5c22de1f061 100644 --- a/packages/wallet/test/PersonalWallet/methods.test.ts +++ b/packages/wallet/test/PersonalWallet/methods.test.ts @@ -1,24 +1,27 @@ /* eslint-disable unicorn/consistent-destructuring, sonarjs/no-duplicate-string, @typescript-eslint/no-floating-promises, promise/no-nesting, promise/always-return */ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, AsyncKeyAgent, GroupedAddress, util } from '@cardano-sdk/key-management'; -import { AssetId, StubKeyAgent, createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; +import { AddressType, Bip32Account, GroupedAddress, Witnesser, util } from '@cardano-sdk/key-management'; +import { AssetId, createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; import { BehaviorSubject, Subscription, firstValueFrom, skip } from 'rxjs'; import { Cardano, + ChainHistoryProvider, + HandleProvider, ProviderError, ProviderFailure, + RewardsProvider, + StakePoolProvider, TxCBOR, TxSubmissionError, TxSubmissionErrorCode, ValueNotConservedData } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; -import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; -import { PersonalWallet, TxInFlight, setupWallet } from '../../src'; +import { InitializeTxProps } from '@cardano-sdk/tx-construction'; +import { PersonalWallet, TxInFlight } 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 delay from 'delay'; const { mockChainHistoryProvider, mockRewardsProvider, utxo } = mocks; @@ -59,18 +62,26 @@ describe('PersonalWallet methods', () => { const address = mocks.utxo[0][0].address!; let txSubmitProvider: mocks.TxSubmitProviderStub; let networkInfoProvider: mocks.NetworkInfoProviderStub; + let assetProvider: mocks.MockAssetProvider; + let stakePoolProvider: StakePoolProvider; + let rewardsProvider: RewardsProvider; + let chainHistoryProvider: ChainHistoryProvider; + let handleProvider: HandleProvider; let wallet: PersonalWallet; let utxoProvider: mocks.UtxoProviderStub; + let witnesser: Witnesser; + let bip32Account: Bip32Account; + beforeEach(async () => { txSubmitProvider = mocks.mockTxSubmitProvider(); networkInfoProvider = mocks.mockNetworkInfoProvider(); utxoProvider = mocks.mockUtxoProvider(); - const assetProvider = mocks.mockAssetProvider(); - const stakePoolProvider = createStubStakePoolProvider(); - const rewardsProvider = mockRewardsProvider(); - const chainHistoryProvider = mockChainHistoryProvider(); - const handleProvider = mocks.mockHandleProvider(); + assetProvider = mocks.mockAssetProvider(); + stakePoolProvider = createStubStakePoolProvider(); + rewardsProvider = mockRewardsProvider(); + chainHistoryProvider = mockChainHistoryProvider(); + handleProvider = mocks.mockHandleProvider(); const groupedAddress: GroupedAddress = { accountIndex: 0, address, @@ -80,32 +91,27 @@ describe('PersonalWallet methods', () => { stakeKeyDerivationPath, type: AddressType.External }; - ({ wallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => { - const asyncKeyAgent = await testAsyncKeyAgent([groupedAddress], dependencies); - asyncKeyAgent.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => - new PersonalWallet( - { name: 'Test Wallet' }, - { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider, - chainHistoryProvider, - handleProvider, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ), - logger - })); + const asyncKeyAgent = await testAsyncKeyAgent(); + bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + bip32Account.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); + witnesser = util.createBip32Ed25519Witnesser(asyncKeyAgent); + wallet = new PersonalWallet( + { name: 'Test Wallet' }, + { + assetProvider, + bip32Account, + chainHistoryProvider, + handleProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser + } + ); + await waitForWalletStateSettle(wallet); }); @@ -218,116 +224,6 @@ describe('PersonalWallet methods', () => { expect(tx.witness.signatures.size).toBe(2); // spending key and stake key for withdrawal }); - it('finalizeTx awaits for non-empty knownAddresses$', (done) => { - const inputResolver: Cardano.InputResolver = { - resolveInput: async (txIn) => - mocks.utxo.find( - ([hydratedTxIn]) => txIn.txId === hydratedTxIn.txId && txIn.index === hydratedTxIn.index - )?.[1] || null - }; - - const mockKeyAgent = new StubKeyAgent(inputResolver); - - setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async () => mockKeyAgent, - createWallet: async (keyAgent) => - new PersonalWallet( - { name: 'Stub Wallet' }, - { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider: mocks.mockAssetProvider(), - chainHistoryProvider: mockChainHistoryProvider(), - handleProvider: mocks.mockHandleProvider(), - logger, - networkInfoProvider: mocks.mockNetworkInfoProvider(), - rewardsProvider: mockRewardsProvider(), - stakePoolProvider: createStubStakePoolProvider(), - txSubmitProvider: mocks.mockTxSubmitProvider(), - utxoProvider: mocks.mockUtxoProvider(), - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ), - logger - }) - .then(({ wallet: stubWallet }) => { - // We need to manually build the TX, since initialize waits for the wallet to settle. The bug happens because - // callers (I.E cip30.ts) can call finalize directly since they already have the tx built. Bypassing initialize. - const txInternals = { - body: { - collaterals: [utxo[2][0]], - fee: 194_805n, - inputs: [utxo[1][0]], - mint: new Map([ - [AssetId.PXL, 5n], - [AssetId.TSLA, 20n] - ]), - outputs, - requiredExtraSignatures: [ - Crypto.Ed25519KeyHashHex('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed5') - ], - scriptIntegrityHash: Crypto.Hash32ByteBase16( - '3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d' - ), - validityInterval: { invalidHereafter: 37_841_696 }, - withdrawals: [ - { - quantity: 33_333n, - stakeAddress: Cardano.RewardAccount( - 'stake_test1up7pvfq8zn4quy45r2g572290p9vf99mr9tn7r9xrgy2l2qdsf58d' - ) - } - ] - }, - hash: Cardano.TransactionId('c3690ddfe8175bcbda4c8d30ba990da057ffd39317e926a9ee24e1272f20fe9c'), - inputSelection: { - change: [ - { - address: Cardano.PaymentAddress( - 'addr_test1qq585l3hyxgj3nas2v3xymd23vvartfhceme6gv98aaeg9muzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475q2g7k3g' - ), - value: { coins: 1_596_110n } - } - ], - fee: 194_805n, - inputs: new Set(), - outputs: [] - } - } as unknown as InitializeTxResult; - - // Finalize the TX first. Should only resolve if the key agent eventually discover some addresses. - // eslint-disable-next-line promise/catch-or-return - stubWallet - .finalizeTx({ tx: txInternals }) - .then((tx) => { - expect(tx.body).toBe(txInternals.body); - expect(tx.id).toBe(txInternals.hash); - expect(tx.witness.signatures.size).toBe(1); - - // eslint-disable-next-line promise/no-callback-in-promise - done(); - }) - .catch(); - - Promise.all([ - delay(1), - mockKeyAgent.setKnownAddresses([ - { - accountIndex: 0, - address: mocks.utxo[0][1].address, - index: 0, - networkId: Cardano.NetworkId.Testnet, - rewardAccount: mocks.rewardAccount, - type: AddressType.External - } - ]) - ]).catch(); - - return wallet; - }) - .catch(); - }); - describe('submitTx', () => { const valueNotConservedError = new ProviderError( ProviderFailure.BadRequest, @@ -542,39 +438,31 @@ describe('PersonalWallet methods', () => { }); it('will retry deriving pubDrepKey if one does not exist', async () => { - let walletKeyAgent: AsyncKeyAgent; - ({ wallet, keyAgent: walletKeyAgent } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => { - const asyncKeyAgent = await testAsyncKeyAgent([], dependencies); - asyncKeyAgent.derivePublicKey = jest.fn().mockRejectedValueOnce('error').mockResolvedValue('string'); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => - new PersonalWallet( - { name: 'Test Wallet' }, - { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider: mocks.mockAssetProvider(), - chainHistoryProvider: mockChainHistoryProvider(), - logger, - networkInfoProvider: mocks.mockNetworkInfoProvider(), - rewardsProvider: mockRewardsProvider(), - stakePoolProvider: mocks.mockStakePoolsProvider(), - txSubmitProvider: mocks.mockTxSubmitProvider(), - utxoProvider: mocks.mockUtxoProvider(), - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ), - logger - })); + wallet.shutdown(); + bip32Account.derivePublicKey = jest + .fn() + .mockRejectedValueOnce('error') + .mockResolvedValue({ hex: () => 'string' }); + wallet = new PersonalWallet( + { name: 'Test Wallet' }, + { + assetProvider, + bip32Account, + chainHistoryProvider, + handleProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser + } + ); await waitForWalletStateSettle(wallet); const response = await wallet.getPubDRepKey(); - expect(typeof response).toBe('string'); - expect(walletKeyAgent.derivePublicKey).toHaveBeenCalledTimes(3); - - wallet.shutdown(); - walletKeyAgent.shutdown(); + expect(response).toBe('string'); + expect(bip32Account.derivePublicKey).toHaveBeenCalledTimes(3); }); }); diff --git a/packages/wallet/test/PersonalWallet/rollback.test.ts b/packages/wallet/test/PersonalWallet/rollback.test.ts index f94ebdcf7b7..d10d5701b30 100644 --- a/packages/wallet/test/PersonalWallet/rollback.test.ts +++ b/packages/wallet/test/PersonalWallet/rollback.test.ts @@ -1,5 +1,5 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AddressType, Bip32Account, GroupedAddress, util } from '@cardano-sdk/key-management'; import { Cardano, ChainHistoryProvider, @@ -8,7 +8,7 @@ import { TxSubmitProvider, UtxoProvider } from '@cardano-sdk/core'; -import { ConnectionStatusTracker, PersonalWallet, PollingConfig, setupWallet } from '../../src'; +import { ConnectionStatusTracker, PersonalWallet, PollingConfig, SingleAddressDiscovery } from '../../src'; import { WalletStores, createInMemoryWalletStores } from '../../src/persistence'; import { createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; import { filter, firstValueFrom } from 'rxjs'; @@ -30,55 +30,47 @@ interface Providers { } const createWallet = async (stores: WalletStores, providers: Providers, pollingConfig?: PollingConfig) => { - const { wallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => { - const groupedAddress: GroupedAddress = { - accountIndex: 0, - address, - index: 0, - networkId: Cardano.NetworkId.Testnet, - rewardAccount, - stakeKeyDerivationPath, - type: AddressType.External - }; - const asyncKeyAgent = await testAsyncKeyAgent([groupedAddress], dependencies); - asyncKeyAgent.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => { - const { - txSubmitProvider, - rewardsProvider, - utxoProvider, - chainHistoryProvider, - networkInfoProvider, - connectionStatusTracker$ - } = providers; - const assetProvider = mocks.mockAssetProvider(); - const stakePoolProvider = createStubStakePoolProvider(); - - return new PersonalWallet( - { name, polling: pollingConfig }, - { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider, - chainHistoryProvider, - connectionStatusTracker$, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - stores, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ); - }, - logger - }); - return wallet; + const groupedAddress: GroupedAddress = { + accountIndex: 0, + address, + index: 0, + networkId: Cardano.NetworkId.Testnet, + rewardAccount, + stakeKeyDerivationPath, + type: AddressType.External + }; + const asyncKeyAgent = await testAsyncKeyAgent(); + const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + bip32Account.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); + const { + txSubmitProvider, + rewardsProvider, + utxoProvider, + chainHistoryProvider, + networkInfoProvider, + connectionStatusTracker$ + } = providers; + const assetProvider = mocks.mockAssetProvider(); + const stakePoolProvider = createStubStakePoolProvider(); + + return new PersonalWallet( + { name, polling: pollingConfig }, + { + addressDiscovery: new SingleAddressDiscovery(), + assetProvider, + bip32Account, + chainHistoryProvider, + connectionStatusTracker$, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + stores, + txSubmitProvider, + utxoProvider, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); }; const txOut: Cardano.TxOut = { diff --git a/packages/wallet/test/PersonalWallet/shutdown.test.ts b/packages/wallet/test/PersonalWallet/shutdown.test.ts index 4e9b96750f2..f20fd71199b 100644 --- a/packages/wallet/test/PersonalWallet/shutdown.test.ts +++ b/packages/wallet/test/PersonalWallet/shutdown.test.ts @@ -1,7 +1,6 @@ /* eslint-disable max-statements */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AddressType, Bip32Account, GroupedAddress, util } from '@cardano-sdk/key-management'; import { AssetId, createStubStakePoolProvider, @@ -21,8 +20,7 @@ import { PersonalWallet, PollingConfig, TxSubmitProviderStats, - WalletNetworkInfoProviderStats, - setupWallet + WalletNetworkInfoProviderStats } from '../../src'; import { WalletStores, createInMemoryWalletStores } from '../../src/persistence'; import { firstValueFrom } from 'rxjs'; @@ -42,61 +40,42 @@ interface Providers { connectionStatusTracker$?: ConnectionStatusTracker; } -const createWallet = async ( - stores: WalletStores, - providers: Providers, - shutdownSpy?: () => void, - pollingConfig?: PollingConfig -) => { - const { wallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => { - const groupedAddress: GroupedAddress = { - accountIndex: 0, - address, - index: 0, - networkId: Cardano.NetworkId.Testnet, - rewardAccount, - stakeKeyDerivationPath, - type: AddressType.External - }; - const asyncKeyAgent = await testAsyncKeyAgent( - [groupedAddress], - dependencies, - testKeyAgent([groupedAddress], dependencies), - shutdownSpy - ); - asyncKeyAgent.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => { - const { rewardsProvider, utxoProvider, chainHistoryProvider, networkInfoProvider, connectionStatusTracker$ } = - providers; - const txSubmitProvider = mocks.mockTxSubmitProvider(); - const assetProvider = mocks.mockAssetProvider(); - const stakePoolProvider = createStubStakePoolProvider(); - - return new PersonalWallet( - { name, polling: pollingConfig }, - { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider, - chainHistoryProvider, - connectionStatusTracker$, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - stores, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ); - }, - logger - }); - return wallet; +const createWallet = async (stores: WalletStores, providers: Providers, pollingConfig?: PollingConfig) => { + const groupedAddress: GroupedAddress = { + accountIndex: 0, + address, + index: 0, + networkId: Cardano.NetworkId.Testnet, + rewardAccount, + stakeKeyDerivationPath, + type: AddressType.External + }; + const asyncKeyAgent = await testAsyncKeyAgent(undefined, testKeyAgent()); + const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + bip32Account.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); + const { rewardsProvider, utxoProvider, chainHistoryProvider, networkInfoProvider, connectionStatusTracker$ } = + providers; + const txSubmitProvider = mocks.mockTxSubmitProvider(); + const assetProvider = mocks.mockAssetProvider(); + const stakePoolProvider = createStubStakePoolProvider(); + + return new PersonalWallet( + { name, polling: pollingConfig }, + { + assetProvider, + bip32Account, + chainHistoryProvider, + connectionStatusTracker$, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + stores, + txSubmitProvider, + utxoProvider, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); }; const assertWalletProperties = async ( @@ -104,7 +83,7 @@ const assertWalletProperties = async ( expectedDelegateeId: Cardano.PoolId | undefined, expectedRewardsHistory = flatten([...mocks.rewardsHistory.values()]) ) => { - expect(wallet.addressManager).toBeTruthy(); + expect(wallet.bip32Account).toBeTruthy(); expect(wallet.witnesser).toBeTruthy(); // name expect(wallet.name).toBe(name); @@ -170,7 +149,6 @@ describe('PersonalWallet shutdown', () => { }); it('completes all wallet Subjects', async () => { - let isKeyAgentShutdown = false; let isCurrentEpoch$Completed = false; let tip$Completed = false; let eraSummaries$Completed = false; @@ -180,18 +158,12 @@ describe('PersonalWallet shutdown', () => { let assets$Completed = false; const stores = createInMemoryWalletStores(); - const wallet1 = await createWallet( - stores, - { - chainHistoryProvider: mocks.mockChainHistoryProvider(), - networkInfoProvider: mocks.mockNetworkInfoProvider(), - rewardsProvider: mocks.mockRewardsProvider(), - utxoProvider: mocks.mockUtxoProvider() - }, - () => { - isKeyAgentShutdown = true; - } - ); + const wallet1 = await createWallet(stores, { + chainHistoryProvider: mocks.mockChainHistoryProvider(), + networkInfoProvider: mocks.mockNetworkInfoProvider(), + rewardsProvider: mocks.mockRewardsProvider(), + utxoProvider: mocks.mockUtxoProvider() + }); // Verify all observables have completed. wallet1.currentEpoch$.subscribe({ @@ -264,7 +236,6 @@ describe('PersonalWallet shutdown', () => { expect(txSubmitProviderStats).toHaveBeenCalledTimes(1); expect(utxoProviderStats).toHaveBeenCalledTimes(1); expect(walletNetworkInfoProviderStats).toHaveBeenCalledTimes(1); - expect(isKeyAgentShutdown).toBeTruthy(); expect(isCurrentEpoch$Completed).toBeTruthy(); expect(tip$Completed).toBeTruthy(); expect(eraSummaries$Completed).toBeTruthy(); diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.integration.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.integration.test.ts index 298e7ba6a7d..33b2fac447d 100644 --- a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.integration.test.ts +++ b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.integration.test.ts @@ -1,9 +1,9 @@ import * as Crypto from '@cardano-sdk/crypto'; +import { Bip32Account, CommunicationType, KeyAgent, util } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; -import { CommunicationType, KeyAgent, util } from '@cardano-sdk/key-management'; import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger'; -import { ObservableWallet, PersonalWallet, restoreKeyAgent, setupWallet } from '../../../src'; +import { ObservableWallet, PersonalWallet, restoreKeyAgent } from '../../../src'; import { createStubStakePoolProvider, mockProviders } from '@cardano-sdk/util-dev'; import { firstValueFrom } from 'rxjs'; import { dummyLogger as logger } from 'ts-log'; @@ -29,8 +29,8 @@ const createWallet = async (keyAgent: KeyAgent) => { return new PersonalWallet( { name: 'Wallet1' }, { - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), assetProvider, + bip32Account: await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent), chainHistoryProvider, logger, networkInfoProvider, @@ -45,27 +45,22 @@ const createWallet = async (keyAgent: KeyAgent) => { const getAddress = async (wallet: ObservableWallet) => (await firstValueFrom(wallet.addresses$))[0].address; +const keyAgentDependencies = { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger }; + describe('LedgerKeyAgent+PersonalWallet', () => { test('creating and restoring LedgerKeyAgent wallet', async () => { - const { wallet: freshWallet, keyAgent: freshKeyAgent } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: (dependencies) => - LedgerKeyAgent.createWithDevice( - { - chainId: Cardano.ChainIds.Preprod, - communicationType: CommunicationType.Node - }, - dependencies - ), - createWallet, - logger - }); - const { wallet: restoredWallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: (dependencies) => restoreKeyAgent(freshKeyAgent.serializableData, dependencies), - createWallet, - logger - }); + const freshKeyAgent = await LedgerKeyAgent.createWithDevice( + { + chainId: Cardano.ChainIds.Preprod, + communicationType: CommunicationType.Node + }, + keyAgentDependencies + ); + const freshWallet = await createWallet(freshKeyAgent); + + const restoredKeyAgent = await restoreKeyAgent(freshKeyAgent.serializableData, keyAgentDependencies); + const restoredWallet = await createWallet(restoredKeyAgent); + expect(await getAddress(freshWallet)).toEqual(await getAddress(restoredWallet)); // TODO: finalizeTx with both wallets, assert that signature equals freshWallet.shutdown(); diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts index ba1a1a99efd..67ff04900a2 100644 --- a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts +++ b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts @@ -1,64 +1,61 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, CommunicationType, SerializableLedgerKeyAgentData, util } from '@cardano-sdk/key-management'; +import { + AddressType, + Bip32Account, + CommunicationType, + SerializableLedgerKeyAgentData, + util +} from '@cardano-sdk/key-management'; import { AssetId, createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; import { Cardano, Serialization } from '@cardano-sdk/core'; import { Hash32ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; import { LedgerKeyAgent, LedgerTransportType } from '@cardano-sdk/hardware-ledger'; -import { PersonalWallet, setupWallet } from '../../../src'; +import { PersonalWallet } from '../../../src'; +import { firstValueFrom } from 'rxjs'; import { dummyLogger as logger } from 'ts-log'; import { mockKeyAgentDependencies } from '../../../../key-management/test/mocks'; import DeviceConnection from '@cardano-foundation/ledgerjs-hw-app-cardano'; describe('LedgerKeyAgent', () => { - let keyAgent: LedgerKeyAgent; + let ledgerKeyAgent: LedgerKeyAgent; let txSubmitProvider: mocks.TxSubmitProviderStub; let wallet: PersonalWallet; beforeAll(async () => { txSubmitProvider = mocks.mockTxSubmitProvider(); - ({ keyAgent, wallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => - await LedgerKeyAgent.createWithDevice( - { - chainId: Cardano.ChainIds.Preprod, - communicationType: CommunicationType.Node - }, - dependencies - ), - createWallet: async (ledgerKeyAgent) => { - const { address, rewardAccount } = await ledgerKeyAgent.deriveAddress( - { index: 0, type: AddressType.External }, - 0 - ); - const assetProvider = mocks.mockAssetProvider(); - const stakePoolProvider = createStubStakePoolProvider(); - const networkInfoProvider = mocks.mockNetworkInfoProvider(); - const utxoProvider = mocks.mockUtxoProvider({ address }); - const rewardsProvider = mocks.mockRewardsProvider({ rewardAccount }); - const chainHistoryProvider = mocks.mockChainHistoryProvider({ rewardAccount }); - const asyncKeyAgent = util.createAsyncKeyAgent(ledgerKeyAgent); - return new PersonalWallet( - { name: 'HW Wallet' }, - { - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), - assetProvider, - chainHistoryProvider, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) - } - ); + ledgerKeyAgent = await LedgerKeyAgent.createWithDevice( + { + chainId: Cardano.ChainIds.Preprod, + communicationType: CommunicationType.Node }, - logger - })); + { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger } + ); + const { address, rewardAccount } = await ledgerKeyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); + const assetProvider = mocks.mockAssetProvider(); + const stakePoolProvider = createStubStakePoolProvider(); + const networkInfoProvider = mocks.mockNetworkInfoProvider(); + const utxoProvider = mocks.mockUtxoProvider({ address }); + const rewardsProvider = mocks.mockRewardsProvider({ rewardAccount }); + const chainHistoryProvider = mocks.mockChainHistoryProvider({ rewardAccount }); + const asyncKeyAgent = util.createAsyncKeyAgent(ledgerKeyAgent); + wallet = new PersonalWallet( + { name: 'HW Wallet' }, + { + assetProvider, + bip32Account: await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent), + chainHistoryProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); }); afterAll(() => wallet.shutdown()); @@ -69,33 +66,29 @@ describe('LedgerKeyAgent', () => { accountIndex: 5, chainId: Cardano.ChainIds.Preprod, communicationType: CommunicationType.Node, - deviceConnection: keyAgent.deviceConnection + deviceConnection: ledgerKeyAgent.deviceConnection }, mockKeyAgentDependencies() ); expect(ledgerKeyAgentWithRandomIndex).toBeInstanceOf(LedgerKeyAgent); expect(ledgerKeyAgentWithRandomIndex.accountIndex).toEqual(5); - expect(ledgerKeyAgentWithRandomIndex.extendedAccountPublicKey).not.toEqual(keyAgent.extendedAccountPublicKey); + expect(ledgerKeyAgentWithRandomIndex.extendedAccountPublicKey).not.toEqual(ledgerKeyAgent.extendedAccountPublicKey); }); test('__typename', () => { - expect(typeof keyAgent.serializableData.__typename).toBe('string'); + expect(typeof ledgerKeyAgent.serializableData.__typename).toBe('string'); }); test('chainId', () => { - expect(keyAgent.chainId).toBe(Cardano.ChainIds.Preprod); + expect(ledgerKeyAgent.chainId).toBe(Cardano.ChainIds.Preprod); }); test('accountIndex', () => { - expect(typeof keyAgent.accountIndex).toBe('number'); - }); - - test('knownAddresses', () => { - expect(Array.isArray(keyAgent.knownAddresses)).toBe(true); + expect(typeof ledgerKeyAgent.accountIndex).toBe('number'); }); test('extendedAccountPublicKey', () => { - expect(typeof keyAgent.extendedAccountPublicKey).toBe('string'); + expect(typeof ledgerKeyAgent.extendedAccountPublicKey).toBe('string'); }); describe('signTransaction', () => { @@ -135,16 +128,21 @@ describe('LedgerKeyAgent', () => { }); it('successfully signs a transaction with assets and validity interval', async () => { - const signatures = await keyAgent.signTransaction(txInternals); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); it('throws if signed transaction hash doesnt match hash computed by the wallet', async () => { await expect( - keyAgent.signTransaction({ - ...txInternals, - hash: 'non-matching' as unknown as Cardano.TransactionId - }) + ledgerKeyAgent.signTransaction( + { + ...txInternals, + hash: 'non-matching' as unknown as Cardano.TransactionId + }, + { knownAddresses: await firstValueFrom(wallet.addresses$), txInKeyPathMap: {} } + ) ).rejects.toThrow(); }); @@ -215,7 +213,9 @@ describe('LedgerKeyAgent', () => { const unsignedTx = await wallet.initializeTx(txProps); - const signatures = await keyAgent.signTransaction(unsignedTx); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: unsignedTx }); expect(signatures.size).toBe(2); }); }); @@ -225,8 +225,8 @@ describe('LedgerKeyAgent', () => { beforeAll(async () => { transportSpy = jest.spyOn(LedgerKeyAgent, 'createTransport'); - if (keyAgent.deviceConnection) { - await keyAgent.deviceConnection.transport.close(); + if (ledgerKeyAgent.deviceConnection) { + await ledgerKeyAgent.deviceConnection.transport.close(); LedgerKeyAgent.deviceConnections = []; } }); @@ -272,8 +272,8 @@ describe('LedgerKeyAgent', () => { describe('establish, check and re-establish device connection', () => { let deviceConnection: DeviceConnection; beforeAll(async () => { - if (keyAgent.deviceConnection) { - await keyAgent.deviceConnection.transport.close(); + if (ledgerKeyAgent.deviceConnection) { + await ledgerKeyAgent.deviceConnection.transport.close(); } deviceConnection = await LedgerKeyAgent.establishDeviceConnection(CommunicationType.Node); }); @@ -319,14 +319,13 @@ describe('LedgerKeyAgent', () => { let serializableData: SerializableLedgerKeyAgentData; beforeEach(() => { - serializableData = keyAgent.serializableData as SerializableLedgerKeyAgentData; + serializableData = ledgerKeyAgent.serializableData as SerializableLedgerKeyAgentData; }); it('all fields are of correct types', () => { expect(typeof serializableData.__typename).toBe('string'); expect(typeof serializableData.accountIndex).toBe('number'); expect(typeof serializableData.chainId).toBe('object'); - expect(Array.isArray(serializableData.knownAddresses)).toBe(true); expect(typeof serializableData.extendedAccountPublicKey).toBe('string'); expect(typeof serializableData.communicationType).toBe('string'); }); diff --git a/packages/wallet/test/hardware/trezor/TrezorKeyAgent.integration.test.ts b/packages/wallet/test/hardware/trezor/TrezorKeyAgent.integration.test.ts index 83b197fd3dd..8c56d40a8ae 100644 --- a/packages/wallet/test/hardware/trezor/TrezorKeyAgent.integration.test.ts +++ b/packages/wallet/test/hardware/trezor/TrezorKeyAgent.integration.test.ts @@ -1,7 +1,7 @@ import * as Crypto from '@cardano-sdk/crypto'; +import { Bip32Account, CommunicationType, KeyAgent, util } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; -import { CommunicationType, KeyAgent, util } from '@cardano-sdk/key-management'; -import { ObservableWallet, PersonalWallet, restoreKeyAgent, setupWallet } from '../../../src'; +import { ObservableWallet, PersonalWallet, restoreKeyAgent } from '../../../src'; import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor'; import { createStubStakePoolProvider, mockProviders } from '@cardano-sdk/util-dev'; import { firstValueFrom } from 'rxjs'; @@ -16,6 +16,8 @@ const { mockUtxoProvider } = mockProviders; +const keyAgentDependencies = { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger }; + const createWallet = async (keyAgent: KeyAgent) => { const txSubmitProvider = mockTxSubmitProvider(); const stakePoolProvider = createStubStakePoolProvider(); @@ -28,8 +30,8 @@ const createWallet = async (keyAgent: KeyAgent) => { return new PersonalWallet( { name: 'Wallet1' }, { - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), assetProvider, + bip32Account: await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent), chainHistoryProvider, logger, networkInfoProvider, @@ -46,31 +48,23 @@ const getAddress = async (wallet: ObservableWallet) => (await firstValueFrom(wal describe('TrezorKeyAgent+PersonalWallet', () => { test('creating and restoring TrezorKeyAgent wallet', async () => { - const { wallet: freshWallet, keyAgent: freshKeyAgent } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: (dependencies) => - TrezorKeyAgent.createWithDevice( - { - chainId: Cardano.ChainIds.Preprod, - trezorConfig: { - communicationType: CommunicationType.Node, - manifest: { - appUrl: 'https://your.application.com', - email: 'email@developer.com' - } - } - }, - dependencies - ), - createWallet, - logger - }); - const { wallet: restoredWallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: (dependencies) => restoreKeyAgent(freshKeyAgent.serializableData, dependencies), - createWallet, - logger - }); + const freshKeyAgent = await TrezorKeyAgent.createWithDevice( + { + chainId: Cardano.ChainIds.Preprod, + trezorConfig: { + communicationType: CommunicationType.Node, + manifest: { + appUrl: 'https://your.application.com', + email: 'email@developer.com' + } + } + }, + keyAgentDependencies + ); + const freshWallet = await createWallet(freshKeyAgent); + + const restoredKeyAgent = await restoreKeyAgent(freshKeyAgent.serializableData, keyAgentDependencies); + const restoredWallet = await createWallet(restoredKeyAgent); expect(await getAddress(freshWallet)).toEqual(await getAddress(restoredWallet)); freshWallet.shutdown(); diff --git a/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts b/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts index 9ac8a774638..f05266513fe 100644 --- a/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts +++ b/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts @@ -1,19 +1,27 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, CommunicationType, SerializableTrezorKeyAgentData, util } from '@cardano-sdk/key-management'; +import { + AddressType, + Bip32Account, + CommunicationType, + SerializableTrezorKeyAgentData, + util +} from '@cardano-sdk/key-management'; import { AssetId, createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; import { Cardano, Serialization } from '@cardano-sdk/core'; import { Hash32ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; -import { PersonalWallet, setupWallet } from '../../../src'; +import { PersonalWallet } from '../../../src'; import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor'; +import { firstValueFrom } from 'rxjs'; import { dummyLogger as logger } from 'ts-log'; import { mockKeyAgentDependencies } from '../../../../key-management/test/mocks'; describe('TrezorKeyAgent', () => { let wallet: PersonalWallet; - let keyAgent: TrezorKeyAgent; + let trezorKeyAgent: TrezorKeyAgent; let txSubmitProvider: mocks.TxSubmitProviderStub; + let address: Cardano.PaymentAddress; const trezorConfig = { communicationType: CommunicationType.Node, @@ -25,46 +33,38 @@ describe('TrezorKeyAgent', () => { beforeAll(async () => { txSubmitProvider = mocks.mockTxSubmitProvider(); - ({ keyAgent, wallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => - await TrezorKeyAgent.createWithDevice( - { - chainId: Cardano.ChainIds.Preprod, - trezorConfig - }, - dependencies - ), - createWallet: async (trezorKeyAgent) => { - const { address, rewardAccount } = await trezorKeyAgent.deriveAddress( - { index: 0, type: AddressType.External }, - 0 - ); - const assetProvider = mocks.mockAssetProvider(); - const stakePoolProvider = createStubStakePoolProvider(); - const networkInfoProvider = mocks.mockNetworkInfoProvider(); - const utxoProvider = mocks.mockUtxoProvider({ address }); - const rewardsProvider = mocks.mockRewardsProvider({ rewardAccount }); - const chainHistoryProvider = mocks.mockChainHistoryProvider({ rewardAccount }); - const asyncKeyAgent = util.createAsyncKeyAgent(trezorKeyAgent); - return new PersonalWallet( - { name: 'HW Wallet' }, - { - addressManager: util.createBip32Ed25519AddressManager(asyncKeyAgent), - assetProvider, - chainHistoryProvider, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) - } - ); + trezorKeyAgent = await TrezorKeyAgent.createWithDevice( + { + chainId: Cardano.ChainIds.Preprod, + trezorConfig }, - logger - })); + { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), logger } + ); + const groupedAddress = await trezorKeyAgent.deriveAddress({ index: 0, type: AddressType.External }, 0); + address = groupedAddress.address; + const rewardAccount = groupedAddress.rewardAccount; + const assetProvider = mocks.mockAssetProvider(); + const stakePoolProvider = createStubStakePoolProvider(); + const networkInfoProvider = mocks.mockNetworkInfoProvider(); + const utxoProvider = mocks.mockUtxoProvider({ address }); + const rewardsProvider = mocks.mockRewardsProvider({ rewardAccount }); + const chainHistoryProvider = mocks.mockChainHistoryProvider({ rewardAccount }); + const asyncKeyAgent = util.createAsyncKeyAgent(trezorKeyAgent); + wallet = new PersonalWallet( + { name: 'HW Wallet' }, + { + assetProvider, + bip32Account: await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent), + chainHistoryProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); }); afterAll(() => wallet.shutdown()); @@ -127,20 +127,23 @@ describe('TrezorKeyAgent', () => { value: { coins: 11_111_111n } } }; - + let rewardAccount: Cardano.RewardAccount; let props: InitializeTxProps; let txInternals: InitializeTxResult; + beforeEach(async () => { + rewardAccount = (await firstValueFrom(wallet.addresses$))[0].rewardAccount; + }); + it('successfully signs simple transaction', async () => { props = { outputs: new Set([outputs.simpleOutput]) }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -150,10 +153,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -164,10 +166,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -183,10 +184,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -199,15 +199,13 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); it('successfully signs stake registration and delegation transaction', async () => { - const rewardAccount = keyAgent.knownAddresses[0].rewardAccount; const stakeCredential = { hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(Cardano.RewardAccount.toHash(rewardAccount)), type: Cardano.CredentialType.KeyHash @@ -229,15 +227,13 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); it('successfully signs stake deregistration transaction', async () => { - const rewardAccount = keyAgent.knownAddresses[0].rewardAccount; const stakeCredential = { hash: Crypto.Hash28ByteBase16.fromEd25519KeyHashHex(Cardano.RewardAccount.toHash(rewardAccount)), type: Cardano.CredentialType.KeyHash @@ -254,15 +250,13 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); it.skip('successfully signs pool registration transaction', async () => { - const rewardAccount = keyAgent.knownAddresses[0].rewardAccount; const poolRewardAcc = Cardano.RewardAccount('stake_test1uqfu74w3wh4gfzu8m6e7j987h4lq9r3t7ef5gaw497uu85qsqfy27'); const metadataJson = { hash: Crypto.Hash32ByteBase16('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5'), @@ -289,19 +283,21 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); it('throws if signed transaction hash doesnt match hash computed by the wallet', async () => { await expect( - keyAgent.signTransaction({ - body: txInternals.body, - hash: 'non-matching' as unknown as Cardano.TransactionId - }) + trezorKeyAgent.signTransaction( + { + body: txInternals.body, + hash: 'non-matching' as unknown as Cardano.TransactionId + }, + { knownAddresses: await firstValueFrom(wallet.addresses$), txInKeyPathMap: {} } + ) ).rejects.toThrow(); }); @@ -311,10 +307,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -324,10 +319,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -337,10 +331,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -351,10 +344,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -384,10 +376,9 @@ describe('TrezorKeyAgent', () => { txInternals = await wallet.initializeTx(txProps); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); @@ -397,9 +388,7 @@ describe('TrezorKeyAgent', () => { Crypto.Ed25519KeyHashHex('9ab1e9d2346c3f4be360d22b8ee7756a0316c3c1aece473e2887ea97') ]; // Add known payment address if exists -> keyPath (acc0) - const knownAddressPaymentCredential = Cardano.Address.fromBech32(keyAgent.knownAddresses[0].address) - ?.asBase() - ?.getPaymentCredential().hash; + const knownAddressPaymentCredential = Cardano.Address.fromBech32(address)?.asBase()?.getPaymentCredential().hash; if (knownAddressPaymentCredential) requiredExtraSignatures.push(Crypto.Ed25519KeyHashHex(knownAddressPaymentCredential)); @@ -409,10 +398,9 @@ describe('TrezorKeyAgent', () => { }; txInternals = await wallet.initializeTx(props); - const signatures = await keyAgent.signTransaction({ - body: txInternals.body, - hash: txInternals.hash - }); + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: txInternals }); expect(signatures.size).toBe(2); }); }); @@ -428,34 +416,30 @@ describe('TrezorKeyAgent', () => { ); expect(trezorKeyAgentWithRandomIndex).toBeInstanceOf(TrezorKeyAgent); expect(trezorKeyAgentWithRandomIndex.accountIndex).toEqual(5); - expect(trezorKeyAgentWithRandomIndex.extendedAccountPublicKey).not.toEqual(keyAgent.extendedAccountPublicKey); + expect(trezorKeyAgentWithRandomIndex.extendedAccountPublicKey).not.toEqual(trezorKeyAgent.extendedAccountPublicKey); }); test('__typename', () => { - expect(typeof keyAgent.serializableData.__typename).toBe('string'); + expect(typeof trezorKeyAgent.serializableData.__typename).toBe('string'); }); test('chainId', () => { - expect(keyAgent.chainId).toBe(Cardano.ChainIds.Preprod); + expect(trezorKeyAgent.chainId).toBe(Cardano.ChainIds.Preprod); }); test('accountIndex', () => { - expect(typeof keyAgent.accountIndex).toBe('number'); - }); - - test('knownAddresses', () => { - expect(Array.isArray(keyAgent.knownAddresses)).toBe(true); + expect(typeof trezorKeyAgent.accountIndex).toBe('number'); }); test('extendedAccountPublicKey', () => { - expect(typeof keyAgent.extendedAccountPublicKey).toBe('string'); + expect(typeof trezorKeyAgent.extendedAccountPublicKey).toBe('string'); }); describe('serializableData', () => { let serializableData: SerializableTrezorKeyAgentData; beforeEach(() => { - serializableData = keyAgent.serializableData as SerializableTrezorKeyAgentData; + serializableData = trezorKeyAgent.serializableData as SerializableTrezorKeyAgentData; }); it('all fields are of correct types', () => { @@ -463,7 +447,6 @@ describe('TrezorKeyAgent', () => { expect(typeof serializableData.accountIndex).toBe('number'); expect(typeof serializableData.chainId).toBe('object'); expect(typeof serializableData.extendedAccountPublicKey).toBe('string'); - expect(Array.isArray(serializableData.knownAddresses)).toBe(true); }); it('is serializable', () => { diff --git a/packages/wallet/test/integration/CustomObservableWallet.test.ts b/packages/wallet/test/integration/CustomObservableWallet.test.ts index ce0be7cc4af..999dda76ea3 100644 --- a/packages/wallet/test/integration/CustomObservableWallet.test.ts +++ b/packages/wallet/test/integration/CustomObservableWallet.test.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-unused-expressions */ /* eslint-disable sonarjs/no-extra-arguments */ /* eslint-disable unicorn/consistent-function-scoping */ +import { Bip32Account, GroupedAddress, util } from '@cardano-sdk/key-management'; import { Cardano, Serialization } from '@cardano-sdk/core'; -import { GroupedAddress, util } from '@cardano-sdk/key-management'; import { ObservableWallet, PersonalWallet } from '../../src'; import { OutputValidator, @@ -43,8 +43,8 @@ describe('CustomObservableWallet', () => { const extensionWallet: LaceObservableWallet = new PersonalWallet( { name: 'Extension Wallet' }, { - addressManager: util.createBip32Ed25519AddressManager(await testAsyncKeyAgent()), assetProvider: mocks.mockAssetProvider(), + bip32Account: await Bip32Account.fromAsyncKeyAgent(await testAsyncKeyAgent()), chainHistoryProvider: mocks.mockChainHistoryProvider(), logger, networkInfoProvider: mocks.mockNetworkInfoProvider(), diff --git a/packages/wallet/test/integration/cip30mapping.test.ts b/packages/wallet/test/integration/cip30mapping.test.ts index 86736997536..c8c5de52a2f 100644 --- a/packages/wallet/test/integration/cip30mapping.test.ts +++ b/packages/wallet/test/integration/cip30mapping.test.ts @@ -12,7 +12,7 @@ import { TxSignError, WalletApi } from '@cardano-sdk/dapp-connector'; -import { AddressType, GroupedAddress, util } from '@cardano-sdk/key-management'; +import { AddressType, Bip32Account, GroupedAddress, util } from '@cardano-sdk/key-management'; import { AssetId, createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; import { CallbackConfirmation, GetCollateralCallbackParams } from '../../src/cip30'; import { @@ -27,7 +27,7 @@ import { import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util'; import { InMemoryUnspendableUtxoStore, createInMemoryWalletStores } from '../../src/persistence'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; -import { PersonalWallet, cip30, setupWallet } from '../../src'; +import { PersonalWallet, cip30 } from '../../src'; import { Providers, createWallet } from './util'; import { buildDRepIDFromDRepKey, waitForWalletStateSettle } from '../util'; import { firstValueFrom, of } from 'rxjs'; @@ -576,33 +576,38 @@ describe('cip30', () => { }); describe('confirmation callbacks', () => { + let address: Cardano.PaymentAddress; + + beforeEach(async () => { + address = (await firstValueFrom(wallet.addresses$))[0].address; + }); + describe('signData', () => { const payload = 'abc123'; test('resolves true', async () => { confirmationCallback.signData = jest.fn().mockResolvedValueOnce(true); - await expect(api.signData(wallet.addresses$.value![0].address, payload)).resolves.not.toThrow(); + await expect(api.signData(address, payload)).resolves.not.toThrow(); }); test('resolves false', async () => { confirmationCallback.signData = jest.fn().mockResolvedValueOnce(false); - await expect(api.signData(wallet.addresses$.value![0].address, payload)).rejects.toThrowError(DataSignError); + await expect(api.signData(address, payload)).rejects.toThrowError(DataSignError); }); test('rejects', async () => { confirmationCallback.signData = jest.fn().mockRejectedValue(1); - await expect(api.signData(wallet.addresses$.value![0].address, payload)).rejects.toThrowError(DataSignError); + await expect(api.signData(address, payload)).rejects.toThrowError(DataSignError); }); test('gets the Cardano.Address equivalent of the hex address', async () => { confirmationCallback.signData = jest.fn().mockResolvedValueOnce(true); - const expectedAddr = wallet.addresses$.value![0].address; - const hexAddr = Cardano.Address.fromBech32(expectedAddr).toBytes(); + const hexAddr = Cardano.Address.fromBech32(address).toBytes(); await api.signData(hexAddr, payload); expect(confirmationCallback.signData).toHaveBeenCalledWith( - expect.objectContaining({ data: expect.objectContaining({ addr: expectedAddr }) }) + expect.objectContaining({ data: expect.objectContaining({ addr: address }) }) ); }); }); @@ -685,31 +690,24 @@ describe('cip30', () => { stakeKeyDerivationPath, type: AddressType.External }; - ({ wallet: mockWallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => { - const asyncKeyAgent = await testAsyncKeyAgent([groupedAddress], dependencies); - asyncKeyAgent.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => - new PersonalWallet( - { name: 'Test Wallet' }, - { - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - assetProvider, - chainHistoryProvider, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - txSubmitProvider, - utxoProvider, - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ), - logger - })); + const asyncKeyAgent = await testAsyncKeyAgent(); + const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + bip32Account.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); + mockWallet = new PersonalWallet( + { name: 'Test Wallet' }, + { + assetProvider, + bip32Account, + chainHistoryProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser: util.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); await waitForWalletStateSettle(mockWallet); mockApi = cip30.createWalletApi( diff --git a/packages/wallet/test/integration/util.ts b/packages/wallet/test/integration/util.ts index 8a74b347ca4..c5da09f349b 100644 --- a/packages/wallet/test/integration/util.ts +++ b/packages/wallet/test/integration/util.ts @@ -1,10 +1,9 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { PersonalWallet, setupWallet } from '../../src'; +import { Bip32Account, util } from '@cardano-sdk/key-management'; +import { PersonalWallet } from '../../src'; import { WalletStores } from '../../src/persistence'; import { createStubStakePoolProvider, mockProviders } from '@cardano-sdk/util-dev'; import { dummyLogger as logger } from 'ts-log'; import { testAsyncKeyAgent } from '../../../key-management/test/mocks'; -import { util } from '@cardano-sdk/key-management'; const { mockAssetProvider, @@ -30,21 +29,18 @@ export type Providers = { [k in keyof RequiredProviders]?: RequiredProviders[k]; }; -export const createWallet = async (stores?: WalletStores, providers: Providers = {}) => - await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: (dependencies) => testAsyncKeyAgent(undefined, dependencies), - createWallet: async (keyAgent) => - new PersonalWallet( - { name: 'Test Wallet' }, - { - ...createDefaultProviders(), - ...providers, - addressManager: util.createBip32Ed25519AddressManager(keyAgent), - logger, - stores, - witnesser: util.createBip32Ed25519Witnesser(keyAgent) - } - ), - logger - }); +export const createWallet = async (stores?: WalletStores, providers: Providers = {}) => { + const keyAgent = await testAsyncKeyAgent(); + const wallet = new PersonalWallet( + { name: 'Test Wallet' }, + { + ...createDefaultProviders(), + ...providers, + bip32Account: await Bip32Account.fromAsyncKeyAgent(keyAgent), + logger, + stores, + witnesser: util.createBip32Ed25519Witnesser(keyAgent) + } + ); + return { keyAgent, wallet }; +}; diff --git a/packages/wallet/test/services/AddressTracker.test.ts b/packages/wallet/test/services/AddressTracker.test.ts new file mode 100644 index 00000000000..5d2ed03dee2 --- /dev/null +++ b/packages/wallet/test/services/AddressTracker.test.ts @@ -0,0 +1,83 @@ +import { AddressTracker, createAddressTracker } from '../../src'; +import { Cardano } from '@cardano-sdk/core'; +import { EMPTY, firstValueFrom, of } from 'rxjs'; +import { GroupedAddress } from '@cardano-sdk/key-management'; +import { WalletStores } from '../../src/persistence'; +import { createTestScheduler, logger } from '@cardano-sdk/util-dev'; + +describe('AddressTracker', () => { + let store: jest.Mocked; + let addressTracker: AddressTracker; + const discoveredAddresses = [{ address: 'addr1' as Cardano.PaymentAddress } as GroupedAddress]; + + beforeEach(() => { + store = { + destroy: jest.fn(), + destroyed: false, + get: jest.fn(() => EMPTY), + set: jest.fn((_: GroupedAddress[]) => of(void 0)) + }; + }); + + afterEach(() => addressTracker.shutdown()); + + describe('load', () => { + describe('no addresses are stored', () => { + it('subscribes to addressDiscovery$, stores discovered addresses and emits from addresses$', () => { + createTestScheduler().run(({ cold, expectObservable, expectSubscriptions, flush }) => { + const addressDiscovery$ = cold('a', { a: discoveredAddresses }); + addressTracker = createAddressTracker({ + addressDiscovery$, + logger, + store + }); + expectObservable(addressTracker.addresses$).toBe('a', { a: discoveredAddresses }); + expectSubscriptions(addressDiscovery$.subscriptions).toBe('^'); + flush(); + expect(store.get).toBeCalledTimes(1); + expect(store.set).toBeCalledTimes(1); + expect(store.set).toBeCalledWith(discoveredAddresses); + }); + }); + }); + + describe('some address(-es) are stored', () => { + it('emits stored addresses and does not subscribe to addressDiscovery$', () => { + createTestScheduler().run(({ cold, expectObservable, expectSubscriptions, flush }) => { + const storedAddresses = [{ address: 'addrstored' as Cardano.PaymentAddress } as GroupedAddress]; + store.get.mockReturnValueOnce(cold('a', { a: storedAddresses })); + const addressDiscovery$ = cold('|'); + addressTracker = createAddressTracker({ + addressDiscovery$, + logger, + store + }); + expectObservable(addressTracker.addresses$).toBe('a', { a: storedAddresses }); + expectSubscriptions(addressDiscovery$.subscriptions).toBe(''); + flush(); + expect(store.get).toBeCalledTimes(1); + expect(store.set).not.toBeCalled(); + }); + }); + }); + }); + + describe('addAddresses', () => { + it('stores new addresses and emits from addresses$', async () => { + const addressDiscovery$ = of(discoveredAddresses); + addressTracker = createAddressTracker({ + addressDiscovery$, + logger, + store + }); + await expect(firstValueFrom(addressTracker.addresses$)).resolves.toEqual(discoveredAddresses); + const newAddress = { address: 'addr2' as Cardano.PaymentAddress } as GroupedAddress; + const combinedAddresses = [...discoveredAddresses, newAddress]; + + await expect(firstValueFrom(addressTracker.addAddresses([newAddress]))).resolves.toEqual(combinedAddresses); + await expect(firstValueFrom(addressTracker.addresses$)).resolves.toEqual(combinedAddresses); + expect(store.set).toBeCalledTimes(2); + expect(store.set).toBeCalledWith(combinedAddresses); + }); + }); +}); diff --git a/packages/wallet/test/services/KeyAgent/restoreKeyAgent.test.ts b/packages/wallet/test/services/KeyAgent/restoreKeyAgent.test.ts index 6ec4ca22aa0..94c046608c6 100644 --- a/packages/wallet/test/services/KeyAgent/restoreKeyAgent.test.ts +++ b/packages/wallet/test/services/KeyAgent/restoreKeyAgent.test.ts @@ -1,6 +1,6 @@ import * as Crypto from '@cardano-sdk/crypto'; +import { Cardano } from '@cardano-sdk/core'; import { - AddressType, CommunicationType, GetPassphrase, KeyAgentDependencies, @@ -8,10 +8,8 @@ import { SerializableInMemoryKeyAgentData, SerializableLedgerKeyAgentData, SerializableTrezorKeyAgentData, - errors, - util + errors } from '@cardano-sdk/key-management'; -import { Cardano } from '@cardano-sdk/core'; import { dummyLogger } from 'ts-log'; import { restoreKeyAgent } from '../../../src'; @@ -19,7 +17,6 @@ import { restoreKeyAgent } from '../../../src'; describe('KeyManagement/restoreKeyAgent', () => { const dependencies: KeyAgentDependencies = { bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - inputResolver: { resolveInput: jest.fn() }, logger: dummyLogger }; @@ -34,72 +31,24 @@ describe('KeyManagement/restoreKeyAgent', () => { 87, 78, 204, 222, 109, 3, 239, 117 ]; - const address = Cardano.PaymentAddress( - 'addr1qx52knza2h5x090n4a5r7yraz3pwcamk9ppvuh7e26nfks7pnmhxqavtqy02zezklh27jt9r6z62sav3mugappdc7xnskxy2pn' - ); - const extendedAccountPublicKey = Crypto.Bip32PublicKeyHex( // eslint-disable-next-line max-len '6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d396199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39' ); - const rewardAccount = Cardano.RewardAccount('stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr'); - - const inMemoryKeyAgentDataWithoutStakeDerivationPath: SerializableInMemoryKeyAgentData = { - __typename: KeyAgentType.InMemory, - accountIndex: 0, - chainId: Cardano.ChainIds.Preview, - encryptedRootPrivateKeyBytes, - extendedAccountPublicKey, - knownAddresses: [ - { - accountIndex: 0, - address, - index: 0, - networkId: Cardano.NetworkId.Mainnet, - rewardAccount, - type: AddressType.External - } - ] - }; - const inMemoryKeyAgentData: SerializableInMemoryKeyAgentData = { __typename: KeyAgentType.InMemory, accountIndex: 0, chainId: Cardano.ChainIds.Preview, encryptedRootPrivateKeyBytes, - extendedAccountPublicKey, - knownAddresses: [ - { - accountIndex: 0, - address, - index: 0, - networkId: Cardano.NetworkId.Mainnet, - rewardAccount, - stakeKeyDerivationPath: util.STAKE_KEY_DERIVATION_PATH, - type: AddressType.External - } - ] + extendedAccountPublicKey }; // eslint-disable-next-line unicorn/consistent-function-scoping const getPassphrase: GetPassphrase = async () => Buffer.from('password'); - it('assumes default stakeKeyDerivationPath if not present in serializable data', async () => { - expect.assertions(1); - const keyAgent = await restoreKeyAgent( - inMemoryKeyAgentDataWithoutStakeDerivationPath, - dependencies, - getPassphrase - ); - - for (const knownAddress of keyAgent.knownAddresses) { - expect(knownAddress.stakeKeyDerivationPath).toBe(util.STAKE_KEY_DERIVATION_PATH); - } - }); - it('can restore key manager from valid data and passphrase', async () => { const keyAgent = await restoreKeyAgent(inMemoryKeyAgentData, dependencies, getPassphrase); - expect(keyAgent.knownAddresses).toEqual(inMemoryKeyAgentData.knownAddresses); + expect(keyAgent.extendedAccountPublicKey).toEqual(inMemoryKeyAgentData.extendedAccountPublicKey); }); it('throws when attempting to restore key manager from valid data and no passphrase', async () => { @@ -127,25 +76,12 @@ describe('KeyManagement/restoreKeyAgent', () => { extendedAccountPublicKey: Crypto.Bip32PublicKeyHex( // eslint-disable-next-line max-len 'fc5ab25e830b67c47d0a17411bf7fdabf711a597fb6cf04102734b0a2934ceaaa65ff5e7c52498d52c07b8ddfcd436fc2b4d2775e2984a49d0c79f65ceee4779' - ), - knownAddresses: [ - { - accountIndex: 0, - address: Cardano.PaymentAddress( - 'addr1qx52knza2h5x090n4a5r7yraz3pwcamk9ppvuh7e26nfks7pnmhxqavtqy02zezklh27jt9r6z62sav3mugappdc7xnskxy2pn' - ), - index: 0, - networkId: Cardano.NetworkId.Mainnet, - rewardAccount: Cardano.RewardAccount('stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr'), - stakeKeyDerivationPath: util.STAKE_KEY_DERIVATION_PATH, - type: AddressType.External - } - ] + ) }; it('can restore key manager from valid data', async () => { const keyAgent = await restoreKeyAgent(ledgerKeyAgentData, dependencies); - expect(keyAgent.knownAddresses).toEqual(ledgerKeyAgentData.knownAddresses); + expect(keyAgent.extendedAccountPublicKey).toEqual(ledgerKeyAgentData.extendedAccountPublicKey); }); }); @@ -158,19 +94,6 @@ describe('KeyManagement/restoreKeyAgent', () => { // eslint-disable-next-line max-len 'fc5ab25e830b67c47d0a17411bf7fdabf711a597fb6cf04102734b0a2934ceaaa65ff5e7c52498d52c07b8ddfcd436fc2b4d2775e2984a49d0c79f65ceee4779' ), - knownAddresses: [ - { - accountIndex: 0, - address: Cardano.PaymentAddress( - 'addr1qx52knza2h5x090n4a5r7yraz3pwcamk9ppvuh7e26nfks7pnmhxqavtqy02zezklh27jt9r6z62sav3mugappdc7xnskxy2pn' - ), - index: 0, - networkId: Cardano.NetworkId.Mainnet, - rewardAccount: Cardano.RewardAccount('stake1u89sasnfyjtmgk8ydqfv3fdl52f36x3djedfnzfc9rkgzrcss5vgr'), - stakeKeyDerivationPath: util.STAKE_KEY_DERIVATION_PATH, - type: AddressType.External - } - ], trezorConfig: { communicationType: CommunicationType.Node, manifest: { @@ -182,7 +105,7 @@ describe('KeyManagement/restoreKeyAgent', () => { it('can restore key manager from valid data', async () => { const keyAgent = await restoreKeyAgent(trezorKeyAgentData, dependencies); - expect(keyAgent.knownAddresses).toEqual(trezorKeyAgentData.knownAddresses); + expect(keyAgent.extendedAccountPublicKey).toEqual(trezorKeyAgentData.extendedAccountPublicKey); }); }); diff --git a/packages/wallet/test/services/PublicStakeKeysTracker.test.ts b/packages/wallet/test/services/PublicStakeKeysTracker.test.ts index 966cac3a79d..c59a164110f 100644 --- a/packages/wallet/test/services/PublicStakeKeysTracker.test.ts +++ b/packages/wallet/test/services/PublicStakeKeysTracker.test.ts @@ -1,4 +1,4 @@ -import { AccountKeyDerivationPath, AsyncKeyAgent, GroupedAddress, KeyRole, util } from '@cardano-sdk/key-management'; +import { AccountKeyDerivationPath, Bip32Account, GroupedAddress, KeyRole } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; import { ObservableWallet } from '../../src'; import { PubStakeKeyAndStatus, createPublicStakeKeysTracker } from '../../src/services/PublicStakeKeysTracker'; @@ -8,7 +8,7 @@ import { mockProviders as mocks } from '@cardano-sdk/util-dev'; describe('PublicStakeKeysTracker', () => { let addresses: GroupedAddress[]; let rewardAccounts: Cardano.RewardAccountInfo[]; - let keyAgent: AsyncKeyAgent; + let bip32Account: Bip32Account; let derivePublicKey: jest.Mock; /** Assert multiple emissions from stakePubKey$ */ @@ -21,7 +21,7 @@ describe('PublicStakeKeysTracker', () => { expect(publicKeyEmissions).toEqual(expectedEmissions); }; - beforeEach(() => { + beforeEach(async () => { addresses = [ { rewardAccount: mocks.rewardAccount, @@ -64,12 +64,16 @@ describe('PublicStakeKeysTracker', () => { } ]; - derivePublicKey = jest + derivePublicKey = derivePublicKey = jest .fn() - .mockImplementation((path: AccountKeyDerivationPath) => Promise.resolve(`abc-${path.index}`)); - keyAgent = { - derivePublicKey - } as unknown as AsyncKeyAgent; + .mockImplementation((path: AccountKeyDerivationPath) => Promise.resolve({ hex: () => `abc-${path.index}` })); + bip32Account = { + accountIndex: 0, + chainId: Cardano.ChainIds.Preview, + deriveAddress: jest.fn(), + derivePublicKey, + extendedAccountPublicKey: '' as unknown + } as Bip32Account; }); it('empty array when there are no reward accounts', async () => { @@ -77,8 +81,8 @@ describe('PublicStakeKeysTracker', () => { const rewardAccounts$ = of([]); const stakePubKeys$ = createPublicStakeKeysTracker({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), addresses$, + bip32Account, rewardAccounts$ }); @@ -91,8 +95,8 @@ describe('PublicStakeKeysTracker', () => { const rewardAccounts$ = of(rewardAccounts); const stakePubKeys$ = createPublicStakeKeysTracker({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), addresses$, + bip32Account, rewardAccounts$ }); @@ -117,8 +121,8 @@ describe('PublicStakeKeysTracker', () => { const rewardAccounts$ = of(rewardAccounts); const stakePubKeys$ = createPublicStakeKeysTracker({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), addresses$, + bip32Account, rewardAccounts$ }); @@ -136,8 +140,8 @@ describe('PublicStakeKeysTracker', () => { const rewardAccounts$ = from([[rewardAccounts[0]], rewardAccounts]); const stakePubKeys$ = createPublicStakeKeysTracker({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), addresses$, + bip32Account, rewardAccounts$ }); @@ -157,8 +161,8 @@ describe('PublicStakeKeysTracker', () => { const rewardAccounts$ = of(rewardAccounts); const stakePubKeys$ = createPublicStakeKeysTracker({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), addresses$, + bip32Account, rewardAccounts$ }); @@ -180,8 +184,8 @@ describe('PublicStakeKeysTracker', () => { ); const stakePubKeys$ = createPublicStakeKeysTracker({ - addressManager: util.createBip32Ed25519AddressManager(keyAgent), addresses$, + bip32Account, rewardAccounts$ }); diff --git a/packages/wallet/test/services/WalletUtil.test.ts b/packages/wallet/test/services/WalletUtil.test.ts index 20310ee4ff0..f6b2d7210c2 100644 --- a/packages/wallet/test/services/WalletUtil.test.ts +++ b/packages/wallet/test/services/WalletUtil.test.ts @@ -1,20 +1,17 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, GroupedAddress, util as KeyManagementUtil } from '@cardano-sdk/key-management'; -import { Cardano } from '@cardano-sdk/core'; import { - PersonalWallet, - WalletUtilContext, - createInputResolver, - createLazyWalletUtil, - requiresForeignSignatures, - setupWallet -} from '../../src'; -import { ProtocolParametersRequiredByOutputValidator } from '@cardano-sdk/tx-construction'; + AddressType, + Bip32Account, + GroupedAddress, + util as KeyManagementUtil, + KeyRole +} from '@cardano-sdk/key-management'; +import { Cardano } from '@cardano-sdk/core'; +import { PersonalWallet, createInputResolver, requiresForeignSignatures } from '../../src'; +import { createAsyncKeyAgent, waitForWalletStateSettle } from '../util'; import { createStubStakePoolProvider, mockProviders as mocks } from '@cardano-sdk/util-dev'; import { dummyLogger as logger } from 'ts-log'; import { of } from 'rxjs'; -import { stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; -import { waitForWalletStateSettle } from '../util'; describe('WalletUtil', () => { describe('createInputResolver', () => { @@ -53,21 +50,6 @@ describe('WalletUtil', () => { }); }); - describe('createLazyWalletUtil', () => { - it('awaits for "initialize" to be called before resolving call to any util', async () => { - const util = createLazyWalletUtil(); - const resultPromise = util.validateValue({ coins: 2_000_000n }); - util.initialize({ - protocolParameters$: of({ - coinsPerUtxoByte: 4310, - maxValueSize: 90 - }) - } as WalletUtilContext); - const result = await resultPromise; - expect(result.coinMissing).toBe(0n); - }); - }); - describe('requiresForeignSignatures', () => { const address = mocks.utxo[0][0].address!; let txSubmitProvider: mocks.TxSubmitProviderStub; @@ -99,34 +81,30 @@ describe('WalletUtil', () => { index: 0, networkId: Cardano.NetworkId.Testnet, rewardAccount: mocks.rewardAccount, - stakeKeyDerivationPath, + stakeKeyDerivationPath: { + index: 0, + role: KeyRole.Stake + }, type: AddressType.External }; - ({ wallet } = await setupWallet({ - bip32Ed25519: new Crypto.SodiumBip32Ed25519(), - createKeyAgent: async (dependencies) => { - const asyncKeyAgent = await testAsyncKeyAgent([groupedAddress], dependencies); - asyncKeyAgent.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); - return asyncKeyAgent; - }, - createWallet: async (keyAgent) => - new PersonalWallet( - { name: 'Test Wallet' }, - { - addressManager: KeyManagementUtil.createBip32Ed25519AddressManager(keyAgent), - assetProvider, - chainHistoryProvider, - logger, - networkInfoProvider, - rewardsProvider, - stakePoolProvider, - txSubmitProvider, - utxoProvider, - witnesser: KeyManagementUtil.createBip32Ed25519Witnesser(keyAgent) - } - ), - logger - })); + const asyncKeyAgent = await createAsyncKeyAgent(); + const bip32Account = await Bip32Account.fromAsyncKeyAgent(asyncKeyAgent); + bip32Account.deriveAddress = jest.fn().mockResolvedValue(groupedAddress); + wallet = new PersonalWallet( + { name: 'Test Wallet' }, + { + assetProvider, + bip32Account, + chainHistoryProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser: KeyManagementUtil.createBip32Ed25519Witnesser(asyncKeyAgent) + } + ); await waitForWalletStateSettle(wallet); diff --git a/packages/wallet/test/services/addressDiscovery/HDSequentialDiscovery.test.ts b/packages/wallet/test/services/addressDiscovery/HDSequentialDiscovery.test.ts index 586b1f6f1f9..384e37c8ab1 100644 --- a/packages/wallet/test/services/addressDiscovery/HDSequentialDiscovery.test.ts +++ b/packages/wallet/test/services/addressDiscovery/HDSequentialDiscovery.test.ts @@ -1,22 +1,35 @@ -import { AddressType, AsyncKeyAgent, KeyRole, util } from '@cardano-sdk/key-management'; +import { AccountAddressDerivationPath, AddressType, Bip32Account, KeyRole } from '@cardano-sdk/key-management'; import { Cardano } from '@cardano-sdk/core'; import { HDSequentialDiscovery } from '../../../src'; +import { createAsyncKeyAgent } from '../../util'; import { createMockChainHistoryProvider, mockAlwaysEmptyChainHistoryProvider, - mockAlwaysFailChainHistoryProvider, - mockChainHistoryProvider, - prepareMockKeyAgentWithData + mockChainHistoryProvider } from './mockData'; -import { firstValueFrom } from 'rxjs'; const asPaymentAddress = (address: string) => address as Cardano.PaymentAddress; describe('HDSequentialDiscovery', () => { - let mockKeyAgent: AsyncKeyAgent; - - beforeEach(() => { - mockKeyAgent = prepareMockKeyAgentWithData(); + let bip32Account: Bip32Account; + + beforeEach(async () => { + bip32Account = await Bip32Account.fromAsyncKeyAgent(await createAsyncKeyAgent()); + // const addresses = createStubAddresses(); + bip32Account.deriveAddress = jest + .fn() + .mockImplementation(async (payment: AccountAddressDerivationPath, stakeKeyIndex = 0) => ({ + accountIndex: 0, + address: `testAddress_${payment.index}_${stakeKeyIndex}_${payment.type}` as Cardano.PaymentAddress, + index: payment.index, + networkId: Cardano.NetworkId.Testnet, + rewardAccount: `testStakeAddress_${stakeKeyIndex}` as Cardano.RewardAccount, + stakeKeyDerivationPath: { + index: stakeKeyIndex, + role: KeyRole.Stake + }, + type: payment.type + })); }); it('can return both "internal" and "external" type addresses', async () => { @@ -33,7 +46,7 @@ describe('HDSequentialDiscovery', () => { 25 ); - const addresses = await discovery.discover(util.createBip32Ed25519AddressManager(mockKeyAgent)); + const addresses = await discovery.discover(bip32Account); expect(addresses.length).toEqual(5); expect(addresses[0]).toEqual({ @@ -96,21 +109,17 @@ describe('HDSequentialDiscovery', () => { }, type: AddressType.External }); - - const knownAddresses = await firstValueFrom(mockKeyAgent.knownAddresses$); - - knownAddresses.sort( - (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index + expect(addresses).toEqual( + [...addresses].sort( + (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index + ) ); - - expect(addresses).toEqual(knownAddresses); }); it('derives exactly 1 address when no used addresses are found', async () => { const discovery = new HDSequentialDiscovery(mockAlwaysEmptyChainHistoryProvider, 25); - const addresses = await discovery.discover(util.createBip32Ed25519AddressManager(mockKeyAgent)); + const addresses = await discovery.discover(bip32Account); expect(addresses).toHaveLength(1); - expect(await firstValueFrom(mockKeyAgent.knownAddresses$)).toHaveLength(1); }); it('return discovered addresses with different stake keys', async () => { @@ -130,7 +139,7 @@ describe('HDSequentialDiscovery', () => { 25 ); - const addresses = await discovery.discover(util.createBip32Ed25519AddressManager(mockKeyAgent)); + const addresses = await discovery.discover(bip32Account); // 5 payment key + 4 stake keys combined with payment index 0 (the first address overlaps in both sets). expect(addresses.length).toEqual(8); @@ -146,19 +155,17 @@ describe('HDSequentialDiscovery', () => { type: 0 }); - const knownAddresses = await firstValueFrom(mockKeyAgent.knownAddresses$); - - knownAddresses.sort( - (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index + expect(addresses).toEqual( + [...addresses].sort( + (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index + ) ); - - expect(addresses).toEqual(knownAddresses); }); it('return all discovered addresses', async () => { const discovery = new HDSequentialDiscovery(mockChainHistoryProvider, 25); - const addresses = await discovery.discover(util.createBip32Ed25519AddressManager(mockKeyAgent)); + const addresses = await discovery.discover(bip32Account); expect(addresses.length).toEqual(50); @@ -173,49 +180,19 @@ describe('HDSequentialDiscovery', () => { type: 0 }); - const knownAddresses = await firstValueFrom(mockKeyAgent.knownAddresses$); - // The mock chain history provider will only 'return' results for even addresses which index is // less than 100. On top of that, the discovery process will return the addresses sorted by payment credential and // stake credential it will not return duplicates. We can reproduce this list from our initial data set by // filtering/ordering the addresses accordingly and asserting the results. - knownAddresses.sort( + const sorted = addresses.sort( (a, b) => a.index - b.index || a.stakeKeyDerivationPath!.index - b.stakeKeyDerivationPath!.index ); - const filtered = [ - ...new Set( - knownAddresses.filter((address) => { - const index = Number(address.address.split('_')[1]); - return index < 100 && index % 2 === 0; - }) - ) - ]; + const filtered = sorted.filter((address) => { + const index = Number(address.address.split('_')[1]); + return index < 100 && index % 2 === 0; + }); expect(addresses).toEqual(filtered); }); - - it('key agent state doesnt change if the discovery process fails', async () => { - // Add a known address to the key agent initial state. - const knownAddress = { - accountIndex: 0, - address: 'known address' as unknown as Cardano.PaymentAddress, - index: 0, - networkId: Cardano.NetworkId.Testnet, - rewardAccount: 'testStakeAddress_0' as unknown as Cardano.RewardAccount, - stakeKeyDerivationPath: { - index: 0, - role: KeyRole.Stake - }, - type: AddressType.External - }; - - await mockKeyAgent.setKnownAddresses([knownAddress]); - - const discovery = new HDSequentialDiscovery(mockAlwaysFailChainHistoryProvider, 25); - await expect(discovery.discover(util.createBip32Ed25519AddressManager(mockKeyAgent))).rejects.toThrow(); - - const knownAddresses = await firstValueFrom(mockKeyAgent.knownAddresses$); - expect(knownAddresses).toEqual([knownAddress]); - }); }); diff --git a/packages/wallet/test/services/addressDiscovery/SingleAddressDiscovery.test.ts b/packages/wallet/test/services/addressDiscovery/SingleAddressDiscovery.test.ts index 8a03a4a7657..4a07c5d8e18 100644 --- a/packages/wallet/test/services/addressDiscovery/SingleAddressDiscovery.test.ts +++ b/packages/wallet/test/services/addressDiscovery/SingleAddressDiscovery.test.ts @@ -1,26 +1,21 @@ -import { AsyncKeyAgent, KeyRole, util } from '@cardano-sdk/key-management'; +import { Bip32Account, KeyRole } from '@cardano-sdk/key-management'; import { SingleAddressDiscovery } from '../../../src'; -import { prepareMockKeyAgentWithData } from './mockData'; +import { createAsyncKeyAgent } from '../../util'; describe('SingleAddressDiscovery', () => { - let mockKeyAgent: AsyncKeyAgent; - - beforeEach(() => { - mockKeyAgent = prepareMockKeyAgentWithData(); - }); - it('return the first derived address', async () => { + const bip32Account = await Bip32Account.fromAsyncKeyAgent(await createAsyncKeyAgent()); const discovery = new SingleAddressDiscovery(); - const addresses = await discovery.discover(util.createBip32Ed25519AddressManager(mockKeyAgent)); + const addresses = await discovery.discover(bip32Account); expect(addresses.length).toEqual(1); expect(addresses[0]).toEqual({ accountIndex: 0, - address: 'testAddress_0_0_0', + address: expect.stringContaining('addr'), index: 0, networkId: 0, - rewardAccount: 'testStakeAddress_0', + rewardAccount: expect.stringContaining('stake'), stakeKeyDerivationPath: { index: 0, role: KeyRole.Stake }, type: 0 }); diff --git a/packages/wallet/test/services/addressDiscovery/mockData.ts b/packages/wallet/test/services/addressDiscovery/mockData.ts index 4aa0edd0d88..851ed754416 100644 --- a/packages/wallet/test/services/addressDiscovery/mockData.ts +++ b/packages/wallet/test/services/addressDiscovery/mockData.ts @@ -1,89 +1,7 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { AddressType, AsyncKeyAgent, GroupedAddress, KeyRole } from '@cardano-sdk/key-management'; -import { BehaviorSubject } from 'rxjs'; import { Cardano, ChainHistoryProvider, Paginated, TransactionsByAddressesArgs } from '@cardano-sdk/core'; const NOT_IMPLEMENTED = 'Not implemented'; -const createMockAsyncKeyAgent = (knownAddresses: Array>): AsyncKeyAgent => { - let currentKnownAddresses = new Array(); - const knownAddresses$ = new BehaviorSubject(currentKnownAddresses); - return { - async deriveAddress(derivationPath, stakeKeyDerivationIndex: number, pure?: boolean) { - const address = { ...knownAddresses[derivationPath.index][stakeKeyDerivationIndex] }; - address.type = derivationPath.type; - address.address = `${address.address}_${derivationPath.type}` as Cardano.PaymentAddress; - - if (!pure) currentKnownAddresses.push(address); - - return address; - }, - derivePublicKey: () => Promise.resolve('00' as unknown as Crypto.Ed25519PublicKeyHex), - getBip32Ed25519: () => Promise.resolve(new Crypto.SodiumBip32Ed25519()), - getChainId: () => - Promise.resolve({ - networkId: 0, - networkMagic: 0 - }), - getExtendedAccountPublicKey: () => Promise.resolve('00' as unknown as Crypto.Bip32PublicKeyHex), - knownAddresses$, - setKnownAddresses: async (addresses: GroupedAddress[]): Promise => { - currentKnownAddresses = addresses; - knownAddresses$.next(currentKnownAddresses); - }, - // eslint-disable-next-line @typescript-eslint/no-empty-function - shutdown: () => {}, - signBlob: () => - Promise.resolve({ - publicKey: '00' as unknown as Crypto.Ed25519PublicKeyHex, - signature: '00' as unknown as Crypto.Ed25519SignatureHex - }), - signTransaction: () => Promise.resolve(new Map()) - }; -}; - -export const prepareMockKeyAgentWithData = () => { - const data = new Array>(); - const stakeKeys = new Array(); - - // Add 20 stake addresses credentials. - for (let i = 0; i < 20; ++i) { - stakeKeys.push({ - accountIndex: 0, - address: `testAddress_0_${i}` as unknown as Cardano.PaymentAddress, - index: i, - networkId: Cardano.NetworkId.Testnet, - rewardAccount: `testStakeAddress_${i}` as unknown as Cardano.RewardAccount, - stakeKeyDerivationPath: { - index: i, - role: KeyRole.Stake - }, - type: AddressType.External - }); - } - - data.push(stakeKeys); - - for (let i = 1; i < 200; ++i) { - data.push([ - { - accountIndex: 0, - address: `testAddress_${i}_0` as unknown as Cardano.PaymentAddress, - index: i, - networkId: Cardano.NetworkId.Testnet, - rewardAccount: 'testStakeAddress_0' as unknown as Cardano.RewardAccount, - stakeKeyDerivationPath: { - index: 0, - role: KeyRole.Stake - }, - type: AddressType.External - } - ]); - } - - return createMockAsyncKeyAgent(data); -}; - export const mockChainHistoryProvider: ChainHistoryProvider = { blocksByHashes: () => { throw new Error(NOT_IMPLEMENTED); diff --git a/packages/wallet/test/setupWallet.test.ts b/packages/wallet/test/setupWallet.test.ts deleted file mode 100644 index f07b518e9f1..00000000000 --- a/packages/wallet/test/setupWallet.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as Crypto from '@cardano-sdk/crypto'; -import { logger } from '@cardano-sdk/util-dev'; -import { setupWallet } from '../src'; - -jest.mock('../src/services/WalletUtil'); -const { createLazyWalletUtil } = jest.requireMock('../src/services/WalletUtil'); - -describe('setupWallet', () => { - const bip32Ed25519 = new Crypto.SodiumBip32Ed25519(); - - it('initializes WalletUtil with the wallet that is then used as InputResolver for KeyAgent', async () => { - const initialize = jest.fn(); - const walletUtil = { initialize }; - createLazyWalletUtil.mockReturnValueOnce(walletUtil); - // actual values doesn't matter for this test, adding any property to be able to assert toEqual - const keyAgent = { keyAgent: true }; - const wallet = { wallet: true }; - - const createKeyAgent = jest.fn().mockResolvedValueOnce(keyAgent); - const createWallet = jest.fn().mockResolvedValueOnce(wallet); - expect( - await setupWallet({ - bip32Ed25519, - createKeyAgent, - createWallet, - logger - }) - ).toEqual({ keyAgent, wallet, walletUtil }); - expect(initialize).toBeCalledWith(wallet); - expect(createKeyAgent).toBeCalledWith({ bip32Ed25519, inputResolver: walletUtil, logger }); - expect(createWallet).toBeCalledWith(keyAgent); - }); -}); diff --git a/packages/wallet/test/util.ts b/packages/wallet/test/util.ts index 3120a8e359e..79e14ef6fa3 100644 --- a/packages/wallet/test/util.ts +++ b/packages/wallet/test/util.ts @@ -1,11 +1,11 @@ -/* eslint-disable func-style */ -/* eslint-disable jsdoc/require-jsdoc */ - import * as Crypto from '@cardano-sdk/crypto'; import { Cardano, TxCBOR } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; +import { InMemoryKeyAgent, util } from '@cardano-sdk/key-management'; import { Observable, catchError, filter, firstValueFrom, throwError, timeout } from 'rxjs'; import { ObservableWallet, OutgoingTx } from '../src'; +import { SodiumBip32Ed25519 } from '@cardano-sdk/crypto'; +import { logger } from '@cardano-sdk/util-dev'; const SECOND = 1000; const MINUTE = 60 * SECOND; @@ -58,3 +58,18 @@ export const buildDRepIDFromDRepKey = ( }); return HexBlob.toTypedBech32('drep', HexBlob.fromBytes(paymentAddress)); }; + +export const createAsyncKeyAgent = async () => + util.createAsyncKeyAgent( + await InMemoryKeyAgent.fromBip39MnemonicWords( + { + chainId: Cardano.ChainIds.Preview, + getPassphrase: async () => Buffer.from([]), + mnemonicWords: util.generateMnemonicWords() + }, + { + bip32Ed25519: new SodiumBip32Ed25519(), + logger + } + ) + ); diff --git a/packages/web-extension/src/keyAgent/util.ts b/packages/web-extension/src/keyAgent/util.ts index cdbfa1808f5..03adbf6d9de 100644 --- a/packages/web-extension/src/keyAgent/util.ts +++ b/packages/web-extension/src/keyAgent/util.ts @@ -6,11 +6,10 @@ export const keyAgentChannel = (walletName: string) => `${walletName}$-keyAgent` export const keyAgentProperties: RemoteApiProperties = { deriveAddress: RemoteApiPropertyType.MethodReturningPromise, derivePublicKey: RemoteApiPropertyType.MethodReturningPromise, + getAccountIndex: RemoteApiPropertyType.MethodReturningPromise, getBip32Ed25519: RemoteApiPropertyType.MethodReturningPromise, getChainId: RemoteApiPropertyType.MethodReturningPromise, getExtendedAccountPublicKey: RemoteApiPropertyType.MethodReturningPromise, - knownAddresses$: RemoteApiPropertyType.HotObservable, - setKnownAddresses: RemoteApiPropertyType.MethodReturningPromise, signBlob: RemoteApiPropertyType.MethodReturningPromise, signTransaction: RemoteApiPropertyType.MethodReturningPromise };