Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions packages/key-management/src/util/ownSignatureKeyPaths.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 };
};

Expand All @@ -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));
}
Expand Down
9 changes: 5 additions & 4 deletions packages/key-management/test/util/ownSignaturePaths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -653,7 +653,7 @@ describe('KeyManagement.util.ownSignaturePaths', () => {
scripts: [
{
__type: Cardano.ScriptType.Native,
keyHash: Ed25519KeyHashHex('b498c0eaceb9a8c7c829d36fc84e892113c9d2636b53b0636d7518b4'),
keyHash: Ed25519KeyHashHex('cefd48e6c035876a52b0926265c4e663113a7caf653e28f7ed8344ef'),
kind: Cardano.NativeScriptKind.RequireSignature
},
{
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module.exports = {
...require('../../test/jest.config'),
setupFiles: ['jest-webextension-mock']
setupFiles: ['jest-webextension-mock', './jest.setup.js']
};
18 changes: 18 additions & 0 deletions packages/wallet/jest.setup.js
Original file line number Diff line number Diff line change
@@ -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];
}
}
};
}
7 changes: 6 additions & 1 deletion packages/wallet/src/Wallets/BaseWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
31 changes: 10 additions & 21 deletions packages/wallet/src/cip30.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -370,23 +370,16 @@ const baseCip30WalletApi = (
return addresses.map((groupAddresses) => cardanoAddressToCbor(groupAddresses.address));
},
getUtxos: async (_: SenderContext, amount?: Cbor, paginate?: Paginate): Promise<Cbor[] | null> => {
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,
Expand Down Expand Up @@ -424,7 +417,6 @@ const baseCip30WalletApi = (
throw new DataSignError(DataSignErrorCode.UserDeclined, 'user declined signing');
},
signTx: async ({ sender }: SenderContext, tx: Cbor, partialSign?: Boolean): Promise<Cbor> => {
const scope = new ManagedFreeableScope();
logger.debug('signTx', tx);
const txCbor = Serialization.TxCBOR(tx);
const txDecoded = Serialization.Transaction.fromCbor(txCbor);
Expand Down Expand Up @@ -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');
}
},
Expand Down
62 changes: 62 additions & 0 deletions packages/wallet/test/PersonalWallet/methods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
95 changes: 87 additions & 8 deletions packages/wallet/test/hardware/ledger/LedgerKeyAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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'),
Expand Down Expand Up @@ -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;
Expand Down
Loading
Loading