Skip to content

Commit

Permalink
feat(wallet): add TxBuilder types prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
mkazlauskas committed Aug 8, 2022
1 parent 29d042e commit b9e6d65
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 0 deletions.
16 changes: 16 additions & 0 deletions packages/wallet/src/TxBuilder/buildTx.ts
@@ -0,0 +1,16 @@
import { NotImplementedError } from '@cardano-sdk/core';
import { ObservableWallet } from '../types';
import { TxBuilder } from './types';

/**
* Minimal sub-type of ObservableWallet that is used by TxBuilder
*/
export type TxBuilderObservableWallet = Pick<ObservableWallet, 'utxo' | 'protocolParameters$' | 'finalizeTx'>;

/**
* REVIEW: `ObservableWallet.buildTx()` would be nice, but it adds quite a lot of complexity
* to web-extension messaging. I suggest we only do this as a separate util like this one for MVP.
*/
export const buildTx = (_observableWallet: ObservableWallet): TxBuilder => {
throw new NotImplementedError('TODO');
};
2 changes: 2 additions & 0 deletions packages/wallet/src/TxBuilder/index.ts
@@ -0,0 +1,2 @@
export * from './types';
export * from './buildTx';
104 changes: 104 additions & 0 deletions packages/wallet/src/TxBuilder/types.ts
@@ -0,0 +1,104 @@
import { Cardano } from '@cardano-sdk/core';
import { InputSelectionError } from '@cardano-sdk/cip2';
import { SignTransactionOptions } from '../KeyManagement';

type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;

export type WalletNotInitializedError = {};
export type ValueValidationError = {} | WalletNotInitializedError;
export type TxOutValidationError = ValueValidationError;
export type TxBodyValidationError = TxOutValidationError | InputSelectionError;

export type Valid<TValid> = TValid & {
isValid: true;
};

export interface Invalid<TError> {
isValid: false;
errors: TError[];
}

export interface ValidTxOutData {
readonly txOut: Cardano.TxOut;
}

export type ValidTxOut = Valid<ValidTxOutData>;
export type InvalidTxOut = Invalid<TxOutValidationError>;
export type MaybeValidTxOut = ValidTxOut | InvalidTxOut;

export interface OutputBuilder {
partialOutput: DeepPartial<Cardano.TxOut>;
value(value: Cardano.Value): OutputBuilder;
/**
* Does not setValue
*/
coin(coin: Cardano.Lovelace): OutputBuilder;
assets(assets: Cardano.TokenMap): OutputBuilder;
/**
* @param quantity To remove an asset, set quantity to 0
*/
asset(assetId: Cardano.AssetId, quantity: bigint): OutputBuilder;
address(address: Cardano.Address): OutputBuilder;
datum(datum: Cardano.util.Hash32ByteBase16): OutputBuilder;
build(): Promise<MaybeValidTxOut>;
}

export interface SignedTx {
readonly tx: Cardano.NewTxAlonzo;
submit(): Promise<void>;
}

export interface ValidTxBody {
readonly body: Cardano.NewTxBodyAlonzo;
readonly auxiliaryData?: Cardano.AuxiliaryData;
readonly extraWitness?: Partial<Cardano.Witness>;

sign(props?: SignTransactionOptions): Promise<SignedTx>;
}

export type ValidTx = Valid<ValidTxBody>;
export type InvalidTx = Invalid<TxBodyValidationError>;
export type MaybeValidTx = ValidTx | InvalidTx;

export interface TxBuilder {
partialTxBody: Partial<Cardano.NewTxBodyAlonzo>;
auxiliaryData?: Cardano.AuxiliaryData;
extraWitness?: Partial<Cardano.Witness>;

addOutput(txOut: Cardano.TxOut): TxBuilder;
/**
* @param txOut must be in partialTxBody.outputs (===)
*/
removeOutput(txOut: Cardano.TxOut): TxBuilder;
/**
* Does not addOutput
*/
buildOutput(txOut?: DeepPartial<Cardano.TxOut>): OutputBuilder;
/**
* Add StakeDelegation and (if needed) StakeKeyRegistration certificate
*/
delegate(poolId: Cardano.PoolId): TxBuilder;
setMetadata(metadata: Cardano.TxMetadata): TxBuilder;
// REVIEW: a design decision here:
// - if this is async, then
// - buildTx(wallet) can be sync.
// - it uses more up-to-date wallet state when building a tx, which is good
// - if this is sync, then
// - buildTx(wallet) must be async and capture a snapshot of wallet state
// at the point when builder is created
build(): Promise<MaybeValidTx>;

// REVIEW: assuming fields below are not needed for Lace right now, so out of scope of MVP:
// - setMint
// TODO: maybe this, or maybe datum should be added together with an output?
// collaterals should be automatically computed and added to tx when you add scripts
// - setScripts(scripts: Array<{script, datum, redeemer}>)
// - setValidityInterval
// TODO: figure out what script_data_hash is used for
// - setScriptIntegrityHash(hash: Cardano.Hash32ByteBase16 | null);
// - setRequiredExtraSignatures(keyHashes: Cardano.Ed25519KeyHash[]);
}
1 change: 1 addition & 0 deletions packages/wallet/src/index.ts
Expand Up @@ -7,3 +7,4 @@ export * as storage from './persistence';
export * as cip30 from './cip30';
export * as cip36 from './cip36';
export * from './setupWallet';
export * from './TxBuilder';
58 changes: 58 additions & 0 deletions packages/wallet/test/integration/buildTx.test.ts
@@ -0,0 +1,58 @@
/* eslint-disable func-style */
/* eslint-disable jsdoc/require-jsdoc */
import * as mocks from '../mocks';
import { Cardano } from '@cardano-sdk/core';
import { MaybeValidTx, MaybeValidTxOut, ObservableWallet, ValidTx, ValidTxOut, buildTx } from '../../src';
import { createWallet } from './util';

function assertTxIsValid(tx: MaybeValidTx): asserts tx is ValidTx {
expect(tx.isValid).toBe(true);
}
function assertTxOutIsValid(txOut: MaybeValidTxOut): asserts txOut is ValidTxOut {
expect(txOut.isValid).toBe(true);
}

describe('buildTx', () => {
let wallet: ObservableWallet;

beforeAll(async () => {
({ wallet } = await createWallet());
});

describe('outputs', () => {
it.skip('can add outputs one by one', async () => {
const tx = await buildTx(wallet).addOutput(mocks.utxo[0][1]).addOutput(mocks.utxo[1][1]).build();
assertTxIsValid(tx);
expect(tx.body.outputs.length).toBe(2);
});

describe('buildOutput', () => {
it.skip('can build a valid output', async () => {
const assetId = Cardano.AssetId('1ec85dcee27f2d90ec1f9a1e4ce74a667dc9be8b184463223f9c960150584c');
const assetQuantity = 100n;
const assets = new Map([[assetId, assetQuantity]]);
const address = Cardano.Address('addr_test1vr8nl4u0u6fmtfnawx2rxfz95dy7m46t6dhzdftp2uha87syeufdg');
const datum = Cardano.Hash32ByteBase16('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d');
const output1Coin = 10_000_000n;
const output2Base = mocks.utxo[0][1];

const txBuilder = buildTx(wallet);

const output1 = await txBuilder
.buildOutput()
.address(address)
.coin(output1Coin)
.asset(assetId, assetQuantity)
.build();
assertTxOutIsValid(output1);
expect(output1.txOut).toEqual({ address, value: { assets, coin: output1Coin } });

const output2 = await txBuilder.buildOutput(output2Base).assets(assets).datum(datum).build();
assertTxOutIsValid(output2);
expect(output2.txOut).toEqual({ datum, ...output2Base, value: { ...output2Base.value, assets } });
});

it.todo('validates required output properties and value constraints');
});
});
});

0 comments on commit b9e6d65

Please sign in to comment.