Skip to content

Commit

Permalink
Merge pull request #1411 from input-output-hk/feat/add-update-signatu…
Browse files Browse the repository at this point in the history
…res-method-to-base-wallet

feat!: replace updateWitness with addSignatures in observable wallet
  • Loading branch information
AngelCastilloB authored Aug 7, 2024
2 parents 308b003 + d0bdffa commit 446232b
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 48 deletions.
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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}`);
Expand Down Expand Up @@ -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) => {
Expand Down Expand Up @@ -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);
});
});
14 changes: 9 additions & 5 deletions packages/e2e/test/wallet_epoch_0/SharedWallet/simpleTx.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
19 changes: 12 additions & 7 deletions packages/e2e/test/wallet_epoch_3/SharedWallet/delegation.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down
52 changes: 35 additions & 17 deletions packages/wallet/src/Wallets/BaseWallet.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Cardano.Tx> {
return this.finalizeTx({
auxiliaryData: tx.auxiliaryData,
async addSignatures({ tx, sender }: AddSignaturesProps): Promise<Serialization.TxCBOR> {
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();
}
}
7 changes: 5 additions & 2 deletions packages/wallet/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export type FinalizeTxProps = Omit<TxContext, 'signingContext'> & {
signingContext?: Partial<SignTransactionContext>;
};

export type UpdateWitnessProps = {
tx: Cardano.Tx;
export type AddSignaturesProps = {
tx: Serialization.TxCBOR;
sender?: MessageSender;
};

Expand Down Expand Up @@ -144,6 +144,9 @@ export interface ObservableWallet {
*/
getNextUnusedAddress(): Promise<WalletAddress[]>;

/** Updates the transaction witness set with signatures from this wallet. */
addSignatures(props: AddSignaturesProps): Promise<Serialization.TxCBOR>;

shutdown(): void;
}

Expand Down
74 changes: 74 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,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';
Expand Down Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions packages/web-extension/src/observableWallet/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export const txBuilderProperties: RemoteApiProperties<Omit<TxBuilder, 'customize
};

export const observableWalletProperties: RemoteApiProperties<ObservableWallet> = {
addSignatures: RemoteApiPropertyType.MethodReturningPromise,
addresses$: RemoteApiPropertyType.HotObservable,
assetInfo$: RemoteApiPropertyType.HotObservable,
balance: {
Expand Down

0 comments on commit 446232b

Please sign in to comment.