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 aa23259
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 59 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,22 @@ const txHash = await signedTx.submit();
console.log(txHash);
```

### Tx Chaining

```js
const tx1 = await lucid.newTx()
.payToAddress("addr...", { lovelace: 5000000n })
.complete();

const tx2 = tx1
// select tx1 outputs that shall become inputs for tx2
.chain((outputs) => outputs.filter(output => output.address === 'addr...'))
.payToAddress("addr...", { lovelace: 2500000n })
.complete();

// sign tx1 & tx2, submit them
```

### Test

```
Expand Down
6 changes: 3 additions & 3 deletions docs/docs/getting-started/choose-provider.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ import { Lucid, Maestro } from "https://deno.land/x/lucid/mod.ts";

const lucid = await Lucid.new(
new Maestro({
network: "Preprod", // For MAINNET: "Mainnet".
apiKey: "<Your-API-Key>", // Get yours by visiting https://docs.gomaestro.org/docs/Getting-started/Sign-up-login.
turboSubmit: false // Read about paid turbo transaction submission feature at https://docs.gomaestro.org/docs/Dapp%20Platform/Turbo%20Transaction.
network: "Preprod", // For MAINNET: "Mainnet".
apiKey: "<Your-API-Key>", // Get yours by visiting https://docs.gomaestro.org/docs/Getting-started/Sign-up-login.
turboSubmit: false, // Read about paid turbo transaction submission feature at https://docs.gomaestro.org/docs/Dapp%20Platform/Turbo%20Transaction.
}),
"Preprod", // For MAINNET: "Mainnet".
);
Expand Down
34 changes: 17 additions & 17 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 @@ -32,14 +26,20 @@ import {
Wallet,
WalletApi,
} from "../types/mod.ts";
import {
coreToUtxo,
createCostModels,
fromHex,
fromUnit,
paymentCredentialOf,
toHex,
toUnit,
Utils,
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
31 changes: 26 additions & 5 deletions src/lucid/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,21 @@ 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 +97,18 @@ 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 +564,14 @@ 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 +586,6 @@ export class Tx {
]),
);
}

this.txBuilder.balance(
changeAddress,
(() => {
Expand Down Expand Up @@ -602,13 +620,16 @@ 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
95 changes: 93 additions & 2 deletions src/lucid/tx_complete.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
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 +124,82 @@ 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),
);
}
}
10 changes: 4 additions & 6 deletions src/types/types.ts
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit aa23259

Please sign in to comment.