Skip to content

Conversation

@ashuralyk
Copy link
Contributor

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.

@changeset-bot
Copy link

changeset-bot bot commented Nov 3, 2025

🦋 Changeset detected

Latest commit: cca8107

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 18 packages
Name Type
@ckb-ccc/core Patch
@ckb-ccc/eip6963 Patch
@ckb-ccc/joy-id Patch
@ckb-ccc/lumos-patches Patch
@ckb-ccc/nip07 Patch
@ckb-ccc/okx Patch
@ckb-ccc/rei Patch
@ckb-ccc/shell Patch
@ckb-ccc/spore Patch
@ckb-ccc/ssri Patch
@ckb-ccc/udt Patch
@ckb-ccc/uni-sat Patch
@ckb-ccc/utxo-global Patch
@ckb-ccc/xverse Patch
@ckb-ccc/ccc Patch
ckb-ccc Patch
@ckb-ccc/connector Patch
@ckb-ccc/connector-react Patch

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

@netlify
Copy link

netlify bot commented Nov 3, 2025

Deploy Preview for liveccc ready!

Name Link
🔨 Latest commit cca8107
🔍 Latest deploy log https://app.netlify.com/projects/liveccc/deploys/69301e61f370c40008043597
😎 Deploy Preview https://deploy-preview-328--liveccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 23 (🟢 up 3 from production)
Accessibility: 88 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 100 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 3, 2025

Deploy Preview for apiccc ready!

Name Link
🔨 Latest commit cca8107
🔍 Latest deploy log https://app.netlify.com/projects/apiccc/deploys/69301e6129a35b00084aa66e
😎 Deploy Preview https://deploy-preview-328--apiccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 88 (🟢 up 6 from production)
Accessibility: 100 (no change from production)
Best Practices: 100 (no change from production)
SEO: 94 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 3, 2025

Deploy Preview for appccc ready!

Name Link
🔨 Latest commit cca8107
🔍 Latest deploy log https://app.netlify.com/projects/appccc/deploys/69301e6182f46b000891802a
😎 Deploy Preview https://deploy-preview-328--appccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 68 (🔴 down 7 from production)
Accessibility: 89 (🟢 up 1 from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 100 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Nov 3, 2025

Deploy Preview for docsccc ready!

Name Link
🔨 Latest commit cca8107
🔍 Latest deploy log https://app.netlify.com/projects/docsccc/deploys/69301e616f68bc00089bc82f
😎 Deploy Preview https://deploy-preview-328--docsccc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 65 (🔴 down 2 from production)
Accessibility: 88 (no change from production)
Best Practices: 92 (no change from production)
SEO: 92 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@ashuralyk ashuralyk changed the title feat: fee payer feat: new layer of FeePayer as a helper for transaction final completion step Nov 5, 2025
@ashuralyk
Copy link
Contributor Author

ashuralyk commented Nov 13, 2025

further implementation on margin fee payer for Spore module, here it is: ashuralyk#8

@Hanssen0

@ashuralyk ashuralyk marked this pull request as ready for review November 13, 2025 11:56
@ashuralyk
Copy link
Contributor Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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.

...feePayers: FeePayer[]
): Promise<void> {
for (const feePayer of feePayers) {
await feePayer.prepareTransaction(this);
Copy link
Member

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.

@ashuralyk
Copy link
Contributor Author

/gemini review

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a 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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous comment, mockFeePayer2.prepareTransaction also receives a cloned transaction. The assertion toHaveBeenCalledWith(tx) should be updated to reflect this.

Suggested change
expect(mockFeePayer2.prepareTransaction).toHaveBeenCalledWith(tx);
expect(mockFeePayer2.prepareTransaction).toHaveBeenCalledWith(expect.any(ccc.Transaction));

Copy link
Member

@Hanssen0 Hanssen0 left a 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 {
Copy link
Member

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
Copy link
Member

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(
Copy link
Member

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.

Copy link
Contributor Author

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(
Copy link
Member

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.

Copy link
Contributor Author

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: {
Copy link
Member

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.

Copy link
Contributor Author

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 {
Copy link
Member

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) {
Copy link
Member

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.

Copy link
Contributor Author

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants