Skip to content

Conversation

@phroi
Copy link
Contributor

@phroi phroi commented Aug 12, 2025

Closes #227

@changeset-bot
Copy link

changeset-bot bot commented Aug 12, 2025

⚠️ No Changeset found

Latest commit: a21ea4c

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@netlify
Copy link

netlify bot commented Aug 12, 2025

Deploy Preview for liveccc failed. Why did it fail? →

Name Link
🔨 Latest commit a21ea4c
🔍 Latest deploy log https://app.netlify.com/projects/liveccc/deploys/689a8bb39383e80008a0be94

@netlify
Copy link

netlify bot commented Aug 12, 2025

Deploy Preview for apiccc failed. Why did it fail? →

Name Link
🔨 Latest commit a21ea4c
🔍 Latest deploy log https://app.netlify.com/projects/apiccc/deploys/689a8bb3d055aa0008c9ea50

@netlify
Copy link

netlify bot commented Aug 12, 2025

Deploy Preview for appccc failed. Why did it fail? →

Name Link
🔨 Latest commit a21ea4c
🔍 Latest deploy log https://app.netlify.com/projects/appccc/deploys/689a8bb32f6e9700087562e7

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.

Summary of Changes

Hello @phroi, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request represents a significant overhaul of the User Defined Token (UDT) management system. The primary goal is to provide a more self-contained and efficient utility for interacting with UDTs on the CKB blockchain. The changes move away from an SSRI-executor-centric approach to a direct, comprehensive management class that handles UDT discovery, balance calculations, and transaction construction, aiming to simplify UDT integration and operations for developers.

Highlights

  • Core Class Refactoring: The core Udt class has been completely re-architected and renamed to UdtManager, signifying a broader scope of responsibility for managing User Defined Tokens.
  • SSRI Protocol Decoupling: The previous reliance on the SSRI protocol for UDT metadata and operations has been removed, streamlining UDT management directly within the UdtManager.
  • Enhanced UDT Transaction Management: New robust methods have been introduced for finding UDT cells, calculating balances, and comprehensively managing UDT inputs and outputs within transactions, including handling change and ensuring sufficient funds.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

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 significant refactoring of the UDT handling logic, replacing the previous SSRI-based Udt class with a new UdtManager. This change makes the UDT logic more self-contained. The new implementation is comprehensive, but I've identified a few issues related to correctness and code clarity. My review includes suggestions to fix a critical bug in input selection, correct a faulty check, and improve the readability of the code by simplifying calculations and renaming a confusing variable.

Comment on lines +243 to +246
output.capacity - initialInput.capacity,
-(await tx.getFee(client)),
);

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 calculation for minCapacity is confusing. The two arguments passed to ccc.numMin are equivalent, as -(await tx.getFee(client)) resolves to the same value as output.capacity - initialInput.capacity. This makes the use of numMin redundant and the code harder to understand. You can simplify this by using just one of the expressions.

    const minCapacity = output.capacity - initialInput.capacity;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

False, capacity here is only protocol capacity

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While the one calculated on the transaction is the global one

}

const burned = initialInput.balance + addedInput.balance - output.balance;
if (burned < ccc.Zero) {
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 variable name burned is misleading. In the context of cryptocurrencies, "burning" usually implies destroying tokens. Here, the variable represents the amount of UDTs to be returned as change. A more descriptive name like changeBalance or balanceDiff would significantly improve code clarity. Please update this variable and its subsequent usages.

    const changeBalance = initialInput.balance + addedInput.balance - output.balance;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

burned is fine

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.

Summary of Changes

Hello @phroi, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a major refactoring of the User Defined Token (UDT) handling logic. The previous Udt class, which relied on the SSRI protocol for token interactions, has been replaced by a new UdtManager class. This new manager provides a more direct and comprehensive set of utilities for managing UDT cells within transactions, focusing on accurate balance tracking, capacity management, and robust transaction completion. The change aims to provide a more flexible and efficient way to interact with UDTs by moving away from dynamic SSRI execution for core operations.

Highlights

  • Class Refactoring and Renaming: The core Udt class has been significantly refactored and renamed to UdtManager. This change reflects a shift from a SSRI-dependent UDT implementation to a more generalized and robust UDT management utility.
  • Removal of SSRI Dependency: The previous reliance on the SSRI protocol for UDT metadata and operations (like name, symbol, transfer, mint) has been removed. The new UdtManager focuses on direct management of UDT cells within transactions.
  • Enhanced UDT Cell Management: New methods have been introduced to streamline UDT cell handling, including finding cells, calculating balances and capacities, and adding/updating UDT inputs and outputs in transactions. This provides a more granular control over UDT assets.
  • Improved Transaction Completion and Error Handling: The transaction completion logic for UDTs has been significantly improved with methods like completeInputsByBalance and completeChangeTo. These methods ensure that transactions have sufficient UDT balance and capacity, and correctly handle change outputs, including a new ErrorUdtInsufficientCoin.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in issue comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@netlify
Copy link

netlify bot commented Aug 12, 2025

Deploy Preview for docsccc ready!

Name Link
🔨 Latest commit a21ea4c
🔍 Latest deploy log https://app.netlify.com/projects/docsccc/deploys/689a8bb3a7c4750008edd7f4
😎 Deploy Preview https://deploy-preview-250--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: 72 (🟢 up 3 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.

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 major refactoring of the UDT handling logic, replacing the SSRI-based Udt class with a new UdtManager. This is a great improvement as it brings more control and transparency to the client-side, removing the dependency on a server-side executor. The new UdtManager provides a comprehensive set of tools for managing UDTs, from finding cells to building complex transactions.

However, my review has identified several critical and high-severity issues in the implementation that need to be addressed. These include incorrect capacity calculation for new UDT outputs, bugs in input gathering logic, and incorrect error reporting for insufficient balances. Please see the detailed comments for each issue. Once these are fixed, this will be a solid foundation for UDT operations.

@phroi
Copy link
Contributor Author

phroi commented Aug 12, 2025

In the end I based it on master, later on we can get back all the other goodies from your PR. OFC the code is not yet ready to be merged, this is more of a proof of concept to gather feedback.

@Hanssen0 could you review and point out what you dislike in this approach?

@phroi
Copy link
Contributor Author

phroi commented Aug 12, 2025

Strange our overlord repeated the same issues two times 🤷‍♂️

@Hanssen0
Copy link
Member

In the end I based it on master, later on we can get back all the other goodies from your PR. OFC the code is not yet ready to be merged, this is more of a proof of concept to gather feedback.

@Hanssen0 could you review and point out what you dislike in this approach?

Let me quote your comment on Discord to allow others understand what we're talking about better:

Mixing SSRI implementation details into a general UDT class is like trying to fit one foot into two shoes.

  1. Solution: Make two classes, Udt and SsriUdt:
    ✅ I'm ok with making most methods in base Udt async so they can sub-classed in SsriUdt, no way around that
    ❌ I'm not ok if base methods signatures contain SSRI details, that's a leaky abstraction
    ❌ I'm not ok if base Udt class cannot express basic metadata of the Udt as properties, just query this metadata in SsriUdt.from at creation time

  2. CompleteX methods are excessively difficult to understand due to the use of closures in parameters. The ship already sailed for ccc.Transaction CompleteX methods, but here we can still make it better.

Basic properties of interest relating to UDT cells are:

  • balance (UDT value)
  • capacity
  • count (how many cells are we adding?)

Instead of function closures, we can use a generator and we hard-code these properties as things of interest when adding cells, it simplifies everything. Any code that needs a closure to make decisions based on other computed cell properties, may not be able to fit in the closure approach anyway.

  1. At the beginning there was not enough docs, we agreed on the issue and fixed it. Now we have so much TypeDoc that makes the code difficult to follow the code logic across methods. Let's trim it down a bit.

@phroi
Copy link
Contributor Author

phroi commented Aug 14, 2025

Appreciated you got back to this PR!! 🤗

Others should also check out the previous proposed PR and our comments over there for even more context.

@Hanssen0 at high level what do you dislike about how the logic is organized in this PR?

(I will fix failing tests, comments and eventual small bugs once we agree that this approach is viable)

@Hanssen0
Copy link
Member

Mixing SSRI implementation details into a general UDT class is like trying to fit one foot into two shoes.

  1. Solution: Make two classes, Udt and SsriUdt:
    ✅ I'm ok with making most methods in base Udt async so they can sub-classed in SsriUdt, no way around that
    ❌ I'm not ok if base methods signatures contain SSRI details, that's a leaky abstraction
    ❌ I'm not ok if base Udt class cannot express basic metadata of the Udt as properties, just query this metadata in SsriUdt.from at creation time

Partially agree. My opinion:

  1. SSRI is one of the solutions to retrieve UDT information without hardcoding.
  2. SSRI is on a lower abstraction level than UDT, and a higher abstraction level than Script.
  3. We need a way to interact with on-chain scripts, but we didn't have that option in the past.

Will UDTs in the future mostly support SSRI? Or should we prefer UDT with SSRI? My answer is yes. Requiring hardcoding UDT info is a historical problem rather than a scenario we should primarily consider. UDT with SSRI should be the native way, rather than a special case of UDT.

So, I won't choose the name SsriUdt. Let's assume we have another protocol in the future, which we'll call "DDD" and require SSRI. Should we name it "SsriDdd" or simply "Ddd"? I prefer the latter one.

On the other hand, we do need to deal with "legacy UDTs", and it will be nice if we can reuse code between "legacy UDTs" and "(SSRI) UDTs". So I'm good with names Udt => UdtLegacy and UdtSsri => Udt and having everything the same as you mentioned.

@Hanssen0
Copy link
Member

  1. CompleteX methods are excessively difficult to understand due to the use of closures in parameters. The ship already sailed for ccc.Transaction CompleteX methods, but here we can still make it better.

Basic properties of interest relating to UDT cells are:

  • balance (UDT value)
  • capacity
  • count (how many cells are we adding?)

Instead of function closures, we can use a generator and we hard-code these properties as things of interest when adding cells, it simplifies everything. Any code that needs a closure to make decisions based on other computed cell properties, may not be able to fit in the closure approach anyway.

I agree that some of them are difficult to understand, like complete and completeInputs, because of the closure. However, they are intended to be complex for maximum flexibility.

The signature of completeInputs is based on the Array.prototype.reduce() with some extra args. It's basically someCellsOnChain.reduce(...), nothing more, nothing less. And as you can see, it's the base of all other completeInputs* methods, which significantly helps us to reduce duplicate code.

Another one, the complete method. I think we can all agree that handling fee in the UTXO model is a nightmare, and the complete method should be able to save us from this disaster. We already see different change strategies, e.g., adding new cells, reusing existing cells, or finding cells with someone's lock. To avoid facing our nightmare again and again, it's necessary for complete to be flexible, that's why the change param is a closure.

@Hanssen0
Copy link
Member

Hanssen0 commented Aug 14, 2025

  1. At the beginning there was not enough docs, we agreed on the issue and fixed it. Now we have so much TypeDoc that makes the code difficult to follow the code logic across methods. Let's trim it down a bit.

Agree. It's better to have more documents than fewer, but not too many. Let's preview the apiccc's final effect and trim down any overly obvious documents.

But I think this is a less serious problem. We want future developers to learn how to interact with those APIs without reading the code, so a clear document is more critical than code without a lengthy document that interrupts our reading.

@Hanssen0
Copy link
Member

Thank you for all your suggestions! I totally understand that we all want to make things the best we can. However, both the existing #228 and our planned approach are too ambitious to complete in the short term. This dramatically increases the reviewing cost and also the chance of introducing new issues.

I suggest that in this PR, we only address the remaining issues in #228 (Sooo sorry I really don't have enough time recently to finish them properly). After we all agree on a working solution, even if it's not perfect, we can merge it first and then create new PRs to improve it further.

}
async *find(
client: ccc.Client,
locks: ccc.Script[],
Copy link
Member

Choose a reason for hiding this comment

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

I'll always prefer Signer. While it might not be the best name for all cases (Like Watcher or sth might be better than SignerReadonly), it does reduce the aspects that newcomers have to learn. Besides, the utilities implemented in Signer also help us to avoid repeating ourselves.

We should use something like SignerCkbScriptListReadonly instead of Script[] everywhere, for special cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Except Signer does not currently offer any form of findCellsOnChain, so for now it's not a drop in replacement.

Let's say Signer was to offer findCellsOnChain and you also implemented SignerCkbScriptListReadonly, then we could easily change the code to use those newly defined utilities.

Copy link
Member

Choose a reason for hiding this comment

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

We can have them.

// eslint-disable-next-line @typescript-eslint/require-await
async infoFrom(
_client: ccc.Client,
initialInfo = {
Copy link
Member

Choose a reason for hiding this comment

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

I personally dislike this design. My principle is that we should always obtain information from the original Transaction (the original one defined by CKB, not CCC's) if possible. An example is the spore package, where all cobuild things are hidden behind the Transaction.witnesses.

Yes, extra information does make implementation simpler, and it will likely improve performance. However, the increased complexity is too painful. It's a trade-off between performance/simplicity, and usability in most cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Hanssen0 where in the original PR was mentioned that CoBuild is a target?

we should always obtain information from the original Transaction

Let's break down what it entails:

  • inputs: the client is exactly there as initial parameter for obtaining information about the original Transaction, like the inclusion block and Header.
  • outputs: we need to update the parameter signature.
    ...cells: {
      cellOutput: ccc.CellOutput;
      outputData: ccc.Hex;
      outPoint?: ccc.OutPoint;
    }[]

becomes:

    ...cells: ccc.Cell[] | {tx : ccc.Transaction, index: number}[]

Not really an issue as you can see.

Copy link
Member

Choose a reason for hiding this comment

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

Let me clarify: the Info is an extra thing that devs must maintain. When using the API, invokers must keep the info along with the tx, even if we can get all we need from the tx. It makes things complex.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Underlying issue is that CCC doesn't offer a good way to attach additional information to entities like Transaction, Cell and Script... As you know I have been struggling with this since the beginning due to the iCKB headers.

I recently switched to client+cache for the additional information and I was hoping that it would be the end of it.

The CoBuild info would be the object that later on gets serialized into the message?

We can just use a global CCC WeakMap to add this extra info, you just need to modify clone and copy to persist this information across copies

Copy link
Member

Choose a reason for hiding this comment

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

export async function prepareSporeTransaction(
signer: ccc.Signer,
txLike: ccc.TransactionLike,
actions: mol.EncodableType<typeof ActionVec>,
): Promise<ccc.Transaction> {
let tx = ccc.Transaction.from(txLike);
if (actions.length === 0) {
return signer.prepareTransaction(tx);
}
const existedActions = extractCobuildActionsFromTx(tx);
tx = await signer.prepareTransaction(tx);
injectCobuild(tx, [existedActions, actions].flat());
return tx;
}

This is the cobuild approach - it's some additional information that stays with the transaction. We have two ways to do this:

  1. Let devs maintain the actions array manually. Every new action should be added to the array. After preparing all actions and before completing the transaction, developers should inject/serialize the actions and then fill them back into the tx.witnesses. This is how the old Spore SDK handles cobuild actions.
  2. Hide all cobuild details. Every time our devs perform a new action on a tx, we parse the information from tx.witnesses, add the action, serialize, and add back to the transaction. This is how we do it now.

The problem here is different from headers caching: we have all we need in the tx; We can parse the cobuild info from tx.witnesses; We can parse the UDT info from tx.inputs / tx.outputs. If we can avoid bothering devs to handle these, we should.

Copy link
Member

Choose a reason for hiding this comment

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

Besides letting devs focus their mind on a simple tx, this also gives extra compatibility across SDK/ABI/API. Imagine that we're dealing with two devices cooperating to build a transaction. If we have additional information attached to the transaction, we will need to design the ABI carefully to carry it. Alternatively, we need to "pack" this information into the transaction and then "unpack" it on the other end.

Both ways are annoying & complex & leaking abstraction. That's why I insist that it's necessary to avoid attaching information to the transaction.

Copy link
Member

Choose a reason for hiding this comment

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

https://github.com/ckb-js/lumos/blob/b1ded0809d7b7b09c3ceafdf9cdb74256232c311/packages/helpers/src/index.ts#L325-L335

Code from Lumos can be seen as another example: fixedEntries and signingEntries. This might work fine at the beginning, while we're still using private keys to sign txs. But soon, as apps need to interact with an increasing number of wallets, things quickly become unmaintainable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'd like to point out that we can add even here whatever we need for CoBuild as optional parameter/property, not really an issue

if (res) {
resTx = res.map((res) => ccc.Transaction.fromBytes(res));
}
async updateOutput(
Copy link
Member

Choose a reason for hiding this comment

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

Definitely practical method for updating an output's balance!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you have any particular issue in mind?

Copy link
Member

Choose a reason for hiding this comment

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

No. I don't think this part has any issues. Just want you to know that I think it's a good idea to have this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was there mostly for passing your review. Seems it wasn't enough 🤣

My usual stance would be: do not update an output cell once you already added to the tx, cause you don't know for sure which other cells has been added to the Transaction that depends on the value of the cell that we are trying to modify.

@phroi
Copy link
Contributor Author

phroi commented Aug 14, 2025

The issues you mentioned are a minor code change and we can address them. Any other issue in the code?

@Hanssen0
Copy link
Member

The issues you mentioned are a minor code change, and we can address them. Any other issue in the code?

I'm unsure about what you mean by in the code. For me, this currently seems neither completed nor compatible with the original API. I can only provide some suggestions about the basic design.

Do you think we should dive deeper into any particular part?

@phroi
Copy link
Contributor Author

phroi commented Aug 15, 2025

I can see that this PR has zero chances of being merged.

Let's get back to your original PR

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.

[FR] Merge iCKB Utils UDTHandler features into CCC's Udt

2 participants