diff --git a/packages/e2e/test/long-running/shared-wallet-delegation-rewards.test.ts b/packages/e2e/test/long-running/shared-wallet-delegation-rewards.test.ts index 4bf2c622b14..f261a621dca 100644 --- a/packages/e2e/test/long-running/shared-wallet-delegation-rewards.test.ts +++ b/packages/e2e/test/long-running/shared-wallet-delegation-rewards.test.ts @@ -1,5 +1,5 @@ import { BaseWallet } from '@cardano-sdk/wallet'; -import { Cardano, StakePoolProvider } from '@cardano-sdk/core'; +import { Cardano, Serialization, StakePoolProvider } from '@cardano-sdk/core'; import { buildSharedWallets } from '../wallet_epoch_0/SharedWallet/utils'; import { filter, firstValueFrom, map, take } from 'rxjs'; import { @@ -20,11 +20,15 @@ const env = getEnv(walletVariables); const submitDelegationTx = async (alice: BaseWallet, bob: BaseWallet, charlotte: BaseWallet, pool: Cardano.PoolId) => { logger.info(`Creating delegation tx at epoch #${(await firstValueFrom(alice.currentEpoch$)).epochNo}`); - let tx = (await alice.createTxBuilder().delegateFirstStakeCredential(pool).build().sign()).tx; + const tx = (await alice.createTxBuilder().delegateFirstStakeCredential(pool).build().sign()).tx; - tx = await bob.updateWitness({ sender: { id: 'e2e' }, tx }); - tx = await charlotte.updateWitness({ sender: { id: 'e2e' }, tx }); - await alice.submitTx(tx); + // Serialize and transmit TX... + let serializedTx = Serialization.Transaction.fromCore(tx).toCbor(); + + serializedTx = await bob.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + serializedTx = await charlotte.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + + await alice.submitTx(serializedTx); const { epochNo } = await firstValueFrom(alice.currentEpoch$); logger.info(`Delegation tx ${tx.id} submitted at epoch #${epochNo}`); @@ -61,15 +65,18 @@ const buildSpendRewardTx = async ( const { body } = await tx.inspect(); logger.debug('Body of tx before sign'); logger.debug(body); - let signedTx = (await tx.sign()).tx; + const signedTx = (await tx.sign()).tx; + + // Serialize and transmit TX... + let serializedTx = Serialization.Transaction.fromCore(signedTx).toCbor(); - signedTx = await bob.updateWitness({ sender: { id: 'e2e' }, tx: signedTx }); - signedTx = await charlotte.updateWitness({ sender: { id: 'e2e' }, tx: signedTx }); + serializedTx = await bob.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + serializedTx = await charlotte.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); logger.debug('Body of tx after sign'); logger.debug(signedTx.body); - return signedTx; + return serializedTx; }; const getPoolIds = async (stakePoolProvider: StakePoolProvider, count: number) => { @@ -187,13 +194,11 @@ describe('shared wallet delegation rewards', () => { logger.info(`Generated rewards: ${rewards} tLovelace`); // Spend reward - const spendRewardTx = await buildSpendRewardTx( - aliceMultiSigWallet, - bobMultiSigWallet, - charlotteMultiSigWallet, - faucetWallet - ); - expect(spendRewardTx.body.withdrawals?.length).toBeGreaterThan(0); - await submitAndConfirm(aliceMultiSigWallet, spendRewardTx); + const spendRewardsTx = Serialization.Transaction.fromCbor( + await buildSpendRewardTx(aliceMultiSigWallet, bobMultiSigWallet, charlotteMultiSigWallet, faucetWallet) + ).toCore(); + + expect(spendRewardsTx.body.withdrawals?.length).toBeGreaterThan(0); + await submitAndConfirm(aliceMultiSigWallet, spendRewardsTx); }); }); diff --git a/packages/e2e/test/wallet_epoch_0/SharedWallet/simpleTx.test.ts b/packages/e2e/test/wallet_epoch_0/SharedWallet/simpleTx.test.ts index fb9fe39480a..463228f0be7 100644 --- a/packages/e2e/test/wallet_epoch_0/SharedWallet/simpleTx.test.ts +++ b/packages/e2e/test/wallet_epoch_0/SharedWallet/simpleTx.test.ts @@ -1,5 +1,5 @@ import { BaseWallet } from '@cardano-sdk/wallet'; -import { Cardano } from '@cardano-sdk/core'; +import { Cardano, Serialization } from '@cardano-sdk/core'; import { buildSharedWallets } from './utils'; import { filter, firstValueFrom, map, take } from 'rxjs'; import { @@ -97,14 +97,18 @@ describe('SharedWallet/simpleTx', () => { // Alice will initiate the transaction. const txBuilder = aliceMultiSigWallet.createTxBuilder(); const txOut = await txBuilder.buildOutput().address(faucetAddress).coin(1_000_000n).build(); - let tx = (await txBuilder.addOutput(txOut).build().sign()).tx; + const tx = (await txBuilder.addOutput(txOut).build().sign()).tx; + + // Serialize and transmit TX... + let serializedTx = Serialization.Transaction.fromCore(tx).toCbor(); // Bob updates the transaction with his witness - tx = await bobMultiSigWallet.updateWitness({ sender: { id: 'e2e' }, tx }); + serializedTx = await bobMultiSigWallet.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); // Charlotte updates the transaction with her witness - tx = await charlotteMultiSigWallet.updateWitness({ sender: { id: 'e2e' }, tx }); - const txId = await charlotteMultiSigWallet.submitTx(tx); + serializedTx = await charlotteMultiSigWallet.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + + const txId = await charlotteMultiSigWallet.submitTx(serializedTx); const finalTxFound = await firstValueFrom( aliceMultiSigWallet.transactions.history$.pipe( diff --git a/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts b/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts index 7ebd854505d..016e597b125 100644 --- a/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts +++ b/packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts @@ -1,7 +1,7 @@ /* eslint-disable max-statements */ import { BaseWallet, ObservableWallet } from '@cardano-sdk/wallet'; import { BigIntMath, isNotNil } from '@cardano-sdk/util'; -import { Cardano, StakePoolProvider } from '@cardano-sdk/core'; +import { Cardano, Serialization, StakePoolProvider } from '@cardano-sdk/core'; import { TX_TIMEOUT_DEFAULT, firstValueFromTimed, @@ -172,9 +172,12 @@ describe('SharedWallet/delegation', () => { .sign() ).tx; - tx = await bobMultiSigWallet.updateWitness({ sender: { id: 'e2e' }, tx }); - tx = await charlotteMultiSigWallet.updateWitness({ sender: { id: 'e2e' }, tx }); - await aliceMultiSigWallet.submitTx(tx); + // Serialize and transmit TX... + let serializedTx = Serialization.Transaction.fromCore(tx).toCbor(); + + serializedTx = await bobMultiSigWallet.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + serializedTx = await charlotteMultiSigWallet.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + await aliceMultiSigWallet.submitTx(serializedTx); // Test it locks available balance after tx is submitted await firstValueFromTimed( @@ -224,10 +227,12 @@ describe('SharedWallet/delegation', () => { // Make a 2nd tx with key de-registration tx = (await aliceMultiSigWallet.createTxBuilder().delegateFirstStakeCredential(null).build().sign()).tx; - tx = await bobMultiSigWallet.updateWitness({ sender: { id: 'e2e' }, tx }); - tx = await charlotteMultiSigWallet.updateWitness({ sender: { id: 'e2e' }, tx }); + serializedTx = Serialization.Transaction.fromCore(tx).toCbor(); + + serializedTx = await bobMultiSigWallet.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); + serializedTx = await charlotteMultiSigWallet.addSignatures({ sender: { id: 'e2e' }, tx: serializedTx }); - await aliceMultiSigWallet.submitTx(tx); + await aliceMultiSigWallet.submitTx(serializedTx); await waitForTx(aliceMultiSigWallet, tx.id); const tx2ConfirmedState = await getWalletStateSnapshot(aliceMultiSigWallet); diff --git a/packages/wallet/src/Wallets/BaseWallet.ts b/packages/wallet/src/Wallets/BaseWallet.ts index 1444fc0be72..23dd55c7cae 100644 --- a/packages/wallet/src/Wallets/BaseWallet.ts +++ b/packages/wallet/src/Wallets/BaseWallet.ts @@ -1,5 +1,16 @@ /* eslint-disable unicorn/no-nested-ternary */ // eslint-disable-next-line import/no-extraneous-dependencies +import { + AddSignaturesProps, + Assets, + FinalizeTxProps, + HandleInfo, + ObservableWallet, + SignDataProps, + SyncStatus, + WalletAddress, + WalletNetworkInfoProvider +} from '../types'; import { AddressDiscovery, AddressTracker, @@ -56,17 +67,6 @@ import { TxSubmitProvider, UtxoProvider } from '@cardano-sdk/core'; -import { - Assets, - FinalizeTxProps, - HandleInfo, - ObservableWallet, - SignDataProps, - SyncStatus, - UpdateWitnessProps, - WalletAddress, - WalletNetworkInfoProvider -} from '../types'; import { BehaviorObservable, TrackerSubject, coldObservableProvider } from '@cardano-sdk/util-rxjs'; import { BehaviorSubject, @@ -878,15 +878,33 @@ export class BaseWallet implements ObservableWallet { return isEmpty ? [knownAddresses[0]] : []; } - /** Update the witness of a transaction with witness provided by this wallet */ - async updateWitness({ tx, sender }: UpdateWitnessProps): Promise { - return this.finalizeTx({ - auxiliaryData: tx.auxiliaryData, + async addSignatures({ tx, sender }: AddSignaturesProps): Promise { + const serializableTx = Serialization.Transaction.fromCbor(tx); + const auxiliaryData = serializableTx.auxiliaryData()?.toCore(); + const body = serializableTx.body().toCore(); + const hash = serializableTx.getId(); + const witness = serializableTx.witnessSet().toCore(); + const bodyCbor = serializableTx.body().toCbor(); + + const witnessedTx = await this.finalizeTx({ + auxiliaryData, + bodyCbor, signingContext: { sender }, - tx: { body: tx.body, hash: tx.id }, - witness: tx.witness + tx: { body, hash }, + witness }); + + const coreWitness = witnessedTx.witness; + const witnessSet = serializableTx.witnessSet(); + + witnessSet.setVkeys( + Serialization.CborSet.fromCore([...coreWitness.signatures], Serialization.VkeyWitness.fromCore) + ); + + serializableTx.setWitnessSet(witnessSet); + + return serializableTx.toCbor(); } } diff --git a/packages/wallet/src/types.ts b/packages/wallet/src/types.ts index 9c217e12b70..aa2ccd57ec4 100644 --- a/packages/wallet/src/types.ts +++ b/packages/wallet/src/types.ts @@ -55,8 +55,8 @@ export type FinalizeTxProps = Omit & { signingContext?: Partial; }; -export type UpdateWitnessProps = { - tx: Cardano.Tx; +export type AddSignaturesProps = { + tx: Serialization.TxCBOR; sender?: MessageSender; }; @@ -144,6 +144,9 @@ export interface ObservableWallet { */ getNextUnusedAddress(): Promise; + /** Updates the transaction witness set with signatures from this wallet. */ + addSignatures(props: AddSignaturesProps): Promise; + shutdown(): void; } diff --git a/packages/wallet/test/PersonalWallet/methods.test.ts b/packages/wallet/test/PersonalWallet/methods.test.ts index f54848b6707..148ca20b9e9 100644 --- a/packages/wallet/test/PersonalWallet/methods.test.ts +++ b/packages/wallet/test/PersonalWallet/methods.test.ts @@ -19,6 +19,7 @@ import { } from '@cardano-sdk/core'; import { HexBlob } from '@cardano-sdk/util'; import { InitializeTxProps } from '@cardano-sdk/tx-construction'; +import { babbageTx } from '../../../core/test/Serialization/testData'; import { buildDRepIDFromDRepKey, toOutgoingTx, waitForWalletStateSettle } from '../util'; import { getPassphrase, stakeKeyDerivationPath, testAsyncKeyAgent } from '../../../key-management/test/mocks'; import { dummyLogger as logger } from 'ts-log'; @@ -534,6 +535,79 @@ describe('BaseWallet methods', () => { }); }); + describe('addSignatures', () => { + it('adds the signatures and preserves all previous witnesses', async () => { + const mockWitnesser = { + signData: jest.fn(), + witness: jest.fn().mockResolvedValue({ + cbor: Serialization.Transaction.fromCore(babbageTx).toCbor(), + context: { + handleResolutions: [] + }, + tx: { + ...babbageTx, + witness: { + ...babbageTx.witness, + signatures: new Map([ + ...babbageTx.witness.signatures.entries(), + [ + '0000000000000000000000000000000000000000000000000000000000000000', + '0000000000000000000000000000000000000000000000000000000000000000' + ] + ]) + } + } + }) + }; + + wallet.shutdown(); + wallet = createPersonalWallet( + { name: 'Test Wallet' }, + { + addressDiscovery, + assetProvider, + bip32Account, + chainHistoryProvider, + handleProvider, + logger, + networkInfoProvider, + rewardsProvider, + stakePoolProvider, + txSubmitProvider, + utxoProvider, + witnesser: mockWitnesser + } + ); + + await waitForWalletStateSettle(wallet); + + const serializedTx = Serialization.Transaction.fromCore(babbageTx).toCbor(); + const tx = await wallet.addSignatures({ tx: serializedTx }); + const updatedTx = Serialization.Transaction.fromCbor(tx).toCore(); + + expect(babbageTx.witness.bootstrap).toEqual(updatedTx.witness.bootstrap); + expect(babbageTx.witness.datums).toEqual(updatedTx.witness.datums); + expect(babbageTx.witness.redeemers).toEqual(updatedTx.witness.redeemers); + expect(babbageTx.witness.scripts).toEqual(updatedTx.witness.scripts); + + for (const [key, value] of Object.entries(babbageTx.witness.signatures)) { + expect(value).toEqual(updatedTx.witness.signatures.get(key as Crypto.Ed25519PublicKeyHex)); + } + + expect(updatedTx.witness.signatures.size).toEqual(babbageTx.witness.signatures.size + 1); + expect( + updatedTx.witness.signatures.get( + '0000000000000000000000000000000000000000000000000000000000000000' as Crypto.Ed25519PublicKeyHex + ) + ).toEqual('0000000000000000000000000000000000000000000000000000000000000000'); + + // signed$ emits transaction and all its witnesses + const signedTxs = await firstValueFrom(wallet.transactions.outgoing.signed$); + + expect(signedTxs[0].tx).toEqual(updatedTx); + }); + }); + // eslint-disable-next-line sonarjs/cognitive-complexity describe('getNextUnusedAddress', () => { const script: Cardano.NativeScript = { diff --git a/packages/web-extension/src/observableWallet/util.ts b/packages/web-extension/src/observableWallet/util.ts index 683f25501b2..211636da25c 100644 --- a/packages/web-extension/src/observableWallet/util.ts +++ b/packages/web-extension/src/observableWallet/util.ts @@ -98,6 +98,7 @@ export const txBuilderProperties: RemoteApiProperties = { + addSignatures: RemoteApiPropertyType.MethodReturningPromise, addresses$: RemoteApiPropertyType.HotObservable, assetInfo$: RemoteApiPropertyType.HotObservable, balance: {