Skip to content

Commit

Permalink
feat(core): fee rate checks
Browse files Browse the repository at this point in the history
  • Loading branch information
Hanssen0 committed Jan 23, 2025
1 parent a2bf9a9 commit a68072a
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/proud-crabs-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-ccc/core": minor
---

feat(core): fee rate checks
33 changes: 21 additions & 12 deletions packages/core/src/ckb/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1567,18 +1567,33 @@ export class Transaction extends mol.Entity.Base<
return this.completeInputsAddOne(from, filter);
}

async fee(client: Client): Promise<Num> {
return (await this.getInputsCapacity(client)) - this.getOutputsCapacity();
}

async feeRate(client: Client): Promise<Num> {
return (
((await this.fee(client)) * numFrom(1000)) /
numFrom(this.toBytes().length + 4)
);
}

estimateFee(feeRate: NumLike): Num {
const txSize = this.toBytes().length + 4;
return (numFrom(txSize) * numFrom(feeRate) + numFrom(1000)) / numFrom(1000);
// + 999 then / 1000 to ceil the calculated fee
return (numFrom(txSize) * numFrom(feeRate) + numFrom(999)) / numFrom(1000);
}

async completeFee(
from: Signer,
change: (tx: Transaction, capacity: Num) => Promise<NumLike> | NumLike,
expectedFeeRate?: NumLike,
filter?: ClientCollectableSearchKeyFilterLike,
options?: { feeRateBlockRange?: NumLike; maxFeeRate?: NumLike },
): Promise<[number, boolean]> {
const feeRate = expectedFeeRate ?? (await from.client.getFeeRate());
const feeRate =
expectedFeeRate ??
(await from.client.getFeeRate(options?.feeRateBlockRange, options));

// Complete all inputs extra infos for cache
await this.getInputsCapacity(from.client);
Expand Down Expand Up @@ -1609,27 +1624,21 @@ export class Transaction extends mol.Entity.Base<
// The initial fee is calculated based on prepared transaction
leastFee = tx.estimateFee(feeRate);
}
const extraCapacity =
(await tx.getInputsCapacity(from.client)) - tx.getOutputsCapacity();
const fee = await tx.fee(from.client);
// The extra capacity paid the fee without a change
if (extraCapacity === leastFee) {
if (fee === leastFee) {
this.copy(tx);
return [collected, false];
}

const needed = numFrom(
await Promise.resolve(change(tx, extraCapacity - leastFee)),
);
const needed = numFrom(await Promise.resolve(change(tx, fee - leastFee)));
// No enough extra capacity to create new cells for change
if (needed > Zero) {
leastExtraCapacity = needed;
continue;
}

if (
(await tx.getInputsCapacity(from.client)) - tx.getOutputsCapacity() !==
leastFee
) {
if ((await tx.fee(from.client)) !== leastFee) {
throw new Error(
"The change function doesn't use all available capacity",
);
Expand Down
27 changes: 24 additions & 3 deletions packages/core/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "../ckb/index.js";
import { Zero } from "../fixedPoint/index.js";
import { Hex, HexLike, hexFrom } from "../hex/index.js";
import { Num, NumLike, numFrom, numMax } from "../num/index.js";
import { Num, NumLike, numFrom, numMax, numMin } from "../num/index.js";
import { reduceAsync, sleep } from "../utils/index.js";
import { ClientCache } from "./cache/index.js";
import { ClientCacheMemory } from "./cache/memory.js";
Expand All @@ -26,6 +26,7 @@ import {
ClientIndexerSearchKeyLike,
ClientIndexerSearchKeyTransactionLike,
ClientTransactionResponse,
ErrorClientMaxFeeRateExceeded,
ErrorClientWaitTransactionTimeout,
KnownScript,
OutputsValidator,
Expand All @@ -50,8 +51,21 @@ export abstract class Client {
abstract getFeeRateStatistics(
blockRange?: NumLike,
): Promise<{ mean: Num; median: Num }>;
async getFeeRate(blockRange?: NumLike): Promise<Num> {
return numMax((await this.getFeeRateStatistics(blockRange)).median, 1000);
async getFeeRate(
blockRange?: NumLike,
options?: { maxFeeRate?: NumLike },
): Promise<Num> {
const feeRate = numMax(
(await this.getFeeRateStatistics(blockRange)).median,
1000,
);

const maxFeeRate = numFrom(options?.maxFeeRate ?? 10000000);
if (maxFeeRate === Zero) {
return feeRate;
}

return numMin(feeRate, maxFeeRate);
}

abstract getTip(): Promise<Num>;
Expand Down Expand Up @@ -476,9 +490,16 @@ export abstract class Client {
async sendTransaction(
transaction: TransactionLike,
validator?: OutputsValidator,
options?: { maxFeeRate?: NumLike },
): Promise<Hex> {
const tx = Transaction.from(transaction);

const maxFeeRate = numFrom(options?.maxFeeRate ?? 10000000);
const fee = await tx.feeRate(this);
if (maxFeeRate > Zero && fee > maxFeeRate) {
throw new ErrorClientMaxFeeRateExceeded(maxFeeRate, fee);
}

const txHash = await this.sendTransactionNoCache(tx, validator);

await this.cache.recordTransactions(tx);
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/client/clientTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,8 +392,21 @@ export class ErrorClientRBFRejected extends ErrorClientBase {
}
}

export class ErrorClientWaitTransactionTimeout extends Error {
export class ErrorClientWaitTransactionTimeout extends ErrorClientBase {
constructor() {
super("Wait transaction timeout");
super({
message: "Wait transaction timeout",
data: "Wait transaction timeout",
});
}
}

export class ErrorClientMaxFeeRateExceeded extends ErrorClientBase {
constructor(limit: NumLike, actual: NumLike) {
const message = `Max fee rate exceeded limit ${numFrom(limit).toString()}, actual ${numFrom(actual).toString()}`;
super({
message,
data: message,
});
}
}

0 comments on commit a68072a

Please sign in to comment.