Skip to content

Commit

Permalink
feat: chaining of txs
Browse files Browse the repository at this point in the history
  • Loading branch information
will-break-it committed Nov 3, 2023
1 parent 457c156 commit 837a8b7
Show file tree
Hide file tree
Showing 6 changed files with 262 additions and 69 deletions.
36 changes: 18 additions & 18 deletions src/lucid/lucid.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { C } from "../core/mod.ts";
import {
coreToUtxo,
createCostModels,
fromHex,
fromUnit,
paymentCredentialOf,
toHex,
toUnit,
Utils,
utxoToCore,
} from "../utils/mod.ts";
import { signData, verifyData } from "../misc/sign_data.ts";
import { discoverOwnUsedTxKeyHashes, walletFromSeed } from "../misc/wallet.ts";
import { Constr, Data } from "../plutus/data.ts";
import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts";
import { Emulator } from "../provider/emulator.ts";
import {
Address,
Credential,
Expand All @@ -27,19 +21,25 @@ import {
Slot,
Transaction,
TxHash,
Unit,
UTxO,
Unit,
Wallet,
WalletApi,
} from "../types/mod.ts";
import {
Utils,
coreToUtxo,
createCostModels,
fromHex,
fromUnit,
paymentCredentialOf,
toHex,
toUnit,
utxoToCore,
} from "../utils/mod.ts";
import { Message } from "./message.ts";
import { Tx } from "./tx.ts";
import { TxComplete } from "./tx_complete.ts";
import { discoverOwnUsedTxKeyHashes, walletFromSeed } from "../misc/wallet.ts";
import { signData, verifyData } from "../misc/sign_data.ts";
import { Message } from "./message.ts";
import { SLOT_CONFIG_NETWORK } from "../plutus/time.ts";
import { Constr, Data } from "../plutus/data.ts";
import { Emulator } from "../provider/emulator.ts";

export class Lucid {
txBuilderConfig!: C.TransactionBuilderConfig;
Expand Down
22 changes: 17 additions & 5 deletions src/lucid/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,17 @@ import {
toScriptRef,
utxoToCore,
} from "../utils/mod.ts";
import { applyDoubleCborEncoding } from "../utils/utils.ts";
import { applyDoubleCborEncoding, coresToUtxos, utxosToCores } from "../utils/utils.ts";
import { Lucid } from "./lucid.ts";
import { TxComplete } from "./tx_complete.ts";

export class Tx {
txBuilder: C.TransactionBuilder;
/** Stores the tx instructions, which get executed after calling .complete() */
private tasks: ((that: Tx) => unknown)[];
private lucid: Lucid;
protected lucid: Lucid;
/** Stores the available input utxo set for this tx (for tx chaining), if undefined falls back to wallet utxos */
private inputUTxOs?: UTxO[];

constructor(lucid: Lucid) {
this.lucid = lucid;
Expand Down Expand Up @@ -91,6 +93,16 @@ export class Tx {
return this;
}

/**
* Defines the set of UTxOs that is considered as inputs for balancing this transactions.
* If not set explicitely, falls back to the wallet's UTxO set.
*/
collectTxInputsFrom(utxos: UTxO[]): Tx {
// NOTE: merge exisitng input utxos to support tx composition
this.tasks.push((tx) => tx.inputUTxOs = [...(tx.inputUTxOs ?? []), ...utxos]);
return this;
}

/**
* All assets should be of the same policy id.
* You can chain mintAssets functions together if you need to mint assets with different policy ids.
Expand Down Expand Up @@ -546,13 +558,12 @@ export class Tx {
task = this.tasks.shift();
}

const utxos = await this.lucid.wallet.getUtxosCore();
const utxos = this.inputUTxOs !== undefined ? utxosToCores(this.inputUTxOs) : await this.lucid.wallet.getUtxosCore();

const changeAddress: C.Address = addressFromWithNetworkCheck(
options?.change?.address || (await this.lucid.wallet.address()),
this.lucid,
);

if (options?.coinSelection || options?.coinSelection === undefined) {
this.txBuilder.add_inputs_from(
utxos,
Expand All @@ -567,7 +578,6 @@ export class Tx {
]),
);
}

this.txBuilder.balance(
changeAddress,
(() => {
Expand Down Expand Up @@ -602,13 +612,15 @@ export class Tx {
})(),
);

const utxoSet = this.inputUTxOs ?? coresToUtxos(await this.lucid.wallet.getUtxosCore());
return new TxComplete(
this.lucid,
await this.txBuilder.construct(
utxos,
changeAddress,
options?.nativeUplc === undefined ? true : options?.nativeUplc,
),
utxoSet
);
}

Expand Down
71 changes: 68 additions & 3 deletions src/lucid/tx_complete.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,33 @@
import { C } from "../core/mod.ts";
import {
Credential,
PrivateKey,
Transaction,
TransactionWitnesses,
TxHash,
UTxO,
} from "../types/mod.ts";
import { coresToOutRefs, fromHex, getAddressDetails, paymentCredentialOf, producedUtxosFrom, toHex } from "../utils/mod.ts";
import { Lucid } from "./lucid.ts";
import { Tx } from "./tx.ts";
import { TxSigned } from "./tx_signed.ts";
import { fromHex, toHex } from "../utils/mod.ts";

export class TxComplete {
txComplete: C.Transaction;
witnessSetBuilder: C.TransactionWitnessSetBuilder;
private tasks: (() => Promise<void>)[];
/** Stores the available input utxo set for this tx (for tx chaining), if undefined falls back to wallet utxos */
private utxos?: UTxO[];
private lucid: Lucid;
fee: number;
exUnits: { cpu: number; mem: number } | null = null;

constructor(lucid: Lucid, tx: C.Transaction) {
constructor(lucid: Lucid, tx: C.Transaction, utxos?: UTxO[]) {
this.lucid = lucid;
this.txComplete = tx;
this.witnessSetBuilder = C.TransactionWitnessSetBuilder.new();
this.tasks = [];
this.utxos = utxos;

this.fee = parseInt(tx.body().fee().to_str());
const redeemers = tx.witness_set().redeemers();
Expand Down Expand Up @@ -111,4 +117,63 @@ export class TxComplete {
toHash(): TxHash {
return C.hash_transaction(this.txComplete.body()).to_hex();
}
}

/**
* This function provides access to the produced outputs of the current transaction
* that can be selectively picked to be chained with a new transaction which is returned
* as result.
*
* @param outputChainSelector provides the tx outputs of the transaction that can be used for chaining a new tx.
* If undefined is returned from this function, all outputs that are spendable from this wallet are chained.
* @param redeemer this arguments is expected to match the number of selected chained outputs from the first argument and can be used
* to chain script outputs with specific redeemers.
* @returns a new transaction that already has inputs set defined by the *outputChainSelector* function.
*/
chain(
outputChainSelector: (utxos: UTxO[]) => UTxO | UTxO[] | undefined,
redeemer?: string | string[] | undefined
): Tx {
const txOutputs = producedUtxosFrom(this);
let chainedOutputs = outputChainSelector(txOutputs);
const inputUTxOs = this.getUpdatedInputUTxOs(this.utxos);
const chainedTx = this.lucid
.newTx()
.collectTxInputsFrom(inputUTxOs);

if (!chainedOutputs || Array.isArray(chainedOutputs) && chainedOutputs.length === 0) {
// chain all spendable unspent transaction outputs
chainedOutputs = inputUTxOs
}

if (Array.isArray(chainedOutputs) && Array.isArray(redeemer)) {
if (!redeemer || chainedOutputs.length === redeemer.length) {
chainedOutputs.forEach((utxo, i) => chainedTx.collectFrom([utxo], redeemer.at(i)));
} else {
throw new Error(`Mismatching number of chained outputs (${chainedOutputs.length}) & redeemers (${redeemer.length})`);
}
} else if (!Array.isArray(chainedOutputs) && !Array.isArray(redeemer)) {
chainedTx.collectFrom([chainedOutputs], redeemer);
} else {
throw new Error('Mismatching types for provided chained output(s) and redeemer(s).');
}
return chainedTx;
}

private getUpdatedInputUTxOs(
inputUTxOs?: UTxO[]
): UTxO[] {
if (!inputUTxOs) return [];
const paymentCredentials = inputUTxOs.map(({ address }) => paymentCredentialOf(address));
const consumedOutRefs = coresToOutRefs(this.txComplete.body().inputs());
const isSpendableByCreds = (walletPaymentCredentials: Credential[]) =>
({ address }: UTxO) =>
walletPaymentCredentials.find(({ hash: walletPKeyHash }) => {
const { paymentCredential: outputPayCred } = getAddressDetails(address);
return (outputPayCred && walletPKeyHash === outputPayCred.hash && outputPayCred.type === "Key");
}) !== undefined;
const producedUtxos = producedUtxosFrom(this);
const isNotConsumed = ({ txHash, outputIndex }: UTxO) => consumedOutRefs.find(outRef => outRef.txHash === txHash && outRef.outputIndex === outputIndex) === undefined;
const isSpendable = isSpendableByCreds(paymentCredentials);
return inputUTxOs.filter(isNotConsumed).concat(producedUtxos.filter(isSpendable));
}
}
14 changes: 6 additions & 8 deletions src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface Provider {
/** Query UTxOs by address or payment credential filtered by a specific unit. */
getUtxosWithUnit(
addressOrCredential: Address | Credential,
unit: Unit,
unit: Unit
): Promise<UTxO[]>;
/** Query a UTxO by a unit. It needs to be an NFT (or optionally the entire supply in one UTxO). */
getUtxoByUnit(unit: Unit): Promise<UTxO>;
Expand Down Expand Up @@ -125,18 +125,16 @@ export type ScriptRef = string;
/** Hex */
export type Payload = string;

export type UTxO = {
txHash: TxHash;
outputIndex: number;
assets: Assets;
export type UTxO = OutRef & TxOutput;
export type OutRef = { txHash: TxHash; outputIndex: number };
export type TxOutput = {
address: Address;
assets: Assets;
datumHash?: DatumHash | null;
datum?: Datum | null;
scriptRef?: Script | null;
};

export type OutRef = { txHash: TxHash; outputIndex: number };

export type AddressType =
| "Base"
| "Enterprise"
Expand Down Expand Up @@ -180,7 +178,7 @@ export interface Wallet {
signTx(tx: C.Transaction): Promise<C.TransactionWitnessSet>;
signMessage(
address: Address | RewardAddress,
payload: Payload,
payload: Payload
): Promise<SignedMessage>;
submitTx(signedTx: Transaction): Promise<TxHash>;
}
Expand Down
Loading

0 comments on commit 837a8b7

Please sign in to comment.