Skip to content

Commit

Permalink
Merge pull request #427 from input-output-hk/feat/tx-builder-stake-ke…
Browse files Browse the repository at this point in the history
…y-dereg

Feat/tx builder stake key dereg
  • Loading branch information
rhyslbw authored Sep 12, 2022
2 parents 33318a1 + 2831a2a commit c5ddf7c
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 186 deletions.
29 changes: 12 additions & 17 deletions packages/e2e/test/wallet/SingleAddressWallet/delegation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -100,11 +99,12 @@ 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 maybeValidTxOut = await txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).build();
assertTxOutIsValid(maybeValidTxOut);
const txBuilder = buildTx({ logger, observableWallet: sourceWallet });

const tx = await txBuilder.addOutput(maybeValidTxOut.txOut).build();
const tx = await txBuilder
.addOutput(txBuilder.buildOutput().address(destAddresses).coin(tx1OutputCoins).toTxOut())
.delegate(poolId)
.build();
assertTxIsValid(tx);

const signedTx = await tx.sign();
Expand Down Expand Up @@ -148,23 +148,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({ logger, observableWallet: 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
65 changes: 39 additions & 26 deletions packages/wallet/src/TxBuilder/OutputBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,22 @@ 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);

/**
* Transforms from `OutputValidation` type emitted by `OutputValidator`, to
* `OutputValidationMinimumCoinError` | `OutputValidationTokenBundleSizeError`
*/
export const toOutputValidationError = (
const toOutputValidationError = (
txOut: Cardano.TxOut,
validation: OutputValidation
): OutputValidationMinimumCoinError | OutputValidationTokenBundleSizeError | undefined => {
Expand All @@ -31,74 +39,79 @@ export 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 {
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.
* Transaction output that is updated by `ObservableWalletTxOutputBuilder` methods.
* Every method call recreates the `partialOutput`, thus updating it immutably.
*/
constructor(outputValidator: OutputValidator, txOut?: PartialTxOut) {
this.partialOutput = { ...txOut };
#partialOutput: PartialTxOut;
#outputValidator: OutputValidator;

constructor({ outputValidator, txOut }: OutputBuilderProps) {
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<MaybeValidTxOut> {
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 };
}
}
Loading

0 comments on commit c5ddf7c

Please sign in to comment.