From 9286c0e6486288523cbd5bc0b97894e0db81ab02 Mon Sep 17 00:00:00 2001 From: Angel Castillo Date: Wed, 4 Jun 2025 19:32:35 +0800 Subject: [PATCH] fix: fix native script singing --- .../src/util/ownSignatureKeyPaths.ts | 18 ++- .../test/util/ownSignaturePaths.test.ts | 9 +- packages/wallet/jest.config.js | 2 +- packages/wallet/jest.setup.js | 18 +++ packages/wallet/src/Wallets/BaseWallet.ts | 7 +- packages/wallet/src/cip30.ts | 31 ++---- .../test/PersonalWallet/methods.test.ts | 62 +++++++++++ .../hardware/ledger/LedgerKeyAgent.test.ts | 95 ++++++++++++++-- .../hardware/trezor/TrezorKeyAgent.test.ts | 103 ++++++++++++++++++ packages/wallet/test/hardware/utils.ts | 32 ++++++ .../test/integration/cip30mapping.test.ts | 66 +++++++++-- 11 files changed, 389 insertions(+), 54 deletions(-) create mode 100644 packages/wallet/jest.setup.js create mode 100644 packages/wallet/test/hardware/utils.ts diff --git a/packages/key-management/src/util/ownSignatureKeyPaths.ts b/packages/key-management/src/util/ownSignatureKeyPaths.ts index ff077b61cc5..10ab7dd320a 100644 --- a/packages/key-management/src/util/ownSignatureKeyPaths.ts +++ b/packages/key-management/src/util/ownSignatureKeyPaths.ts @@ -1,8 +1,7 @@ import * as Crypto from '@cardano-sdk/crypto'; -import { AccountKeyDerivationPath, GroupedAddress, KeyRole, TxInId, TxInKeyPathMap } from '../types'; +import { AccountKeyDerivationPath, GroupedAddress, TxInId, TxInKeyPathMap } from '../types'; import { Cardano } from '@cardano-sdk/core'; import { DREP_KEY_DERIVATION_PATH } from './key'; -import { Ed25519KeyHashHex } from '@cardano-sdk/crypto'; import { isNotNil } from '@cardano-sdk/util'; import isEqual from 'lodash/isEqual.js'; import uniqBy from 'lodash/uniqBy.js'; @@ -167,7 +166,9 @@ export const checkStakeCredentialCertificates = ( const getSignersData = (groupedAddresses: GroupedAddress[]): StakeKeySignerData[] => uniqBy(groupedAddresses, 'rewardAccount') .map((groupedAddress) => { - const stakeKeyHash = Cardano.RewardAccount.toHash(groupedAddress.rewardAccount) as unknown as Ed25519KeyHashHex; + const stakeKeyHash = Cardano.RewardAccount.toHash( + groupedAddress.rewardAccount + ) as unknown as Crypto.Ed25519KeyHashHex; const poolId = Cardano.PoolId.fromKeyHash(stakeKeyHash); return { derivationPath: groupedAddress.stakeKeyDerivationPath, @@ -299,18 +300,13 @@ const checkStakeCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519Ke const checkPaymentCredential = (address: GroupedAddress, keyHash: Crypto.Ed25519KeyHashHex) => { const paymentCredential = Cardano.Address.fromBech32(address.address)?.asBase()?.getPaymentCredential(); - if (paymentCredential?.type === Cardano.CredentialType.ScriptHash && paymentCredential.hash === keyHash) + if (paymentCredential?.hash === keyHash) { return { derivationPaths: [{ index: address.index, role: Number(address.type) }], requiresForeignSignatures: false }; - - if (paymentCredential?.type === Cardano.CredentialType.ScriptHash) { - return { - derivationPaths: [{ index: address.index, role: KeyRole.External }], - requiresForeignSignatures: false - }; } + return { derivationPaths: [], requiresForeignSignatures: true }; }; @@ -327,7 +323,7 @@ const processSignatureScript = ( for (const address of groupedAddresses) { if (address.stakeKeyDerivationPath) { - signatureCheck = checkStakeCredential(address, script.keyHash); + signatureCheck = combineSignatureChecks(signatureCheck, checkStakeCredential(address, script.keyHash)); } signatureCheck = combineSignatureChecks(signatureCheck, checkPaymentCredential(address, script.keyHash)); } diff --git a/packages/key-management/test/util/ownSignaturePaths.test.ts b/packages/key-management/test/util/ownSignaturePaths.test.ts index 02f1746397f..000c2b79072 100644 --- a/packages/key-management/test/util/ownSignaturePaths.test.ts +++ b/packages/key-management/test/util/ownSignaturePaths.test.ts @@ -634,10 +634,10 @@ describe('KeyManagement.util.ownSignaturePaths', () => { expect(util.ownSignatureKeyPaths(txBody, [knownAddress1], {}, undefined, scripts)).toEqual([]); }); it('includes derivation paths for multi-signature native scripts', async () => { - const scriptAddress = Cardano.PaymentAddress( + const walletAddress = Cardano.PaymentAddress( 'addr_test1xr806j8xcq6cw6jjkzfxyewyue33zwnu4ajnu28hakp5fmc6gddlgeqee97vwdeafwrdgrtzp2rw8rlchjf25ld7r2ssptq3m9' ); - const scriptRewardAccount = Cardano.RewardAccount( + const walletRewardAccount = Cardano.RewardAccount( 'stake_test17qdyxkl5vsvujlx8xu75hpk5p43q4phr3lutey420klp4gg7zmhrn' ); const txBody: Cardano.TxBody = { @@ -653,7 +653,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => { scripts: [ { __type: Cardano.ScriptType.Native, - keyHash: Ed25519KeyHashHex('b498c0eaceb9a8c7c829d36fc84e892113c9d2636b53b0636d7518b4'), + keyHash: Ed25519KeyHashHex('cefd48e6c035876a52b0926265c4e663113a7caf653e28f7ed8344ef'), kind: Cardano.NativeScriptKind.RequireSignature }, { @@ -665,7 +665,8 @@ describe('KeyManagement.util.ownSignaturePaths', () => { } ]; - const knownAddress = createGroupedAddress(scriptAddress, scriptRewardAccount, AddressType.External, 0); + const knownAddress = createGroupedAddress(walletAddress, walletRewardAccount, AddressType.External, 0); + expect(util.ownSignatureKeyPaths(txBody, [knownAddress], {}, undefined, scripts)).toEqual([ { index: 0, diff --git a/packages/wallet/jest.config.js b/packages/wallet/jest.config.js index c3ad765240c..18e65f4f21b 100644 --- a/packages/wallet/jest.config.js +++ b/packages/wallet/jest.config.js @@ -1,4 +1,4 @@ module.exports = { ...require('../../test/jest.config'), - setupFiles: ['jest-webextension-mock'] + setupFiles: ['jest-webextension-mock', './jest.setup.js'] }; diff --git a/packages/wallet/jest.setup.js b/packages/wallet/jest.setup.js new file mode 100644 index 00000000000..326971a15b6 --- /dev/null +++ b/packages/wallet/jest.setup.js @@ -0,0 +1,18 @@ +// Polyfill for Array.prototype.findLast on unit tests +if (!Array.prototype.findLast) { + // eslint-disable-next-line no-extend-native + Array.prototype.findLast = function (predicate, thisArg) { + if (!Array.isArray(this)) { + throw new TypeError('Array.prototype.findLast called on non-array object'); + } + if (typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + + for (let i = this.length - 1; i >= 0; i--) { + if (predicate.call(thisArg, this[i], i, this)) { + return this[i]; + } + } + }; +} diff --git a/packages/wallet/src/Wallets/BaseWallet.ts b/packages/wallet/src/Wallets/BaseWallet.ts index a86fead4b8a..fb50b01d954 100644 --- a/packages/wallet/src/Wallets/BaseWallet.ts +++ b/packages/wallet/src/Wallets/BaseWallet.ts @@ -668,11 +668,16 @@ export class BaseWallet implements ObservableWallet { transaction = Serialization.Transaction.fromCbor(tx); } + const txWitness = transaction.witnessSet().toCore(); const context = { ...signingContext, dRepPublicKey, knownAddresses, - txInKeyPathMap: await util.createTxInKeyPathMap(transaction.body().toCore(), knownAddresses, this.util) + scripts: [...(signingContext?.scripts ?? []), ...(witness?.scripts ?? []), ...(txWitness?.scripts ?? [])], + // Script wallets cant sign specific outputs with keys, the signatures are added to satisfy the witness script + txInKeyPathMap: isBip32PublicCredentialsManager(this.#publicCredentialsManager) + ? await util.createTxInKeyPathMap(transaction.body().toCore(), knownAddresses, this.util) + : {} }; const result = await this.witnesser.witness(transaction, context, signingOptions); diff --git a/packages/wallet/src/cip30.ts b/packages/wallet/src/cip30.ts index ab1a4d59308..81dec8f70e4 100644 --- a/packages/wallet/src/cip30.ts +++ b/packages/wallet/src/cip30.ts @@ -20,7 +20,7 @@ import { } from '@cardano-sdk/dapp-connector'; import { Cardano, Milliseconds, Serialization, coalesceValueQuantities } from '@cardano-sdk/core'; import { Ed25519KeyHashHex } from '@cardano-sdk/crypto'; -import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util'; +import { HexBlob } from '@cardano-sdk/util'; import { InputSelectionError, InputSelectionFailure } from '@cardano-sdk/input-selection'; import { Logger } from 'ts-log'; import { MessageSender } from '@cardano-sdk/key-management'; @@ -370,23 +370,16 @@ const baseCip30WalletApi = ( return addresses.map((groupAddresses) => cardanoAddressToCbor(groupAddresses.address)); }, getUtxos: async (_: SenderContext, amount?: Cbor, paginate?: Paginate): Promise => { - const scope = new ManagedFreeableScope(); - try { - const wallet = await firstValueFrom(wallet$); - await waitForWalletStateSettle(wallet); - let utxos = amount - ? await selectUtxo(wallet, parseValueCbor(amount).toCore(), !!paginate) - : await firstValueFrom(wallet.utxo.available$); - if (!utxos) return null; - if (paginate) { - utxos = utxos.slice(paginate.page * paginate.limit, paginate.page * paginate.limit + paginate.limit); - } - const cbor = utxos.map((core) => Serialization.TransactionUnspentOutput.fromCore(core).toCbor()); - scope.dispose(); - return cbor; - } finally { - scope.dispose(); + const wallet = await firstValueFrom(wallet$); + await waitForWalletStateSettle(wallet); + let utxos = amount + ? await selectUtxo(wallet, parseValueCbor(amount).toCore(), !!paginate) + : await firstValueFrom(wallet.utxo.available$); + if (!utxos) return null; + if (paginate) { + utxos = utxos.slice(paginate.page * paginate.limit, paginate.page * paginate.limit + paginate.limit); } + return utxos.map((core) => Serialization.TransactionUnspentOutput.fromCore(core).toCbor()); }, signData: async ( { sender }: SenderContext, @@ -424,7 +417,6 @@ const baseCip30WalletApi = ( throw new DataSignError(DataSignErrorCode.UserDeclined, 'user declined signing'); }, signTx: async ({ sender }: SenderContext, tx: Cbor, partialSign?: Boolean): Promise => { - const scope = new ManagedFreeableScope(); logger.debug('signTx', tx); const txCbor = Serialization.TxCBOR(tx); const txDecoded = Serialization.Transaction.fromCbor(txCbor); @@ -482,11 +474,8 @@ const baseCip30WalletApi = ( const message = formatUnknownError(error); throw new TxSignError(TxSignErrorCode.UserDeclined, message); } - } finally { - scope.dispose(); } } else { - scope.dispose(); throw new TxSignError(TxSignErrorCode.UserDeclined, 'user declined signing tx'); } }, diff --git a/packages/wallet/test/PersonalWallet/methods.test.ts b/packages/wallet/test/PersonalWallet/methods.test.ts index 170a0abfc7f..8a675c21fae 100644 --- a/packages/wallet/test/PersonalWallet/methods.test.ts +++ b/packages/wallet/test/PersonalWallet/methods.test.ts @@ -19,6 +19,8 @@ import { } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps } from '@cardano-sdk/tx-construction'; +import { LargeFirstSelector } from '@cardano-sdk/input-selection'; +import { MockChangeAddressResolver, getPayToPubKeyHashScript, getPaymentCredential } from '../hardware/utils'; import { babbageTx } from '../../../core/test/Serialization/testData'; import { buildDRepAddressFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util'; import { getPassphrase, stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; @@ -321,6 +323,66 @@ describe('BaseWallet methods', () => { expect(tx.witness.signatures.size).toBe(2); // spending key and stake key for withdrawal }); + it('can sign with native scripts credentials', async () => { + const walletWithMockInputResolver = createPersonalWallet( + { name: 'Test Wallet' }, + { + addressDiscovery, + assetProvider, + bip32Account, + chainHistoryProvider, + handleProvider, + inputResolver: { + resolveInput: jest.fn().mockResolvedValue(outputs[0]) + }, + logger, + networkInfoProvider, + rewardAccountInfoProvider, + rewardsProvider, + txSubmitProvider, + utxoProvider, + witnesser + } + ); + + const selector = new LargeFirstSelector({ + changeAddressResolver: new MockChangeAddressResolver() + }); + walletWithMockInputResolver.setInputSelector(selector); + const txBuilder = walletWithMockInputResolver.createTxBuilder(); + const firstAddress = (await firstValueFrom(walletWithMockInputResolver.addresses$))[0].address; + + const builtTx = await txBuilder + .addInput( + [ + { + address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'), + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + }, + outputs[0] + ], + { + script: getPayToPubKeyHashScript( + getPaymentCredential(firstAddress).hash as unknown as Crypto.Ed25519KeyHashHex + ) + } + ) + .addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut()) + .customize(({ txBody }) => ({ + ...txBody, + withdrawals: [] + })) + .build() + .inspect(); + + const { + witness: { signatures } + } = await walletWithMockInputResolver.finalizeTx({ tx: builtTx, witness: builtTx.witness }); + + expect(signatures.size).toBe(1); + }); + it('passes through sender to witnesser', async () => { const sender = { url: 'https://lace.io' }; const txInternals = await wallet.initializeTx(props); diff --git a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts index be943d46596..762075c1817 100644 --- a/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts +++ b/packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts @@ -16,7 +16,14 @@ import { HID } from 'node-hid'; import { Hash32ByteBase16 } from '@cardano-sdk/crypto'; import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; +import { LargeFirstSelector } from '@cardano-sdk/input-selection'; import { LedgerKeyAgent, LedgerTransportType } from '@cardano-sdk/hardware-ledger'; +import { + MockChangeAddressResolver, + getPayToPubKeyHashScript, + getPaymentCredential, + getStakeCredential +} from '../utils'; import { buildDRepAddressFromDRepKey } from '../../util'; import { firstValueFrom } from 'rxjs'; import { getDevices } from '@ledgerhq/hw-transport-node-hid-noevents'; @@ -40,14 +47,6 @@ const cleanupEstablishedConnections = async () => { LedgerKeyAgent.deviceConnections = []; }; -const getStakeCredential = (rewardAccount: Cardano.RewardAccount) => { - const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount); - return { - hash: stakeKeyHash, - type: Cardano.CredentialType.KeyHash - }; -}; - const signAndDecode = async (signWith: Cardano.PaymentAddress | Cardano.RewardAccount, wallet: BaseWallet) => { const dataSignature = await wallet.signData({ payload: HexBlob('abc123'), @@ -357,6 +356,86 @@ describe('LedgerKeyAgent', () => { expect(signatures.size).toBe(3); }); + describe('Native Scripts', () => { + it('can sign transaction with native script - Payment credential', async () => { + const selector = new LargeFirstSelector({ + changeAddressResolver: new MockChangeAddressResolver() + }); + wallet.setInputSelector(selector); + const txBuilder = wallet.createTxBuilder(); + const firstAddress = (await firstValueFrom(wallet.addresses$))[0].address; + + const builtTx = await txBuilder + .addInput( + [ + { + address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'), + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + }, + outputs[0] + ], + { + script: getPayToPubKeyHashScript( + getPaymentCredential(firstAddress).hash as unknown as Crypto.Ed25519KeyHashHex + ) + } + ) + .addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut()) + .customize(({ txBody }) => ({ + ...txBody, + withdrawals: [] + })) + .build() + .inspect(); + + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: builtTx, witness: builtTx.witness }); + + expect(signatures.size).toBe(1); + }); + + it('can sign transaction with native script - Stake credential', async () => { + const selector = new LargeFirstSelector({ + changeAddressResolver: new MockChangeAddressResolver() + }); + wallet.setInputSelector(selector); + const txBuilder = wallet.createTxBuilder(); + + const builtTx = await txBuilder + .addInput( + [ + { + address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'), + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + }, + outputs[0] + ], + { + script: getPayToPubKeyHashScript( + getStakeCredential((await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address) + .hash as unknown as Crypto.Ed25519KeyHashHex + ) + } + ) + .addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut()) + .customize(({ txBody }) => ({ + ...txBody, + withdrawals: [] + })) + .build() + .inspect(); + + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: builtTx, witness: builtTx.witness }); + + expect(signatures.size).toBe(1); + }); + }); + describe('conway-era', () => { describe('ordinary tx mode', () => { let dRepPublicKey: Crypto.Ed25519PublicKeyHex | undefined; diff --git a/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts b/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts index 844cb808d51..1b2b83485e1 100644 --- a/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts +++ b/packages/wallet/test/hardware/trezor/TrezorKeyAgent.test.ts @@ -13,6 +13,8 @@ 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 { LargeFirstSelector } from '@cardano-sdk/input-selection'; +import { MockChangeAddressResolver, getPayToPubKeyHashScript, getPaymentCredential } from '../utils'; import { TrezorKeyAgent } from '@cardano-sdk/hardware-trezor'; import { firstValueFrom } from 'rxjs'; import { dummyLogger as logger } from 'ts-log'; @@ -453,6 +455,107 @@ describe('TrezorKeyAgent', () => { }); }); + describe('Native Scripts', () => { + const outputs = [ + { + address: Cardano.PaymentAddress( + 'addr_test1qpu5vlrf4xkxv2qpwngf6cjhtw542ayty80v8dyr49rf5ewvxwdrt70qlcpeeagscasafhffqsxy36t90ldv06wqrk2qum8x5w' + ), + value: { coins: 11_111_111n } + }, + { + address: Cardano.PaymentAddress( + 'addr_test1qz2fxv2umyhttkxyxp8x0dlpdt3k6cwng5pxj3jhsydzer3jcu5d8ps7zex2k2xt3uqxgjqnnj83ws8lhrn648jjxtwq2ytjqp' + ), + value: { + assets: new Map([ + [AssetId.PXL, 10n], + [AssetId.TSLA, 6n] + ]), + coins: 5n + } + } + ]; + + it('can sign transaction with native script - Payment credential', async () => { + const selector = new LargeFirstSelector({ + changeAddressResolver: new MockChangeAddressResolver() + }); + wallet.setInputSelector(selector); + const txBuilder = wallet.createTxBuilder(); + const firstAddress = (await firstValueFrom(wallet.addresses$))[0].address; + + const builtTx = await txBuilder + .addInput( + [ + { + address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'), + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + }, + outputs[0] + ], + { + script: getPayToPubKeyHashScript( + getPaymentCredential(firstAddress).hash as unknown as Crypto.Ed25519KeyHashHex + ) + } + ) + .addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut()) + .customize(({ txBody }) => ({ + ...txBody, + withdrawals: [] + })) + .build() + .inspect(); + + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: builtTx, witness: builtTx.witness }); + + expect(signatures.size).toBe(1); + }); + + it('can sign transaction with native script - Stake credential', async () => { + const selector = new LargeFirstSelector({ + changeAddressResolver: new MockChangeAddressResolver() + }); + wallet.setInputSelector(selector); + const txBuilder = wallet.createTxBuilder(); + + const builtTx = await txBuilder + .addInput( + [ + { + address: Cardano.PaymentAddress('addr_test1vzztre5epvtj5p72sh28nvrs3e6s4xxn95f66cvg0sqsk7qd3mah0'), + index: 0, + txId: Cardano.TransactionId('0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5') + }, + outputs[0] + ], + { + script: getPayToPubKeyHashScript( + getStakeCredential((await firstValueFrom(wallet.delegation.rewardAccounts$))?.[0].address) + .hash as unknown as Crypto.Ed25519KeyHashHex + ) + } + ) + .addOutput(txBuilder.buildOutput().address(outputs[0].address).coin(BigInt(5_111_111n)).toTxOut()) + .customize(({ txBody }) => ({ + ...txBody, + withdrawals: [] + })) + .build() + .inspect(); + + const { + witness: { signatures } + } = await wallet.finalizeTx({ tx: builtTx, witness: builtTx.witness }); + + expect(signatures.size).toBe(1); + }); + }); + it('can be created with any account index', async () => { const trezorKeyAgentWithRandomIndex = await TrezorKeyAgent.createWithDevice( { diff --git a/packages/wallet/test/hardware/utils.ts b/packages/wallet/test/hardware/utils.ts new file mode 100644 index 00000000000..78832d344f4 --- /dev/null +++ b/packages/wallet/test/hardware/utils.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as Crypto from '@cardano-sdk/crypto'; +import { Cardano } from '@cardano-sdk/core'; +import { ChangeAddressResolver, Selection } from '@cardano-sdk/input-selection'; + +export class MockChangeAddressResolver implements ChangeAddressResolver { + async resolve(selection: Selection) { + return selection.change.map((txOut) => ({ + ...txOut, + address: Cardano.PaymentAddress( + 'addr_test1qqydn46r6mhge0kfpqmt36m6q43knzsd9ga32n96m89px3nuzcjqw982pcftgx53fu5527z2cj2tkx2h8ux2vxsg475qypp3m9' + ) + })); + } +} + +export const getStakeCredential = (rewardAccount: Cardano.RewardAccount) => { + const stakeKeyHash = Cardano.RewardAccount.toHash(rewardAccount); + return { + hash: stakeKeyHash, + type: Cardano.CredentialType.KeyHash + }; +}; + +export const getPaymentCredential = (address: Cardano.PaymentAddress) => + Cardano.Address.fromBech32(address).asBase()!.getPaymentCredential(); + +export const getPayToPubKeyHashScript = (keyHash: Crypto.Ed25519KeyHashHex): Cardano.Script => ({ + __type: Cardano.ScriptType.Native, + keyHash, + kind: Cardano.NativeScriptKind.RequireSignature +}); diff --git a/packages/wallet/test/integration/cip30mapping.test.ts b/packages/wallet/test/integration/cip30mapping.test.ts index 4b5cac5f7a2..d2584333c16 100644 --- a/packages/wallet/test/integration/cip30mapping.test.ts +++ b/packages/wallet/test/integration/cip30mapping.test.ts @@ -1,6 +1,6 @@ /* eslint-disable unicorn/consistent-destructuring */ /* eslint-disable unicorn/consistent-function-scoping */ -/* eslint-disable @typescript-eslint/no-explicit-any, sonarjs/no-duplicate-string */ +/* eslint-disable @typescript-eslint/no-explicit-any, sonarjs/no-duplicate-string, @typescript-eslint/no-floating-promises */ import * as Crypto from '@cardano-sdk/crypto'; import { APIErrorCode, @@ -28,13 +28,14 @@ import { coalesceValueQuantities } from '@cardano-sdk/core'; import { Ed25519KeyHashHex } from '@cardano-sdk/crypto'; -import { HexBlob, ManagedFreeableScope } from '@cardano-sdk/util'; +import { HexBlob } from '@cardano-sdk/util'; import { InMemoryUnspendableUtxoStore, createInMemoryWalletStores } from '../../src/persistence'; import { InitializeTxProps, InitializeTxResult } from '@cardano-sdk/tx-construction'; import { NEVER, Observable, delay, firstValueFrom, of } from 'rxjs'; import { Providers, createWallet } from './util'; import { address_0_0, address_1_0, rewardAccount_0, rewardAccount_1 } from '../services/ChangeAddress/testData'; import { buildDRepAddressFromDRepKey, signTx, waitForWalletStateSettle } from '../util'; +import { getPayToPubKeyHashScript, getPaymentCredential, getStakeCredential } from '../hardware/utils'; import { dummyLogger as logger } from 'ts-log'; import { stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; import uniq from 'lodash/uniq.js'; @@ -48,7 +49,9 @@ const { utxosWithLowCoins } = mocks; -type TestProviders = Required>; +type TestProviders = Required< + Pick +>; const createMockGenericCallback = (result: T) => jest.fn().mockResolvedValue(result); const foreignTx = Serialization.TxCBOR( '84a70081825820dce442e983f3f5cd5b2644bc57f749075390f1fbae9ab55bf454342959c885db00018182583900d161d64eef0eeb59f9124f520f8c8f3b717ed04198d54c8b17e604aea63c153fb3ea8a4ea4f165574ea91173756de0bf30222ca0e95a649a1a0082607b021a0016360509a1581cb77934706fa311b6568d1070c2d23f092324b35ad623aa571a0e3726a14e4d6573685f476966745f43617264200b5820d8175f3b1276a48939a6ccee220a7f81b6422167317ba3ff6325cba1fb6ccbe70d818258208d68748457cd0f1a8596f41fd2125a415315897d2da4a4b94335829cee7198ae001281825820dce442e983f3f5cd5b2644bc57f749075390f1fbae9ab55bf454342959c885db00a2068259016b590168010000333232323232323223223222253330083232533300d3010002132533300b3370e6eb4c034009200113371e0020122940dd718058008b180700099299980499b8748008c028dd50008a5eb7bdb1804dd5980718059baa001323300100132330010013756601e602060206020602060186ea8c03cc030dd50019129998070008a5eb7bdb1804c8c8c8c94ccc03ccdc8a45000021533300f3371e91010000210031005133013337606ea4008dd3000998030030019bab3010003375c601c0046024004602000244a66601a002298103d87a8000132323232533300e337220140042a66601c66e3c0280084cdd2a4000660246e980052f5c02980103d87a80001330060060033756601e0066eb8c034008c044008c03c00452613656375c0026eb80055cd2ab9d5573caae7d5d02ba157449810f4e4d6573685f476966745f43617264004c011e581cb77934706fa311b6568d1070c2d23f092324b35ad623aa571a0e3726000159023c59023901000033323232323232322322232323225333009323232533300c3007300d3754002264646464a666026602c00426464a666024601a60266ea803854ccc048c034c04cdd5191980080080311299980b8008a60103d87a80001323253330163375e603660306ea800804c4cdd2a40006603400497ae0133004004001301b002301900115333012300c00113371e00402029405854ccc048cdc3800a4002266e3c0080405281bad3013002375c60220022c602800264a66601e601260206ea800452f5bded8c026eacc050c044dd500099191980080099198008009bab3016301730173017301700522533301500114bd6f7b630099191919299980b19b91488100002153330163371e9101000021003100513301a337606ea4008dd3000998030030019bab3017003375c602a0046032004602e00244a666028002298103d87a800013232323253330153372200e0042a66602a66e3c01c0084cdd2a4000660326e980052f5c02980103d87a80001330060060033756602c0066eb8c050008c060008c058004dd7180998081baa00337586024002601c6ea800858c040c044008c03c004c02cdd50008a4c26cac64a66601060060022a66601660146ea8010526161533300830020011533300b300a37540082930b0b18041baa003370e90011b8748000dd7000ab9a5573aaae7955cfaba05742ae8930010f4e4d6573685f476966745f43617264004c012bd8799fd8799f58203159a6f2ae24c5bfbed947fe0ecfe936f088c8d265484e6979cacb607d33c811ff05ff0001058284000040821a006acfc01ab2d05e00840100d87a80821a006acfc01ab2d05e00f5f6' @@ -107,8 +110,10 @@ describe('cip30', () => { beforeEach(async () => { providers = { + chainHistoryProvider: mockChainHistoryProvider({ rewardAccount: rewardAccount_0 }), networkInfoProvider: mockNetworkInfoProvider(), - txSubmitProvider: mockTxSubmitProvider() + txSubmitProvider: mockTxSubmitProvider(), + utxoProvider: mocks.mockUtxoProvider({ address: address_0_0 }) }; providers.networkInfoProvider.ledgerTip.mockImplementation( () => @@ -146,22 +151,23 @@ describe('cip30', () => { // eslint-disable-next-line sonarjs/cognitive-complexity describe('with default mock data', () => { - let scope: ManagedFreeableScope; let providers: TestProviders; beforeAll(async () => { // CREATE A WALLET - scope = new ManagedFreeableScope(); providers = { + chainHistoryProvider: mockChainHistoryProvider({ + rewardAccount: rewardAccount_0 + }), networkInfoProvider: mockNetworkInfoProvider(), - txSubmitProvider: mockTxSubmitProvider() + txSubmitProvider: mockTxSubmitProvider(), + utxoProvider: mocks.mockUtxoProvider({ address: address_0_0 }) }; ({ wallet, api, confirmationCallback } = await createWalletAndApiWithStores([mockUtxo[4]], providers)); }); afterAll(() => { wallet.shutdown(); - scope.dispose(); }); describe('createWalletApi', () => { @@ -512,6 +518,50 @@ describe('cip30', () => { hexTx = Serialization.Transaction.fromCore(finalizedTx).toCbor(); }); + describe('Native Scripts', () => { + const testNativeScriptSigning = async ( + label: string, + getKeyHash: (groupedAddress: GroupedAddress) => Crypto.Ed25519KeyHashHex + ) => { + it(label, async () => { + const tx = await signTx({ + addresses$: wallet.addresses$, + tx: await wallet.initializeTx(simpleTxProps), + walletUtil: wallet.util + }); + + const knownAddresses = await firstValueFrom(wallet.addresses$); + + // Make all inputs foreign, delete any signatures, withdrawals and populate scripts. + for (const input of tx.body.inputs) { + input.txId = Cardano.TransactionId('0'.repeat(64)); + } + tx.body.withdrawals = []; + tx.witness.signatures = new Map(); + tx.witness.scripts = [getPayToPubKeyHashScript(getKeyHash(knownAddresses[0]))]; + + const cbor = Serialization.Transaction.fromCore(tx).toCbor(); + const cip30witnessSetCbor = await api.signTx(context, cbor, true); + const { signatures } = Serialization.TransactionWitnessSet.fromCbor( + HexBlob(cip30witnessSetCbor) + ).toCore(); + + expect(signatures.size).toBe(1); + }); + }; + + testNativeScriptSigning( + 'can sign transaction with native script - Payment credential', + (groupedAddress) => getPaymentCredential(groupedAddress.address).hash as unknown as Crypto.Ed25519KeyHashHex + ); + + testNativeScriptSigning( + 'can sign transaction with native script - Stake credential', + (groupedAddress) => + getStakeCredential(groupedAddress.rewardAccount).hash as unknown as Crypto.Ed25519KeyHashHex + ); + }); + it('resolves own signatures with TransactionWitnessSet', async () => { const cip30witnessSetCbor = await api.signTx(context, hexTx); const cip30witnessSet = Serialization.TransactionWitnessSet.fromCbor(HexBlob(cip30witnessSetCbor));