Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: chaining of txs #231

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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