-
Notifications
You must be signed in to change notification settings - Fork 34
feat: new layer of FeePayer as a helper for transaction final completion step
#328
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
base: master
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: cca8107 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for liveccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for apiccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for appccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for docsccc ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
FeePayer as a helper for transaction final completion step
|
further implementation on margin fee payer for Spore module, here it is: ashuralyk#8 |
|
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a new FeePayer abstraction layer to handle the transaction completion step, which is a significant and positive architectural change. It successfully decouples fee payment logic from the Transaction class, making it more modular and extensible. The refactoring of the Signer class to leverage this new abstraction is clean. However, I've identified a critical issue in the new completeByFeePayer method where the return value of prepareTransaction is ignored, potentially leading to incorrect transaction preparation and fee calculation.
packages/core/src/ckb/transaction.ts
Outdated
| ...feePayers: FeePayer[] | ||
| ): Promise<void> { | ||
| for (const feePayer of feePayers) { | ||
| await feePayer.prepareTransaction(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with Gemini here: we should use the return value of prepareTransaction rather than assuming it modifies the tx in place. The previous implementation should be considered a bug.
Possible situation: A wallet (like JoyID, a web wallet) decides to handle the prepareTransaction stage itself to support more lock scripts in the future, and we serialize the tx for the API. This prevents the operation from happening in place.
To fix this problem without changing APIs, I assume we can use Transaction.copy to modify this at the end of completeBy* methods.
|
/gemini review |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request introduces a new FeePayer abstraction layer, significantly improving the modularity and extensibility of transaction completion logic. By moving the fee calculation and input collection responsibilities from the Transaction class to dedicated FeePayer and FeePayerFromAddress classes, the codebase becomes cleaner and more maintainable. The Signer class now correctly extends FeePayerFromAddress, inheriting these capabilities. The new test cases for margin calculation and the FeePayer layer are comprehensive and well-structured, ensuring the new functionality works as expected. This is a well-executed architectural improvement.
|
|
||
| await tx.completeByFeePayer(client, mockFeePayer1, mockFeePayer2); | ||
|
|
||
| expect(mockFeePayer1.prepareTransaction).toHaveBeenCalledWith(tx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The completeByFeePayer method clones the transaction before passing it to prepareTransaction. Therefore, mockFeePayer1.prepareTransaction receives a clone of the original tx, not the original tx object itself. The current assertion toHaveBeenCalledWith(tx) will likely fail because tx is the original transaction instance, not the cloned one passed to the mock. Consider asserting with expect.any(ccc.Transaction) or expect.objectContaining if specific properties of the cloned transaction need to be verified.
| expect(mockFeePayer1.prepareTransaction).toHaveBeenCalledWith(tx); | |
| expect(mockFeePayer1.prepareTransaction).toHaveBeenCalledWith(expect.any(ccc.Transaction)); |
| await tx.completeByFeePayer(client, mockFeePayer1, mockFeePayer2); | ||
|
|
||
| expect(mockFeePayer1.prepareTransaction).toHaveBeenCalledWith(tx); | ||
| expect(mockFeePayer2.prepareTransaction).toHaveBeenCalledWith(tx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar to the previous comment, mockFeePayer2.prepareTransaction also receives a cloned transaction. The assertion toHaveBeenCalledWith(tx) should be updated to reflect this.
| expect(mockFeePayer2.prepareTransaction).toHaveBeenCalledWith(tx); | |
| expect(mockFeePayer2.prepareTransaction).toHaveBeenCalledWith(expect.any(ccc.Transaction)); |
Hanssen0
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great design for the FeePayer aspect! This should help us reduce future workload. However, this PR still needs to be polished to be developer-friendly.
An important note here is that TypeLike is usually for parameter types, while Type is for member/variable types. If a member of a new type has a TypeLike type, it almost always means the new type should also be a TypeLike.
I suggest making new option types here TypeLike, and adding corresponding new Type for them. For example:
export interface FeeRateOptionsLike {
feeRate?: NumLike;
options?: {
feeRateBlockRange?: NumLike;
maxFeeRate?: NumLike;
};
}
export class FeeRateOptions {
constructor(
public readonly feeRate?: Num;
public readonly options?: {
feeRateBlockRange?: Num;
maxFeeRate?: Num;
}
) {}
...
}| }, numFrom(0)); | ||
| } | ||
|
|
||
| getOutputCapacityMargin(index: number): Num { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that both margin and getOutputCapacityMargin methods are not used in this PR. Let's remove them until we see the actual use case.
| @@ -0,0 +1,6 @@ | |||
| --- | |||
| "@ckb-ccc/core": patch | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a minor change since it introduced new APIs.
| addedCount: collectedCells.length, | ||
| accumulated: acc, | ||
| }; | ||
| const { addedCount, accumulated } = await from.completeInputs( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm good with moving completeInputs to FeePayer interface to allow devs to customize input collection logic. But it seems that we're currently placing these methods in the FeePayerFromAddress class, which means other FeePayers like FeePayerGroup can't use this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To apply this change, I need to move getAddressObjs pairing with completeInputs as well, I originally thought the concept of Address is dedicated for FeePayerFromAddress only, not a general concept for all of FeePayers
| export abstract class FeePayer { | ||
| constructor() {} | ||
|
|
||
| abstract completeTxFee( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it's better to make completeTxFee a pure function (tx: TxLike) => Tx? Modifying a function argument in place is always confusing and problem-causing.
Also, this helps us to relax the parameter type restriction from Tx to TxLike.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid misunderstanding of your suggestion, are you only pointing out to replace the first parameter of tx: Transaction into tx: TransactionLike, despite erasing the others after it?
| }; | ||
| } | ||
|
|
||
| setOptionalProperties(props: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of setting/changing options after constructing an instance, I suggest we put these parameters in the class constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because of the original implementation, these options are set per related method as an optional function parameter, this causes each call of those methods can accept totally different options even they belong to a same instance.
Moving the initialization of those options into class constructor is absolutely fine, but do we still keep setOptionalProperties method for the opportunity of changing them for the next call of other methods?
| return this; | ||
| } | ||
|
|
||
| pop(): FeePayer | undefined { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's hard to see the use case where we need to modify the fee payers after creating the group. I think we should just remove the push & pop method and let devs specify them before constructing.
| export abstract class Signer { | ||
| constructor(protected client_: Client) {} | ||
| export abstract class Signer extends FeePayerFromAddress { | ||
| constructor(protected client_: Client) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think it should make our life easier if we move this client_ member to FeePayer, so that we can eliminate those client parameters appearing in every method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, your suggestion is about replacing extending Signer with FeePayerFromAddress with just injecting an abstracted parameter FeePayer to its constructor?

Description
Add a new layer named
FeePayer(could be changed further), to abstract our transaction complete step, like balancing process, paying with capacity margin or with DeFi swap ratio, etc.This change can bring much clearer layer hierarchy on CCC, it makes transaction completion step programmable.