Skip to content

Commit

Permalink
feat: add option to include tiny change in transaction fee
Browse files Browse the repository at this point in the history
  • Loading branch information
hadelive committed Nov 7, 2024
1 parent e88f247 commit b2c367b
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-impalas-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@lucid-evolution/lucid": patch
---

inlude tiny change in fee
49 changes: 38 additions & 11 deletions packages/lucid/src/tx-builder/internal/CompleteTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ export type CompleteOptions = {
*/
canonical?: boolean;

/**
* Include tiny change (lovelace) in transaction fee if it's too small
* @default false
*/
includeTinyChangeInFee?: boolean;

/**
* Preset UTXOs from the wallet to include in coin selection.
* If not provided, wallet UTXOs will be fetched by the provider.
Expand All @@ -81,6 +87,11 @@ export type CompleteOptions = {
presetWalletInputs?: UTxO[];
};

type CoinSelectionResult = {
selected: UTxO[];
burnable: Assets;
};

export const completeTxError = (cause: unknown) =>
new TxBuilderError({ cause: `{ Complete: ${cause} }` });

Expand All @@ -105,6 +116,7 @@ export const complete = (
localUPLCEval = true,
setCollateral = 5_000_000n,
canonical = false,
includeTinyChangeInFee = false,
presetWalletInputs = [],
} = options;

Expand Down Expand Up @@ -138,6 +150,7 @@ export const complete = (
changeAddress,
coinSelection,
localUPLCEval,
includeTinyChangeInFee,
false,
);
// Second round of coin selection by including script execution costs in fee estimation.
Expand All @@ -150,6 +163,7 @@ export const complete = (
changeAddress,
coinSelection,
localUPLCEval,
includeTinyChangeInFee,
true,
);

Expand Down Expand Up @@ -200,6 +214,7 @@ export const selectionAndEvaluation = (
changeAddress: string,
coinSelection: boolean,
localUPLCEval: boolean,
includeTinyChangeInFee: boolean,
script_calculation: boolean,
): Effect.Effect<void, TransactionError, never> =>
Effect.gen(function* () {
Expand All @@ -208,14 +223,22 @@ export const selectionAndEvaluation = (
config.collectedInputs,
);

const inputsToAdd =
const { selected: inputsToAdd, burnable } =
coinSelection !== false
? yield* doCoinSelection(config, availableInputs, script_calculation)
: [];
? yield* doCoinSelection(
config,
availableInputs,
script_calculation,
includeTinyChangeInFee,
)
: { selected: [], burnable: {} };

// Skip UPLC evaluation for the second time if no new inputs are added
if (_Array.isEmptyArray(inputsToAdd) && script_calculation) return;
let estimatedFee = 0n;
if (_Array.isEmptyArray(inputsToAdd)) {
if (script_calculation) return;
estimatedFee += burnable.lovelace;
}
if (_Array.isNonEmptyArray(inputsToAdd)) {
for (const utxo of inputsToAdd) {
const input = CML.SingleInputBuilder.from_transaction_unspent_output(
Expand Down Expand Up @@ -466,14 +489,14 @@ const findCollateral = (
`Your wallet does not have enough funds to cover the required ${setCollateral} Lovelace collateral. Or it contains UTxOs with reference scripts; which
are excluded from collateral selection.`,
);
const selected = yield* recursive(
const { selected } = yield* recursive(
sortUTxOs(inputs),
collateralLovelace,
coinsPerUtxoByte,
undefined,
false,
error,
);

if (selected.length > 3)
yield* completeTxError(
`Selected ${selected.length} inputs as collateral, but max collateral inputs is 3 to cover the ${setCollateral} Lovelace collateral ${stringify(selected)}`,
Expand All @@ -485,7 +508,8 @@ const doCoinSelection = (
config: TxBuilder.TxBuilderConfig,
availableInputs: UTxO[],
script_calculation: boolean,
): Effect.Effect<UTxO[], TxBuilderError> =>
includeTinyChangeInFee: boolean,
): Effect.Effect<{ selected: UTxO[]; burnable: Assets }, TxBuilderError> =>
Effect.gen(function* () {
// NOTE: This is a fee estimation. If the amount is not enough, it may require increasing the fee.
const estimatedFee: Assets = {
Expand Down Expand Up @@ -520,13 +544,13 @@ const doCoinSelection = (
// Note: We are not done with coin selection even if "requiredAssets" is empty.
// Because "notRequiredAssets" may not contain enough ADA to cover for minimum Ada requirement
// when they need to be sent as change output. Hence, we allow for "recursive" to be invoked.
const selected = yield* recursive(
return yield* recursive(
sortUTxOs(availableInputs),
requiredAssets,
config.lucidConfig.protocolParameters.coinsPerUtxoByte,
notRequiredAssets,
includeTinyChangeInFee,
);
return selected;
});

/**
Expand Down Expand Up @@ -745,8 +769,9 @@ export const recursive = (
requiredAssets: Assets,
coinsPerUtxoByte: bigint,
externalAssets: Assets = {},
includeTinyChangeInFee?: boolean,
error?: TxBuilderError,
): Effect.Effect<UTxO[], TxBuilderError> =>
): Effect.Effect<CoinSelectionResult, TxBuilderError> =>
Effect.gen(function* () {
let selected: UTxO[] = [];
error ??= completeTxError(
Expand Down Expand Up @@ -779,6 +804,8 @@ export const recursive = (

const extraSelected = selectUTxOs(remainingInputs, extraLovelace);
if (_Array.isEmptyArray(extraSelected)) {
if (includeTinyChangeInFee)
return { selected: [...selected], burnable: extraLovelace };
yield* completeTxError(
`Your wallet does not have enough funds to cover required minimum ADA for change output: ${stringify(extraLovelace)}
Or it contains UTxOs with reference scripts; which are excluded from coin selection.`,
Expand All @@ -797,5 +824,5 @@ export const recursive = (
Option.getOrUndefined,
);
}
return selected;
return { selected, burnable: { lovelace: 0n } };
});
8 changes: 8 additions & 0 deletions packages/lucid/test/emulator/emulator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
signByWalletFromPrivateKey,
emulatorFromPrivateKey,
depositFundsLockRefScript,
sendAllFund,
} from "./service.js";

const distributeRewards = Effect.gen(function* ($) {
Expand Down Expand Up @@ -164,4 +165,11 @@ describe.sequential("Emulator", () => {
emulatorFromPrivateKey.awaitBlock(4);
expect(exit._tag).toBe("Success");
});
test("sendAllFund", async () => {
const program = pipe(sendAllFund, Effect.provide(EmulatorUser.layer));
const exit = await Effect.runPromiseExit(program);
emulator.awaitBlock(4);
emulatorFromPrivateKey.awaitBlock(4);
expect(exit._tag).toBe("Success");
});
});
50 changes: 49 additions & 1 deletion packages/lucid/test/emulator/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const emulatorFromPrivateKey = await Effect.gen(function* () {
}).pipe(Effect.runPromise);

export const emulator = await Effect.gen(function* () {
return new Emulator([EMULATOR_ACCOUNT]);
return new Emulator([EMULATOR_ACCOUNT, EMULATOR_ACCOUNT_1]);
}).pipe(Effect.runPromise);

const makeEmulatorUser = Effect.gen(function* ($) {
Expand Down Expand Up @@ -327,3 +327,51 @@ export const depositFundsLockRefScript = Effect.gen(function* ($) {
.completeProgram();
return signBuilder;
}).pipe(Effect.flatMap(handleSignSubmitWithoutValidation), withLogRetry);

export const sendAllFund = Effect.gen(function* ($) {
const { user } = yield* EmulatorUser;
user.selectWallet.fromSeed(EMULATOR_ACCOUNT_1.seedPhrase);
const publicKeyHash = getAddressDetails(
yield* Effect.promise(() => user.wallet().address()),
).paymentCredential?.hash;
const datum = Data.to(new Constr(0, [publicKeyHash!]));

const helloCBOR = yield* pipe(
Effect.fromNullable(
scripts.validators.find(
(v) => v.title === "hello_world.hello_world.spend",
),
),
Effect.andThen((script) => script.compiledCode),
);
const hello: SpendingValidator = {
type: "PlutusV3",
script: applyDoubleCborEncoding(helloCBOR),
};
const contractAddress = validatorToAddress("Custom", hello);
yield* pipe(
Effect.tryPromise(() => user.utxosAt(contractAddress)),
// Effect.andThen((utxos) => Console.log(utxos)),
);

const utxos = yield* Effect.promise(() => user.wallet().getUtxos());
let totalFund = 0n;
for (const utxo of utxos) {
totalFund += utxo.assets["lovelace"];
}
let remaining = 500_000n;

const signBuilder = yield* user
.newTx()
.pay.ToAddressWithData(
contractAddress,
{
kind: "inline",
value: datum,
},
{ lovelace: totalFund - remaining },
hello,
)
.completeProgram({ includeTinyChangeInFee: true });
return signBuilder;
}).pipe(Effect.flatMap(handleSignSubmitWithoutValidation), withLogRetry);

0 comments on commit b2c367b

Please sign in to comment.