Skip to content

feat!: replace updateWitness with addSignatures in observable wallet #1411

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
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
Loading