Skip to content

Commit 6fda61c

Browse files
committed
feat(core): fee rate checks
1 parent a2bf9a9 commit 6fda61c

File tree

5 files changed

+76
-19
lines changed

5 files changed

+76
-19
lines changed

.changeset/proud-crabs-grow.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@ckb-ccc/core": minor
3+
---
4+
5+
feat(core): fee rate checks

packages/core/src/ckb/transaction.ts

+21-12
Original file line numberDiff line numberDiff line change
@@ -1567,18 +1567,33 @@ export class Transaction extends mol.Entity.Base<
15671567
return this.completeInputsAddOne(from, filter);
15681568
}
15691569

1570+
async fee(client: Client): Promise<Num> {
1571+
return (await this.getInputsCapacity(client)) - this.getOutputsCapacity();
1572+
}
1573+
1574+
async feeRate(client: Client): Promise<Num> {
1575+
return (
1576+
((await this.fee(client)) * numFrom(1000)) /
1577+
numFrom(this.toBytes().length + 4)
1578+
);
1579+
}
1580+
15701581
estimateFee(feeRate: NumLike): Num {
15711582
const txSize = this.toBytes().length + 4;
1572-
return (numFrom(txSize) * numFrom(feeRate) + numFrom(1000)) / numFrom(1000);
1583+
// + 999 then / 1000 to ceil the calculated fee
1584+
return (numFrom(txSize) * numFrom(feeRate) + numFrom(999)) / numFrom(1000);
15731585
}
15741586

15751587
async completeFee(
15761588
from: Signer,
15771589
change: (tx: Transaction, capacity: Num) => Promise<NumLike> | NumLike,
15781590
expectedFeeRate?: NumLike,
15791591
filter?: ClientCollectableSearchKeyFilterLike,
1592+
options?: { feeRateBlockRange?: NumLike; maxFeeRate?: NumLike },
15801593
): Promise<[number, boolean]> {
1581-
const feeRate = expectedFeeRate ?? (await from.client.getFeeRate());
1594+
const feeRate =
1595+
expectedFeeRate ??
1596+
(await from.client.getFeeRate(options?.feeRateBlockRange, options));
15821597

15831598
// Complete all inputs extra infos for cache
15841599
await this.getInputsCapacity(from.client);
@@ -1609,27 +1624,21 @@ export class Transaction extends mol.Entity.Base<
16091624
// The initial fee is calculated based on prepared transaction
16101625
leastFee = tx.estimateFee(feeRate);
16111626
}
1612-
const extraCapacity =
1613-
(await tx.getInputsCapacity(from.client)) - tx.getOutputsCapacity();
1627+
const fee = await tx.fee(from.client);
16141628
// The extra capacity paid the fee without a change
1615-
if (extraCapacity === leastFee) {
1629+
if (fee === leastFee) {
16161630
this.copy(tx);
16171631
return [collected, false];
16181632
}
16191633

1620-
const needed = numFrom(
1621-
await Promise.resolve(change(tx, extraCapacity - leastFee)),
1622-
);
1634+
const needed = numFrom(await Promise.resolve(change(tx, fee - leastFee)));
16231635
// No enough extra capacity to create new cells for change
16241636
if (needed > Zero) {
16251637
leastExtraCapacity = needed;
16261638
continue;
16271639
}
16281640

1629-
if (
1630-
(await tx.getInputsCapacity(from.client)) - tx.getOutputsCapacity() !==
1631-
leastFee
1632-
) {
1641+
if ((await tx.fee(from.client)) !== leastFee) {
16331642
throw new Error(
16341643
"The change function doesn't use all available capacity",
16351644
);

packages/core/src/client/client.ts

+29-4
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,15 @@ import {
99
} from "../ckb/index.js";
1010
import { Zero } from "../fixedPoint/index.js";
1111
import { Hex, HexLike, hexFrom } from "../hex/index.js";
12-
import { Num, NumLike, numFrom, numMax } from "../num/index.js";
12+
import { Num, NumLike, numFrom, numMax, numMin } from "../num/index.js";
1313
import { reduceAsync, sleep } from "../utils/index.js";
1414
import { ClientCache } from "./cache/index.js";
1515
import { ClientCacheMemory } from "./cache/memory.js";
16-
import { ClientCollectableSearchKeyLike } from "./clientTypes.advanced.js";
16+
import {
17+
ClientCollectableSearchKeyLike,
18+
DEFAULT_MAX_FEE_RATE,
19+
DEFAULT_MIN_FEE_RATE,
20+
} from "./clientTypes.advanced.js";
1721
import {
1822
CellDepInfo,
1923
CellDepInfoLike,
@@ -26,6 +30,7 @@ import {
2630
ClientIndexerSearchKeyLike,
2731
ClientIndexerSearchKeyTransactionLike,
2832
ClientTransactionResponse,
33+
ErrorClientMaxFeeRateExceeded,
2934
ErrorClientWaitTransactionTimeout,
3035
KnownScript,
3136
OutputsValidator,
@@ -50,8 +55,21 @@ export abstract class Client {
5055
abstract getFeeRateStatistics(
5156
blockRange?: NumLike,
5257
): Promise<{ mean: Num; median: Num }>;
53-
async getFeeRate(blockRange?: NumLike): Promise<Num> {
54-
return numMax((await this.getFeeRateStatistics(blockRange)).median, 1000);
58+
async getFeeRate(
59+
blockRange?: NumLike,
60+
options?: { maxFeeRate?: NumLike },
61+
): Promise<Num> {
62+
const feeRate = numMax(
63+
(await this.getFeeRateStatistics(blockRange)).median,
64+
DEFAULT_MIN_FEE_RATE,
65+
);
66+
67+
const maxFeeRate = numFrom(options?.maxFeeRate ?? DEFAULT_MAX_FEE_RATE);
68+
if (maxFeeRate === Zero) {
69+
return feeRate;
70+
}
71+
72+
return numMin(feeRate, maxFeeRate);
5573
}
5674

5775
abstract getTip(): Promise<Num>;
@@ -476,9 +494,16 @@ export abstract class Client {
476494
async sendTransaction(
477495
transaction: TransactionLike,
478496
validator?: OutputsValidator,
497+
options?: { maxFeeRate?: NumLike },
479498
): Promise<Hex> {
480499
const tx = Transaction.from(transaction);
481500

501+
const maxFeeRate = numFrom(options?.maxFeeRate ?? DEFAULT_MAX_FEE_RATE);
502+
const fee = await tx.feeRate(this);
503+
if (maxFeeRate > Zero && fee > maxFeeRate) {
504+
throw new ErrorClientMaxFeeRateExceeded(maxFeeRate, fee);
505+
}
506+
482507
const txHash = await this.sendTransactionNoCache(tx, validator);
483508

484509
await this.cache.recordTransactions(tx);

packages/core/src/client/clientTypes.advanced.ts

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import { ScriptLike } from "../ckb/index.js";
22
import { HexLike } from "../hex/index.js";
33
import { Num, numFrom, NumLike } from "../num/index.js";
44

5+
export const DEFAULT_MAX_FEE_RATE = 10000000;
6+
export const DEFAULT_MIN_FEE_RATE = 1000;
7+
58
export function clientSearchKeyRangeFrom([a, b]: [NumLike, NumLike]): [
69
Num,
710
Num,

packages/core/src/client/clientTypes.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -392,8 +392,23 @@ export class ErrorClientRBFRejected extends ErrorClientBase {
392392
}
393393
}
394394

395-
export class ErrorClientWaitTransactionTimeout extends Error {
396-
constructor() {
397-
super("Wait transaction timeout");
395+
export class ErrorClientWaitTransactionTimeout extends ErrorClientBase {
396+
constructor(timeoutLike: NumLike) {
397+
const timeout = numFrom(timeoutLike);
398+
super({
399+
message: `Wait transaction timeout ${timeout}ms`,
400+
data: JSON.stringify({ timeout }),
401+
});
402+
}
403+
}
404+
405+
export class ErrorClientMaxFeeRateExceeded extends ErrorClientBase {
406+
constructor(limitLike: NumLike, actualLike: NumLike) {
407+
const limit = numFrom(limitLike);
408+
const actual = numFrom(actualLike);
409+
super({
410+
message: `Max fee rate exceeded limit ${limit.toString()}, actual ${actual.toString()}. Developer might forgot to complete transaction fee before sending. See https://docs.ckbccc.com/classes/_ckb_ccc_core.index.ccc.Transaction.html#completeFeeBy.`,
411+
data: JSON.stringify({ limit, actual }),
412+
});
398413
}
399414
}

0 commit comments

Comments
 (0)