From 431cf51a1903eaf7ece50228c587ebea4ccd5fc9 Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Wed, 7 Sep 2022 14:06:16 +0200 Subject: [PATCH 1/4] feat!: txBuilder postpone adding certificates until build Improve UX by allowing the user to run delegate(...) synchronously. Actual rewards accounts fetch and adding certificates are added on build(). BREAKING CHANGE: TxBuilder.delegate returns synchronously. No await needed anymore. --- .../SingleAddressWallet/delegation.test.ts | 4 +- packages/wallet/src/TxBuilder/buildTx.ts | 63 +++++++++++++------ packages/wallet/src/TxBuilder/types.ts | 12 ++-- .../wallet/test/integration/buildTx.test.ts | 51 +++++++-------- 4 files changed, 74 insertions(+), 56 deletions(-) diff --git a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts index 0c82679afcc..1ff542a4a34 100644 --- a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts +++ b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts @@ -100,11 +100,11 @@ describe('SingleAddressWallet/delegation', () => { // Make a 1st tx with key registration (if not already registered) and stake delegation // Also send some coin to another wallet const destAddresses = (await firstValueFrom(destWallet.addresses$))[0].address; - const txBuilder = await buildTx(sourceWallet).delegate(poolId); + const txBuilder = buildTx(sourceWallet); const maybeValidTxOut = await txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).build(); assertTxOutIsValid(maybeValidTxOut); - const tx = await txBuilder.addOutput(maybeValidTxOut.txOut).build(); + const tx = await txBuilder.addOutput(maybeValidTxOut.txOut).delegate(poolId).build(); assertTxIsValid(tx); const signedTx = await tx.sign(); diff --git a/packages/wallet/src/TxBuilder/buildTx.ts b/packages/wallet/src/TxBuilder/buildTx.ts index bf2849199db..bc18f7f2f2b 100644 --- a/packages/wallet/src/TxBuilder/buildTx.ts +++ b/packages/wallet/src/TxBuilder/buildTx.ts @@ -34,6 +34,16 @@ export type ObservableWalletTxBuilderDependencies = Pick ReturnType; } & WalletUtilContext; +interface Delegate { + type: 'delegate'; + poolId: Cardano.PoolId; +} + +interface KeyDeregistration { + type: 'deregister'; +} +type DelegateConfig = Delegate | KeyDeregistration; + /** * Transactions built with {@link ObservableWalletTxBuilder.build} method, use this method to sign the transaction. * @@ -66,6 +76,7 @@ export class ObservableWalletTxBuilder implements TxBuilder { #observableWallet: ObservableWalletTxBuilderDependencies; #util: OutputValidator; + #delegateConfig: DelegateConfig; /** * @param observableWallet minimal ObservableWallet needed to do actions like {@link build()}, {@link delegate()} etc. @@ -96,26 +107,8 @@ export class ObservableWalletTxBuilder implements TxBuilder { return new ObservableWalletTxOutputBuilder(this.#util, txOut); } - async delegate(poolId: Cardano.PoolId): Promise { - const rewardAccounts = await firstValueFrom(this.#observableWallet.delegation.rewardAccounts$); - - if (!rewardAccounts?.length) { - // This shouldn't happen - throw new IncompatibleWalletError(); - } - - // Discard previous delegation and prepare for new one - this.partialTxBody = { ...this.partialTxBody, certificates: [] }; - - for (const rewardAccount of rewardAccounts) { - const stakeKeyHash = Cardano.Ed25519KeyHash.fromRewardAccount(rewardAccount.address); - - if (rewardAccount.keyStatus === StakeKeyStatus.Unregistered) { - this.partialTxBody.certificates!.push(ObservableWalletTxBuilder.#createStakeKeyCert(stakeKeyHash)); - } - - this.partialTxBody.certificates!.push(ObservableWalletTxBuilder.#createDelegationCert(poolId, stakeKeyHash)); - } + delegate(poolId: Cardano.PoolId): TxBuilder { + this.#delegateConfig = { poolId, type: 'delegate' }; return this; } @@ -136,6 +129,7 @@ export class ObservableWalletTxBuilder implements TxBuilder { async build(): Promise { try { + await this.#addDelegationCertificates(); await this.#validateOutputs(); const tx = await this.#observableWallet.initializeTx({ @@ -183,6 +177,35 @@ export class ObservableWalletTxBuilder implements TxBuilder { return []; } + async #addDelegationCertificates(): Promise { + // Deregistration not implemented yet + if (!this.#delegateConfig || this.#delegateConfig.type === 'deregister') { + return Promise.resolve(); + } + + const rewardAccounts = await firstValueFrom(this.#observableWallet.delegation.rewardAccounts$); + + if (!rewardAccounts?.length) { + // This shouldn't happen + throw new IncompatibleWalletError(); + } + + // Discard previous delegation and prepare for new one + this.partialTxBody = { ...this.partialTxBody, certificates: [] }; + + for (const rewardAccount of rewardAccounts) { + const stakeKeyHash = Cardano.Ed25519KeyHash.fromRewardAccount(rewardAccount.address); + + if (rewardAccount.keyStatus === StakeKeyStatus.Unregistered) { + this.partialTxBody.certificates!.push(ObservableWalletTxBuilder.#createStakeKeyCert(stakeKeyHash)); + } + + this.partialTxBody.certificates!.push( + ObservableWalletTxBuilder.#createDelegationCert(this.#delegateConfig.poolId, stakeKeyHash) + ); + } + } + static #createDelegationCert( poolId: Cardano.PoolId, stakeKeyHash: Cardano.Ed25519KeyHash diff --git a/packages/wallet/src/TxBuilder/types.ts b/packages/wallet/src/TxBuilder/types.ts index d16ebfb110d..ee82d64e43c 100644 --- a/packages/wallet/src/TxBuilder/types.ts +++ b/packages/wallet/src/TxBuilder/types.ts @@ -38,7 +38,7 @@ export type TxOutValidationError = | OutputValidationMissingRequiredError | OutputValidationMinimumCoinError | OutputValidationTokenBundleSizeError; -export type TxBodyValidationError = TxOutValidationError | InputSelectionError; +export type TxBodyValidationError = TxOutValidationError | InputSelectionError | IncompatibleWalletError; export type Valid = TValid & { isValid: true; @@ -148,14 +148,14 @@ export interface TxBuilder { */ buildOutput(txOut?: PartialTxOut): OutputBuilder; /** - * Add StakeDelegation and (if needed) StakeKeyRegistration certificate to {@link partialTxBody}. - * If wallet contains multiple reward accounts, it will create certificates for all of them. - * The call returns a Promise because it waits for the reward accounts to be returned by the wallet. + * Configure transaction to include delegation. + * - On `build()`, StakeDelegation and (if needed) StakeKeyRegistration certificates are added in + * the transaction body. + * - If wallet contains multiple reward accounts, it will create certificates for all of them. * * @param poolId Pool Id to delegate to. - * @throws `IncompatibleWalletError` if no reward accounts are provided by the wallet. */ - delegate(poolId: Cardano.PoolId): Promise; + delegate(poolId: Cardano.PoolId): TxBuilder; /** Sets TxMetadata in {@link auxiliaryData} */ setMetadata(metadata: Cardano.TxMetadata): TxBuilder; /** Sets extra signers in {@link extraSigners} */ diff --git a/packages/wallet/test/integration/buildTx.test.ts b/packages/wallet/test/integration/buildTx.test.ts index 0540c4d1be5..949f54a43be 100644 --- a/packages/wallet/test/integration/buildTx.test.ts +++ b/packages/wallet/test/integration/buildTx.test.ts @@ -320,7 +320,7 @@ describe('buildTx', () => { it('certificates are added to tx.body on build', async () => { const address = Cardano.Address('addr_test1vr8nl4u0u6fmtfnawx2rxfz95dy7m46t6dhzdftp2uha87syeufdg'); - await txBuilder.delegate(poolId); + txBuilder.delegate(poolId); const maybeValidTxOut = await txBuilder.buildOutput().address(address).coin(10_000_000n).build(); assertTxOutIsValid(maybeValidTxOut); const txBuilt = await txBuilder.addOutput(maybeValidTxOut.txOut).build(); @@ -330,43 +330,36 @@ describe('buildTx', () => { }); it('adds both stake key and delegation certificates when reward account was not registered', async () => { - const txDelegate = await txBuilder.delegate(poolId); - const [stakeKeyCert, delegationCert] = txDelegate.partialTxBody.certificates!; + const txDelegate = await txBuilder.delegate(poolId).build(); + assertTxIsValid(txDelegate); + const [stakeKeyCert, delegationCert] = txDelegate.body.certificates!; expect(stakeKeyCert.__typename).toBe(Cardano.CertificateType.StakeKeyRegistration); if (delegationCert.__typename === Cardano.CertificateType.StakeDelegation) { expect(delegationCert.poolId).toBe(poolId); } - expect.assertions(2); + expect.assertions(3); }); it('delegate again removes previous certificates', async () => { - await txBuilder.delegate(poolId); + await txBuilder.delegate(poolId).build(); const poolIdOther = somePartialStakePools[1].id; - const secondDelegation = await txBuilder.delegate(poolIdOther); - expect(secondDelegation.partialTxBody.certificates?.length).toBe(2); - const delegationCert = secondDelegation.partialTxBody.certificates![1] as Cardano.StakeDelegationCertificate; + const secondDelegation = await txBuilder.delegate(poolIdOther).build(); + assertTxIsValid(secondDelegation); + expect(secondDelegation.body.certificates?.length).toBe(2); + const delegationCert = secondDelegation.body.certificates![1] as Cardano.StakeDelegationCertificate; expect(delegationCert.poolId).toBe(poolIdOther); }); - it('always recreates partialTxBody', async () => { - const txDelegate = await txBuilder.delegate(poolId); - const { partialTxBody } = txDelegate; - - const poolIdOther = somePartialStakePools[1].id; - const txDelegateOther = await txBuilder.delegate(poolIdOther); - assertObjectRefsAreDifferent(txDelegateOther.partialTxBody, partialTxBody); - }); - it('throws IncompatibleWallet error if no reward accounts were found', async () => { wallet.delegation.rewardAccounts$ = of([]); - try { - await txBuilder.delegate(poolId); - } catch (error) { - expect(error instanceof IncompatibleWalletError).toBeTruthy(); + const txBuilt = await txBuilder.delegate(poolId).build(); + if (!txBuilt.isValid) { + expect(txBuilt.errors?.length).toBe(1); + expect(txBuilt.errors[0] instanceof IncompatibleWalletError).toBeTruthy(); } - expect.assertions(1); + expect.assertions(2); }); it('adds only delegation certificate with correct poolId when reward account was already registered', async () => { @@ -382,14 +375,15 @@ describe('buildTx', () => { rewardBalance: 33_333n } ]); - const txDelegate = await txBuilder.delegate(poolId); - expect(txDelegate.partialTxBody.certificates?.length).toBe(1); - const [delegationCert] = txDelegate.partialTxBody.certificates!; + const txDelegate = await txBuilder.delegate(poolId).build(); + assertTxIsValid(txDelegate); + expect(txDelegate.body.certificates?.length).toBe(1); + const [delegationCert] = txDelegate.body.certificates!; if (delegationCert.__typename === Cardano.CertificateType.StakeDelegation) { expect(delegationCert.poolId).toBe(poolId); } - expect.assertions(2); + expect.assertions(3); }); it('adds multiple certificates when handling multiple reward accounts', async () => { @@ -416,8 +410,9 @@ describe('buildTx', () => { } ]); - const txDelegate = await txBuilder.delegate(poolId); - expect(txDelegate.partialTxBody.certificates?.length).toBe(4); + const txDelegate = await txBuilder.delegate(poolId).build(); + assertTxIsValid(txDelegate); + expect(txDelegate.body.certificates?.length).toBe(4); }); }); From b0d335861e2fa2274740f34240dba041e295fef2 Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Wed, 7 Sep 2022 15:39:05 +0200 Subject: [PATCH 2/4] feat: txBuilder deregister stake key cert --- .../SingleAddressWallet/delegation.test.ts | 18 +++------ packages/wallet/src/TxBuilder/buildTx.ts | 40 ++++++++++--------- packages/wallet/src/TxBuilder/types.ts | 9 +++-- .../wallet/test/integration/buildTx.test.ts | 26 ++++++++++++ 4 files changed, 59 insertions(+), 34 deletions(-) diff --git a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts index 1ff542a4a34..57ec4fff924 100644 --- a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts +++ b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts @@ -84,7 +84,6 @@ describe('SingleAddressWallet/delegation', () => { test('balance & transaction', async () => { // source wallet has the highest balance to begin with const [sourceWallet, destWallet] = await chooseWallets(); - const [{ rewardAccount }] = await firstValueFrom(sourceWallet.addresses$); const protocolParameters = await firstValueFrom(sourceWallet.protocolParameters$); const stakeKeyDeposit = BigInt(protocolParameters.stakeKeyDeposit); @@ -148,23 +147,18 @@ describe('SingleAddressWallet/delegation', () => { } // Make a 2nd tx with key deregistration - const tx2Internals = await sourceWallet.initializeTx({ - certificates: [ - { - __typename: Cardano.CertificateType.StakeKeyDeregistration, - stakeKeyHash: Cardano.Ed25519KeyHash.fromRewardAccount(rewardAccount) - } - ] - }); - await sourceWallet.submitTx(await sourceWallet.finalizeTx({ tx: tx2Internals })); - await waitForTx(sourceWallet, tx2Internals.hash); + const txDeregister = await buildTx(sourceWallet).delegate().build(); + assertTxIsValid(txDeregister); + const txDeregisterSigned = await txDeregister.sign(); + await txDeregisterSigned.submit(); + await waitForTx(sourceWallet, txDeregisterSigned.tx.id); const tx2ConfirmedState = await getWalletStateSnapshot(sourceWallet); // No longer delegating expect(tx2ConfirmedState.rewardAccount.delegatee?.nextNextEpoch?.id).toBeUndefined(); // Deposit is returned to wallet balance - const expectedCoinsAfterTx2 = expectedCoinsAfterTx1 + stakeKeyDeposit - tx2Internals.body.fee; + const expectedCoinsAfterTx2 = expectedCoinsAfterTx1 + stakeKeyDeposit - txDeregisterSigned.tx.body.fee; expect(tx2ConfirmedState.balance.total.coins).toBe(expectedCoinsAfterTx2); expect(tx2ConfirmedState.balance.total).toEqual(tx2ConfirmedState.balance.available); expect(tx2ConfirmedState.balance.deposit).toBe(0n); diff --git a/packages/wallet/src/TxBuilder/buildTx.ts b/packages/wallet/src/TxBuilder/buildTx.ts index bc18f7f2f2b..915f81513c9 100644 --- a/packages/wallet/src/TxBuilder/buildTx.ts +++ b/packages/wallet/src/TxBuilder/buildTx.ts @@ -107,8 +107,8 @@ export class ObservableWalletTxBuilder implements TxBuilder { return new ObservableWalletTxOutputBuilder(this.#util, txOut); } - delegate(poolId: Cardano.PoolId): TxBuilder { - this.#delegateConfig = { poolId, type: 'delegate' }; + delegate(poolId?: Cardano.PoolId): TxBuilder { + this.#delegateConfig = poolId ? { poolId, type: 'delegate' } : { type: 'deregister' }; return this; } @@ -178,8 +178,8 @@ export class ObservableWalletTxBuilder implements TxBuilder { } async #addDelegationCertificates(): Promise { - // Deregistration not implemented yet - if (!this.#delegateConfig || this.#delegateConfig.type === 'deregister') { + if (!this.#delegateConfig) { + // Delegation was not configured by user return Promise.resolve(); } @@ -195,14 +195,25 @@ export class ObservableWalletTxBuilder implements TxBuilder { for (const rewardAccount of rewardAccounts) { const stakeKeyHash = Cardano.Ed25519KeyHash.fromRewardAccount(rewardAccount.address); - - if (rewardAccount.keyStatus === StakeKeyStatus.Unregistered) { - this.partialTxBody.certificates!.push(ObservableWalletTxBuilder.#createStakeKeyCert(stakeKeyHash)); + if (this.#delegateConfig.type === 'deregister' && rewardAccount.keyStatus !== StakeKeyStatus.Unregistered) { + // Deregister scenario + this.partialTxBody.certificates!.push({ + __typename: Cardano.CertificateType.StakeKeyDeregistration, + stakeKeyHash + }); + } else if (this.#delegateConfig.type === 'delegate') { + // Register and delegate scenario + if (rewardAccount.keyStatus === StakeKeyStatus.Unregistered) { + this.partialTxBody.certificates!.push({ + __typename: Cardano.CertificateType.StakeKeyRegistration, + stakeKeyHash + }); + } + + this.partialTxBody.certificates!.push( + ObservableWalletTxBuilder.#createDelegationCert(this.#delegateConfig.poolId, stakeKeyHash) + ); } - - this.partialTxBody.certificates!.push( - ObservableWalletTxBuilder.#createDelegationCert(this.#delegateConfig.poolId, stakeKeyHash) - ); } } @@ -216,13 +227,6 @@ export class ObservableWalletTxBuilder implements TxBuilder { stakeKeyHash }; } - - static #createStakeKeyCert(stakeKeyHash: Cardano.Ed25519KeyHash): Cardano.StakeAddressCertificate { - return { - __typename: Cardano.CertificateType.StakeKeyRegistration, - stakeKeyHash - }; - } } /** diff --git a/packages/wallet/src/TxBuilder/types.ts b/packages/wallet/src/TxBuilder/types.ts index ee82d64e43c..1387c58024e 100644 --- a/packages/wallet/src/TxBuilder/types.ts +++ b/packages/wallet/src/TxBuilder/types.ts @@ -149,13 +149,14 @@ export interface TxBuilder { buildOutput(txOut?: PartialTxOut): OutputBuilder; /** * Configure transaction to include delegation. - * - On `build()`, StakeDelegation and (if needed) StakeKeyRegistration certificates are added in - * the transaction body. + * - On `build()`, StakeKeyDeregistration or StakeDelegation and (if needed) + * StakeKeyRegistration certificates are added in the transaction body. + * - Stake key deregister is done by not providing the `poolId` parameter: `delegate()`. * - If wallet contains multiple reward accounts, it will create certificates for all of them. * - * @param poolId Pool Id to delegate to. + * @param poolId Pool Id to delegate to. If undefined, stake key deregistration will be done. */ - delegate(poolId: Cardano.PoolId): TxBuilder; + delegate(poolId?: Cardano.PoolId): TxBuilder; /** Sets TxMetadata in {@link auxiliaryData} */ setMetadata(metadata: Cardano.TxMetadata): TxBuilder; /** Sets extra signers in {@link extraSigners} */ diff --git a/packages/wallet/test/integration/buildTx.test.ts b/packages/wallet/test/integration/buildTx.test.ts index 949f54a43be..c1aa05a7b04 100644 --- a/packages/wallet/test/integration/buildTx.test.ts +++ b/packages/wallet/test/integration/buildTx.test.ts @@ -414,6 +414,32 @@ describe('buildTx', () => { assertTxIsValid(txDelegate); expect(txDelegate.body.certificates?.length).toBe(4); }); + + it('undefined poolId adds stake key deregister certificate if already registered', async () => { + wallet.delegation.rewardAccounts$ = of([ + { + address: Cardano.RewardAccount('stake_test1uqu7qkgf00zwqupzqfzdq87dahwntcznklhp3x30t3ukz6gswungn'), + delegatee: { + currentEpoch: undefined, + nextEpoch: undefined, + nextNextEpoch: undefined + }, + keyStatus: StakeKeyStatus.Registered, + rewardBalance: 33_333n + } + ]); + const txDeregister = await txBuilder.delegate().build(); + assertTxIsValid(txDeregister); + expect(txDeregister.body.certificates?.length).toBe(1); + const [deregisterCert] = txDeregister.body.certificates!; + expect(deregisterCert.__typename).toBe(Cardano.CertificateType.StakeKeyDeregistration); + }); + + it('undefined poolId does NOT add certificate if not registered', async () => { + const txDeregister = await txBuilder.delegate().build(); + assertTxIsValid(txDeregister); + expect(txDeregister.body.certificates?.length).toBeFalsy(); + }); }); it('can be used to build, sign and submit a tx', async () => { From d07a89a7cb5610768daccc92058595906ea344d2 Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Thu, 8 Sep 2022 21:07:29 +0000 Subject: [PATCH 3/4] feat: outputBuilder txOut method returns snapshot --- .../SingleAddressWallet/delegation.test.ts | 9 +-- .../wallet/src/TxBuilder/OutputBuilder.ts | 48 +++++++++------ packages/wallet/src/TxBuilder/buildTx.ts | 13 ++-- packages/wallet/src/TxBuilder/types.ts | 26 ++++---- .../wallet/test/integration/buildTx.test.ts | 60 ++++++++++++------- 5 files changed, 91 insertions(+), 65 deletions(-) diff --git a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts index 57ec4fff924..abb68fa7046 100644 --- a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts +++ b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts @@ -3,7 +3,7 @@ import { Awaited } from '@cardano-sdk/util'; import { Cardano } from '@cardano-sdk/core'; import { ObservableWallet, StakeKeyStatus, buildTx } from '@cardano-sdk/wallet'; import { TX_TIMEOUT, firstValueFromTimed, waitForWalletStateSettle } from '../util'; -import { assertTxIsValid, assertTxOutIsValid } from '../../../../wallet/test/util'; +import { assertTxIsValid } from '../../../../wallet/test/util'; import { env } from '../environment'; import { getWallet } from '../../../src/factories'; import { logger } from '@cardano-sdk/util-dev'; @@ -100,10 +100,11 @@ describe('SingleAddressWallet/delegation', () => { // Also send some coin to another wallet const destAddresses = (await firstValueFrom(destWallet.addresses$))[0].address; const txBuilder = buildTx(sourceWallet); - const maybeValidTxOut = await txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).build(); - assertTxOutIsValid(maybeValidTxOut); - const tx = await txBuilder.addOutput(maybeValidTxOut.txOut).delegate(poolId).build(); + const tx = await txBuilder + .addOutput(txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).toTxOut()) + .delegate(poolId) + .build(); assertTxIsValid(tx); const signedTx = await tx.sign(); diff --git a/packages/wallet/src/TxBuilder/OutputBuilder.ts b/packages/wallet/src/TxBuilder/OutputBuilder.ts index 3400d8ec3d0..ac7fe741977 100644 --- a/packages/wallet/src/TxBuilder/OutputBuilder.ts +++ b/packages/wallet/src/TxBuilder/OutputBuilder.ts @@ -18,7 +18,7 @@ const isViableTxOut = (txOut: PartialTxOut): txOut is Cardano.TxOut => !!(txOut? * Transforms from `OutputValidation` type emitted by `OutputValidator`, to * `OutputValidationMinimumCoinError` | `OutputValidationTokenBundleSizeError` */ -export const toOutputValidationError = ( +const toOutputValidationError = ( txOut: Cardano.TxOut, validation: OutputValidation ): OutputValidationMinimumCoinError | OutputValidationTokenBundleSizeError | undefined => { @@ -34,8 +34,11 @@ export const toOutputValidationError = ( * `OutputBuilder` implementation based on the minimal wallet type. */ export class ObservableWalletTxOutputBuilder implements OutputBuilder { - partialOutput: PartialTxOut; - + /** + * Transaction output that is updated by `OutputBuilder` methods. + * Every method call recreates the `partialOutput`, thus updating it immutably. + */ + #partialOutput: PartialTxOut; #outputValidator: OutputValidator; /** @@ -44,61 +47,68 @@ export class ObservableWalletTxOutputBuilder implements OutputBuilder { * @param txOut optional partial transaction output to use for initialization. */ constructor(outputValidator: OutputValidator, txOut?: PartialTxOut) { - this.partialOutput = { ...txOut }; + this.#partialOutput = { ...txOut }; this.#outputValidator = outputValidator; } + toTxOut(): Cardano.TxOut { + if (!isViableTxOut(this.#partialOutput)) { + throw new OutputValidationMissingRequiredError(this.#partialOutput); + } + return { ...this.#partialOutput }; + } + value(value: Cardano.Value): OutputBuilder { - this.partialOutput = { ...this.partialOutput, value: { ...value } }; + this.#partialOutput = { ...this.#partialOutput, value: { ...value } }; return this; } coin(coin: Cardano.Lovelace): OutputBuilder { - this.partialOutput = { ...this.partialOutput, value: { ...this.partialOutput?.value, coins: coin } }; + this.#partialOutput = { ...this.#partialOutput, value: { ...this.#partialOutput?.value, coins: coin } }; return this; } assets(assets: Cardano.TokenMap): OutputBuilder { - this.partialOutput = { - ...this.partialOutput, - value: { ...this.partialOutput?.value, assets } + this.#partialOutput = { + ...this.#partialOutput, + value: { ...this.#partialOutput?.value, assets } }; return this; } asset(assetId: Cardano.AssetId, quantity: bigint): OutputBuilder { - const assets: Cardano.TokenMap = new Map(this.partialOutput?.value?.assets); + const assets: Cardano.TokenMap = new Map(this.#partialOutput?.value?.assets); quantity === 0n ? assets.delete(assetId) : assets.set(assetId, quantity); return this.assets(assets); } address(address: Cardano.Address): OutputBuilder { - this.partialOutput = { ...this.partialOutput, address }; + this.#partialOutput = { ...this.#partialOutput, address }; return this; } datum(datum: Cardano.util.Hash32ByteBase16): OutputBuilder { - this.partialOutput = { ...this.partialOutput, datum }; + this.#partialOutput = { ...this.#partialOutput, datum }; return this; } async build(): Promise { - if (!isViableTxOut(this.partialOutput)) { + let txOut: Cardano.TxOut; + try { + txOut = this.toTxOut(); + } catch (error) { return Promise.resolve({ - errors: [new OutputValidationMissingRequiredError(this.partialOutput)], + errors: [error as OutputValidationMissingRequiredError], isValid: false }); } - const outputValidation = toOutputValidationError( - this.partialOutput, - await this.#outputValidator.validateOutput(this.partialOutput) - ); + const outputValidation = toOutputValidationError(txOut, await this.#outputValidator.validateOutput(txOut)); if (outputValidation) { return { errors: [outputValidation], isValid: false }; } - return { isValid: true, txOut: this.partialOutput }; + return { isValid: true, txOut }; } } diff --git a/packages/wallet/src/TxBuilder/buildTx.ts b/packages/wallet/src/TxBuilder/buildTx.ts index 915f81513c9..8871e991bb4 100644 --- a/packages/wallet/src/TxBuilder/buildTx.ts +++ b/packages/wallet/src/TxBuilder/buildTx.ts @@ -6,15 +6,13 @@ import { IncompatibleWalletError, MaybeValidTx, OutputBuilder, - OutputValidationMinimumCoinError, - OutputValidationTokenBundleSizeError, PartialTxOut, SignedTx, TxBodyValidationError, TxBuilder, TxOutValidationError } from './types'; -import { ObservableWalletTxOutputBuilder, toOutputValidationError } from './OutputBuilder'; +import { ObservableWalletTxOutputBuilder } from './OutputBuilder'; import { OutputValidator, RewardAccount, StakeKeyStatus, WalletUtilContext, createWalletUtil } from '../services'; import { SignTransactionOptions, TransactionSigner } from '@cardano-sdk/key-management'; import { deepEquals } from '@cardano-sdk/util'; @@ -164,12 +162,9 @@ export class ObservableWalletTxBuilder implements TxBuilder { } async #validateOutputs(): Promise { - const outputValidations = - this.partialTxBody.outputs && (await this.#util.validateOutputs(this.partialTxBody.outputs)); - - const errors = [...(outputValidations?.entries() || [])] - .map(([txOut, validation]) => toOutputValidationError(txOut, validation)) - .filter((err): err is OutputValidationTokenBundleSizeError | OutputValidationMinimumCoinError => !!err); + const errors = ( + await Promise.all(this.partialTxBody.outputs?.map((output) => this.buildOutput(output).build()) || []) + ).flatMap((output) => (output.isValid ? [] : output.errors)); if (errors.length > 0) { throw errors; diff --git a/packages/wallet/src/TxBuilder/types.ts b/packages/wallet/src/TxBuilder/types.ts index 1387c58024e..c185b0b2a13 100644 --- a/packages/wallet/src/TxBuilder/types.ts +++ b/packages/wallet/src/TxBuilder/types.ts @@ -63,17 +63,19 @@ export type MaybeValidTxOut = ValidTxOut | InvalidTxOut; */ export interface OutputBuilder { /** - * Transaction output that is updated by `OutputBuilder` methods. - * It should not be updated directly, but this is not restricted to allow experimental TxOutput changes that are not - * yet available in the OutputBuilder interface. - * Every method call recreates the `partialOutput`, thus updating it immutably. + * Create transaction output snapshot, as it was configured until the point of calling this method. + * + * @returns {Cardano.TxOut} transaction output snapshot. + * - It can be used in `TxBuilder.addOutput()`. + * - It will be validated once `TxBuilder.build()` method is called. + * @throws {OutputValidationMissingRequiredError} if the mandatory fields 'address' or 'coins' are missing */ - partialOutput: PartialTxOut; - /** Sets {@link partialOutput} `value` field. Preexisting `value` is overwritten. */ + toTxOut(): Cardano.TxOut; + /** Sets transaction output `value` field. Preexisting `value` is overwritten. */ value(value: Cardano.Value): OutputBuilder; - /** Sets {@link partialOutput}.value `coins` field. */ + /** Sets transaction output value `coins` field. */ coin(coin: Cardano.Lovelace): OutputBuilder; - /** Sets {@link partialOutput}.value `assets` field. Preexisting assets are overwritten */ + /** Sets transaction output value `assets` field. Preexisting assets are overwritten */ assets(assets: Cardano.TokenMap): OutputBuilder; /** * Add/Remove/Update asset. @@ -85,12 +87,12 @@ export interface OutputBuilder { * @param quantity To remove an asset, set quantity to 0 */ asset(assetId: Cardano.AssetId, quantity: bigint): OutputBuilder; - /** Sets {@link partialOutput} `address` field. */ + /** Sets transaction output `address` field. */ address(address: Cardano.Address): OutputBuilder; - /** Sets {@link partialOutput} `datum` field. */ + /** Sets transaction output `datum` field. */ datum(datum: Cardano.util.Hash32ByteBase16): OutputBuilder; /** - * Checks if the constructed `partialOutput` is complete and valid + * Checks if the transaction output is complete and valid * * @returns {Promise} When it is a `ValidTxOut` it can be used as input in `TxBuilder.addOutput()`. * In case of it is an `InvalidTxOut`, it embeds a TxOutValidationError with more details @@ -141,7 +143,7 @@ export interface TxBuilder { */ removeOutput(txOut: Cardano.TxOut): TxBuilder; /** - * Does *not* addOutput + * Does *not* addOutput. * * @param txOut optional partial transaction output to use for initialization. * @returns {OutputBuilder} {@link OutputBuilder} util for building transaction outputs. diff --git a/packages/wallet/test/integration/buildTx.test.ts b/packages/wallet/test/integration/buildTx.test.ts index c1aa05a7b04..fa07a6d9350 100644 --- a/packages/wallet/test/integration/buildTx.test.ts +++ b/packages/wallet/test/integration/buildTx.test.ts @@ -202,67 +202,66 @@ describe('buildTx', () => { output1Coin = 10_000_000n; output2Base = mocks.utxo[0][1]; - outputBuilder = txBuilder.buildOutput(); + outputBuilder = txBuilder.buildOutput().address(address).coin(output1Coin); }); it('can create OutputBuilder without initial output', () => { - expect(outputBuilder.partialOutput).toBeTruthy(); + expect(outputBuilder.toTxOut()).toBeTruthy(); }); it('can create OutputBuilder starting from an existing output', () => { const outputBuilderFromExisting = txBuilder.buildOutput(output); - expect(outputBuilderFromExisting.partialOutput).toEqual(output); + expect(outputBuilderFromExisting.toTxOut()).toEqual(output); }); - it('can set partialOutput value overwriting preexisting value', () => { + it('can set output value, overwriting preexisting value', () => { const outValue = { assets, coins: output1Coin }; outputBuilder.value(outValue); - expect(outputBuilder.partialOutput.value).toEqual(outValue); + expect(outputBuilder.toTxOut().value).toEqual(outValue); // Setting outValueOther will remove previously configured assets const outValueOther = { coins: output1Coin + 100n }; outputBuilder.value(outValueOther); - expect(outputBuilder.partialOutput.value).toEqual(outValueOther); + expect(outputBuilder.toTxOut().value).toEqual(outValueOther); }); it('can set coin value', () => { - outputBuilder.coin(output1Coin); - expect(outputBuilder.partialOutput.value).toEqual({ coins: output1Coin }); + expect(outputBuilder.toTxOut().value).toEqual({ coins: output1Coin }); }); - it('can set partialOutput assets', () => { + it('can set assets', () => { outputBuilder.assets(assets); - expect(outputBuilder.partialOutput.value).toEqual({ assets }); + expect(outputBuilder.toTxOut().value).toEqual(expect.objectContaining({ assets })); }); it('can add assets one by one', () => { outputBuilder.asset(AssetId.PXL, 5n).asset(AssetId.TSLA, 10n); - expect(outputBuilder.partialOutput.value?.assets?.size).toBe(2); - expect(outputBuilder.partialOutput.value?.assets?.get(AssetId.PXL)).toBe(5n); - expect(outputBuilder.partialOutput.value?.assets?.get(AssetId.TSLA)).toBe(10n); + const txOut = outputBuilder.toTxOut(); + expect(txOut.value?.assets?.size).toBe(2); + expect(txOut.value?.assets?.get(AssetId.PXL)).toBe(5n); + expect(txOut.value?.assets?.get(AssetId.TSLA)).toBe(10n); }); it('can update asset quantity by assetId', () => { outputBuilder.asset(AssetId.PXL, 5n).asset(AssetId.TSLA, 10n); outputBuilder.asset(AssetId.PXL, 11n); - expect(outputBuilder.partialOutput.value?.assets?.get(AssetId.PXL)).toBe(11n); + expect(outputBuilder.toTxOut().value?.assets?.get(AssetId.PXL)).toBe(11n); }); it('can remove asset by using quantity 0', () => { outputBuilder.assets(assets); - expect(outputBuilder.partialOutput.value?.assets?.size).toBe(1); + expect(outputBuilder.toTxOut().value?.assets?.size).toBe(1); outputBuilder.asset(assetId, 0n); - expect(outputBuilder.partialOutput.value?.assets?.size).toBe(0); + expect(outputBuilder.toTxOut().value?.assets?.size).toBe(0); }); it('can set address', () => { - outputBuilder.address(address); - expect(outputBuilder.partialOutput.address).toEqual(address); + expect(outputBuilder.toTxOut().address).toEqual(address); }); it('can set datum', () => { outputBuilder.datum(datum); - expect(outputBuilder.partialOutput.datum).toEqual(datum); + expect(outputBuilder.toTxOut().datum).toEqual(datum); }); it('can build a valid output', async () => { @@ -285,7 +284,7 @@ describe('buildTx', () => { }); }); - describe('can validate', () => { + describe('can build and validate', () => { it('missing coin field', async () => { const builtOutput = await txBuilder.buildOutput().address(address).build(); const [error] = (!builtOutput.isValid && builtOutput.errors) || []; @@ -309,6 +308,24 @@ describe('buildTx', () => { assertTxOutIsValid(builtOutput); }); }); + + describe('can validate required output fields', () => { + it('missing coin field', () => { + expect(() => txBuilder.buildOutput().address(address).toTxOut()).toThrowError( + OutputValidationMissingRequiredError + ); + }); + + it('missing address field', () => { + expect(() => txBuilder.buildOutput().coin(output1Coin).toTxOut()).toThrowError( + OutputValidationMissingRequiredError + ); + }); + + it('legit output with valid with address and coin', async () => { + expect(() => txBuilder.buildOutput().address(address).coin(output1Coin).toTxOut()).not.toThrow(); + }); + }); }); describe('delegate', () => { @@ -491,7 +508,8 @@ describe('buildTx', () => { validateValue: jest.fn(), validateValues: jest.fn() }; - const tx = await buildTx(wallet, mockValidator).addOutput(output).addOutput(output2).build(); + const builder = buildTx(wallet, mockValidator).addOutput(output); + const tx = await builder.addOutput(builder.buildOutput(output2).toTxOut()).build(); expect(tx.isValid).toBeFalsy(); const [error1, error2] = (!tx.isValid && tx.errors) || []; From 2831a2ac99909fa6f2641f27c633932a4cbdb588 Mon Sep 17 00:00:00 2001 From: Mircea Hasegan Date: Fri, 9 Sep 2022 07:27:13 +0000 Subject: [PATCH 4/4] feat!: buildTx added logger Logger param for TxBuilder. BREAKING CHANGE: buildTx() requires positional params and mandatory logger --- .../SingleAddressWallet/delegation.test.ts | 4 +- .../SingleAddressWallet/metadata.test.ts | 2 +- .../wallet/src/TxBuilder/OutputBuilder.ts | 19 +++--- packages/wallet/src/TxBuilder/buildTx.ts | 62 +++++++++++++------ packages/wallet/src/TxBuilder/index.ts | 2 +- packages/wallet/src/TxBuilder/types.ts | 7 ++- .../wallet/test/integration/buildTx.test.ts | 46 +++++++------- 7 files changed, 83 insertions(+), 59 deletions(-) diff --git a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts index abb68fa7046..f7028541a4b 100644 --- a/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts +++ b/packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts @@ -99,7 +99,7 @@ describe('SingleAddressWallet/delegation', () => { // Make a 1st tx with key registration (if not already registered) and stake delegation // Also send some coin to another wallet const destAddresses = (await firstValueFrom(destWallet.addresses$))[0].address; - const txBuilder = buildTx(sourceWallet); + const txBuilder = buildTx({ logger, observableWallet: sourceWallet }); const tx = await txBuilder .addOutput(txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).toTxOut()) @@ -148,7 +148,7 @@ describe('SingleAddressWallet/delegation', () => { } // Make a 2nd tx with key deregistration - const txDeregister = await buildTx(sourceWallet).delegate().build(); + const txDeregister = await buildTx({ logger, observableWallet: sourceWallet }).delegate().build(); assertTxIsValid(txDeregister); const txDeregisterSigned = await txDeregister.sign(); await txDeregisterSigned.submit(); diff --git a/packages/e2e/test/wallet/SingleAddressWallet/metadata.test.ts b/packages/e2e/test/wallet/SingleAddressWallet/metadata.test.ts index 6464119e56a..a3351f98995 100644 --- a/packages/e2e/test/wallet/SingleAddressWallet/metadata.test.ts +++ b/packages/e2e/test/wallet/SingleAddressWallet/metadata.test.ts @@ -22,7 +22,7 @@ describe('SingleAddressWallet/metadata', () => { const walletUtil = createWalletUtil(wallet); const { minimumCoin } = await walletUtil.validateValue({ coins: 0n }); - const builtTx = await buildTx(wallet) + const builtTx = await buildTx({ logger, observableWallet: wallet }) .addOutput({ address: ownAddress, value: { coins: minimumCoin } }) .setMetadata(metadata) .build(); diff --git a/packages/wallet/src/TxBuilder/OutputBuilder.ts b/packages/wallet/src/TxBuilder/OutputBuilder.ts index ac7fe741977..ddcb6cb4a62 100644 --- a/packages/wallet/src/TxBuilder/OutputBuilder.ts +++ b/packages/wallet/src/TxBuilder/OutputBuilder.ts @@ -11,6 +11,14 @@ import { import { OutputValidation } from '../types'; import { OutputValidator } from '../services'; +/** Properties needed to construct an {@link ObservableWalletTxOutputBuilder} */ +export interface OutputBuilderProps { + /** This validator is normally created and passed as an arg here by the {@link TxBuilder.buildOutput} method */ + outputValidator: OutputValidator; + /** Optional partial transaction output to use for initialization. */ + txOut?: PartialTxOut; +} + /** Determines if the `PartialTxOut` arg have at least an address and coins. */ const isViableTxOut = (txOut: PartialTxOut): txOut is Cardano.TxOut => !!(txOut?.address && txOut?.value?.coins); @@ -31,22 +39,17 @@ const toOutputValidationError = ( }; /** - * `OutputBuilder` implementation based on the minimal wallet type. + * `OutputBuilder` implementation based on the minimal wallet type: {@link ObservableWalletTxBuilderDependencies}. */ export class ObservableWalletTxOutputBuilder implements OutputBuilder { /** - * Transaction output that is updated by `OutputBuilder` methods. + * Transaction output that is updated by `ObservableWalletTxOutputBuilder` methods. * Every method call recreates the `partialOutput`, thus updating it immutably. */ #partialOutput: PartialTxOut; #outputValidator: OutputValidator; - /** - * - * @param outputValidator this validator is normally created and passed as an arg here, by the TxBuilder - * @param txOut optional partial transaction output to use for initialization. - */ - constructor(outputValidator: OutputValidator, txOut?: PartialTxOut) { + constructor({ outputValidator, txOut }: OutputBuilderProps) { this.#partialOutput = { ...txOut }; this.#outputValidator = outputValidator; } diff --git a/packages/wallet/src/TxBuilder/buildTx.ts b/packages/wallet/src/TxBuilder/buildTx.ts index 8871e991bb4..96a0dcae689 100644 --- a/packages/wallet/src/TxBuilder/buildTx.ts +++ b/packages/wallet/src/TxBuilder/buildTx.ts @@ -12,6 +12,7 @@ import { TxBuilder, TxOutValidationError } from './types'; +import { Logger } from 'ts-log'; import { ObservableWalletTxOutputBuilder } from './OutputBuilder'; import { OutputValidator, RewardAccount, StakeKeyStatus, WalletUtilContext, createWalletUtil } from '../services'; import { SignTransactionOptions, TransactionSigner } from '@cardano-sdk/key-management'; @@ -32,6 +33,20 @@ export type ObservableWalletTxBuilderDependencies = Pick ReturnType; } & WalletUtilContext; +/** + * Properties needed by {@link buildTx} to build a {@link ObservableWalletTxBuilder} TxBuilder + * + * - {@link BuildTxProps.observableWallet} minimal ObservableWallet needed to do actions like {@link build()}, + * {@link delegate()} etc. + * - {@link BuildTxProps.outputValidator} optional custom output validator util. + * Uses {@link createWalletUtil} by default. + */ +export interface BuildTxProps { + observableWallet: ObservableWalletTxBuilderDependencies; + outputValidator?: OutputValidator; + logger: Logger; +} + interface Delegate { type: 'delegate'; poolId: Cardano.PoolId; @@ -73,19 +88,14 @@ export class ObservableWalletTxBuilder implements TxBuilder { signingOptions?: SignTransactionOptions; #observableWallet: ObservableWalletTxBuilderDependencies; - #util: OutputValidator; + #outputValidator: OutputValidator; #delegateConfig: DelegateConfig; + #logger: Logger; - /** - * @param observableWallet minimal ObservableWallet needed to do actions like {@link build()}, {@link delegate()} etc. - * @param util optional custom output validator util. Uses {@link createWalletUtil} by default. - */ - constructor( - observableWallet: ObservableWalletTxBuilderDependencies, - util: OutputValidator = createWalletUtil(observableWallet) - ) { + constructor({ observableWallet, outputValidator = createWalletUtil(observableWallet), logger }: BuildTxProps) { this.#observableWallet = observableWallet; - this.#util = util; + this.#outputValidator = outputValidator; + this.#logger = logger; } addOutput(txOut: Cardano.TxOut): TxBuilder { @@ -102,7 +112,7 @@ export class ObservableWalletTxBuilder implements TxBuilder { } buildOutput(txOut?: PartialTxOut): OutputBuilder { - return new ObservableWalletTxOutputBuilder(this.#util, txOut); + return new ObservableWalletTxOutputBuilder({ outputValidator: this.#outputValidator, txOut }); } delegate(poolId?: Cardano.PoolId): TxBuilder { @@ -154,6 +164,7 @@ export class ObservableWalletTxBuilder implements TxBuilder { }; } catch (error) { const errors = Array.isArray(error) ? (error as TxBodyValidationError[]) : [error as TxBodyValidationError]; + this.#logger.debug('Build errors', errors); return { errors, isValid: false @@ -190,15 +201,29 @@ export class ObservableWalletTxBuilder implements TxBuilder { for (const rewardAccount of rewardAccounts) { const stakeKeyHash = Cardano.Ed25519KeyHash.fromRewardAccount(rewardAccount.address); - if (this.#delegateConfig.type === 'deregister' && rewardAccount.keyStatus !== StakeKeyStatus.Unregistered) { + if (this.#delegateConfig.type === 'deregister') { // Deregister scenario - this.partialTxBody.certificates!.push({ - __typename: Cardano.CertificateType.StakeKeyDeregistration, - stakeKeyHash - }); + if (rewardAccount.keyStatus === StakeKeyStatus.Unregistered) { + this.#logger.warn( + 'Skipping stake key deregister. Stake key not registered.', + rewardAccount.address, + rewardAccount.keyStatus + ); + } else { + this.partialTxBody.certificates!.push({ + __typename: Cardano.CertificateType.StakeKeyDeregistration, + stakeKeyHash + }); + } } else if (this.#delegateConfig.type === 'delegate') { // Register and delegate scenario - if (rewardAccount.keyStatus === StakeKeyStatus.Unregistered) { + if (rewardAccount.keyStatus !== StakeKeyStatus.Unregistered) { + this.#logger.debug( + 'Skipping stake key register. Stake key already registered', + rewardAccount.address, + rewardAccount.keyStatus + ); + } else { this.partialTxBody.certificates!.push({ __typename: Cardano.CertificateType.StakeKeyRegistration, stakeKeyHash @@ -230,5 +255,4 @@ export class ObservableWalletTxBuilder implements TxBuilder { * `ObservableWallet.buildTx()` would be nice, but it adds quite a lot of complexity * to web-extension messaging, so it will be separate util like this one for MVP. */ -export const buildTx = (observableWallet: ObservableWalletTxBuilderDependencies, util?: OutputValidator): TxBuilder => - new ObservableWalletTxBuilder(observableWallet, util); +export const buildTx = (props: BuildTxProps): TxBuilder => new ObservableWalletTxBuilder(props); diff --git a/packages/wallet/src/TxBuilder/index.ts b/packages/wallet/src/TxBuilder/index.ts index 3fc2daf828f..3b244f00b61 100644 --- a/packages/wallet/src/TxBuilder/index.ts +++ b/packages/wallet/src/TxBuilder/index.ts @@ -1,3 +1,3 @@ export * from './types'; export * from './buildTx'; -// intentionally excluded OutputBuilder from the exports because it should only be used as part of TxBuilder +export * from './OutputBuilder'; diff --git a/packages/wallet/src/TxBuilder/types.ts b/packages/wallet/src/TxBuilder/types.ts index c185b0b2a13..6a3842782d1 100644 --- a/packages/wallet/src/TxBuilder/types.ts +++ b/packages/wallet/src/TxBuilder/types.ts @@ -66,9 +66,10 @@ export interface OutputBuilder { * Create transaction output snapshot, as it was configured until the point of calling this method. * * @returns {Cardano.TxOut} transaction output snapshot. - * - It can be used in `TxBuilder.addOutput()`. - * - It will be validated once `TxBuilder.build()` method is called. - * @throws {OutputValidationMissingRequiredError} if the mandatory fields 'address' or 'coins' are missing + * - It can be used in {@link TxBuilder.addOutput}. + * - It will be validated once {@link TxBuilder.build} method is called. + * @throws OutputValidationMissingRequiredError {@link OutputValidationMissingRequiredError} if + * the mandatory fields 'address' or 'coins' are missing */ toTxOut(): Cardano.TxOut; /** Sets transaction output `value` field. Preexisting `value` is overwritten. */ diff --git a/packages/wallet/test/integration/buildTx.test.ts b/packages/wallet/test/integration/buildTx.test.ts index fa07a6d9350..afa3d5309de 100644 --- a/packages/wallet/test/integration/buildTx.test.ts +++ b/packages/wallet/test/integration/buildTx.test.ts @@ -1,6 +1,6 @@ /* eslint-disable func-style */ /* eslint-disable jsdoc/require-jsdoc */ -import { AssetId, somePartialStakePools } from '@cardano-sdk/util-dev'; +import { AssetId, logger, somePartialStakePools } from '@cardano-sdk/util-dev'; import { Cardano } from '@cardano-sdk/core'; import { of } from 'rxjs'; @@ -28,19 +28,19 @@ function assertObjectRefsAreDifferent(obj1: unknown, obj2: unknown): void { } describe('buildTx', () => { - let wallet: ObservableWallet; + let observableWallet: ObservableWallet; let txBuilder: TxBuilder; let output: Cardano.TxOut; let output2: Cardano.TxOut; beforeEach(async () => { - ({ wallet } = await createWallet()); + ({ wallet: observableWallet } = await createWallet()); output = mocks.utxo[0][1]; output2 = mocks.utxo[1][1]; - txBuilder = buildTx(wallet); + txBuilder = buildTx({ logger, observableWallet }); }); - afterEach(() => wallet.shutdown()); + afterEach(() => observableWallet.shutdown()); describe('addOutput', () => { it('can add output without mutating partialTxBody', () => { @@ -169,8 +169,8 @@ describe('buildTx', () => { }); it('uses signingOptions to finalize transaction when submitting', async () => { - const origFinalizeTx = wallet.finalizeTx; - wallet.finalizeTx = jest.fn(origFinalizeTx); + const origFinalizeTx = observableWallet.finalizeTx; + observableWallet.finalizeTx = jest.fn(origFinalizeTx); const tx = await txBuilder.addOutput(mocks.utxo[0][1]).setSigningOptions(signingOptions).build(); assertTxIsValid(tx); @@ -179,7 +179,7 @@ describe('buildTx', () => { expect(tx.signingOptions).toEqual(signingOptions); await (await tx.sign()).submit(); - expect(wallet.finalizeTx).toHaveBeenLastCalledWith(expect.objectContaining({ signingOptions })); + expect(observableWallet.finalizeTx).toHaveBeenLastCalledWith(expect.objectContaining({ signingOptions })); }); }); @@ -346,7 +346,7 @@ describe('buildTx', () => { expect(txBuilt.body.certificates?.length).toBe(2); }); - it('adds both stake key and delegation certificates when reward account was not registered', async () => { + it('adds both stake key and delegation certificates when stake key was not registered', async () => { const txDelegate = await txBuilder.delegate(poolId).build(); assertTxIsValid(txDelegate); const [stakeKeyCert, delegationCert] = txDelegate.body.certificates!; @@ -370,7 +370,7 @@ describe('buildTx', () => { }); it('throws IncompatibleWallet error if no reward accounts were found', async () => { - wallet.delegation.rewardAccounts$ = of([]); + observableWallet.delegation.rewardAccounts$ = of([]); const txBuilt = await txBuilder.delegate(poolId).build(); if (!txBuilt.isValid) { expect(txBuilt.errors?.length).toBe(1); @@ -379,8 +379,8 @@ describe('buildTx', () => { expect.assertions(2); }); - it('adds only delegation certificate with correct poolId when reward account was already registered', async () => { - wallet.delegation.rewardAccounts$ = of([ + it('adds only delegation certificate with correct poolId when stake key was already registered', async () => { + observableWallet.delegation.rewardAccounts$ = of([ { address: Cardano.RewardAccount('stake_test1uqu7qkgf00zwqupzqfzdq87dahwntcznklhp3x30t3ukz6gswungn'), delegatee: { @@ -404,7 +404,7 @@ describe('buildTx', () => { }); it('adds multiple certificates when handling multiple reward accounts', async () => { - wallet.delegation.rewardAccounts$ = of([ + observableWallet.delegation.rewardAccounts$ = of([ { address: Cardano.RewardAccount('stake_test1uqu7qkgf00zwqupzqfzdq87dahwntcznklhp3x30t3ukz6gswungn'), delegatee: { @@ -433,7 +433,7 @@ describe('buildTx', () => { }); it('undefined poolId adds stake key deregister certificate if already registered', async () => { - wallet.delegation.rewardAccounts$ = of([ + observableWallet.delegation.rewardAccounts$ = of([ { address: Cardano.RewardAccount('stake_test1uqu7qkgf00zwqupzqfzdq87dahwntcznklhp3x30t3ukz6gswungn'), delegatee: { @@ -460,7 +460,7 @@ describe('buildTx', () => { }); it('can be used to build, sign and submit a tx', async () => { - const tx = await buildTx(wallet).addOutput(mocks.utxo[0][1]).build(); + const tx = await buildTx({ logger, observableWallet }).addOutput(mocks.utxo[0][1]).build(); if (tx.isValid) { const signedTx = await tx.sign(); await signedTx.submit(); @@ -496,19 +496,15 @@ describe('buildTx', () => { }; const mockValidator: OutputValidator = { - validateOutput: jest.fn(), - validateOutputs: jest.fn(() => - Promise.resolve( - new Map([ - [output, coinMissingValidation], - [output2, bundleSizeValidation] - ]) - ) - ), + validateOutput: jest + .fn() + .mockResolvedValueOnce(coinMissingValidation) + .mockResolvedValueOnce(bundleSizeValidation), + validateOutputs: jest.fn(), validateValue: jest.fn(), validateValues: jest.fn() }; - const builder = buildTx(wallet, mockValidator).addOutput(output); + const builder = buildTx({ logger, observableWallet, outputValidator: mockValidator }).addOutput(output); const tx = await builder.addOutput(builder.buildOutput(output2).toTxOut()).build(); expect(tx.isValid).toBeFalsy();