feat(coin): new coin package#415
Conversation
Hanssen0
commented
Jun 30, 2026
- I have read the Contributing Guidelines
🦋 Changeset detectedLatest commit: 1b32769 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 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 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 liveccc 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. |
There was a problem hiding this comment.
Pull request overview
This PR introduces a new @ckb-ccc/coin workspace package that provides a generic fungible-token (Coin) helper built on @ckb-ccc/core, and wires it into the monorepo’s testing/docs tooling and the @ckb-ccc/shell re-export surface.
Changes:
- Added new
packages/coinpackage (Coin implementation, tests, build/lint/test/tooling config, docs). - Updated root Vitest + TypeDoc configs to include
packages/coin. - Updated
@ckb-ccc/shellto depend on and re-export@ckb-ccc/coin; updated lockfile accordingly.
Reviewed changes
Copilot reviewed 21 out of 23 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.mts | Adds packages/coin to the monorepo Vitest projects list. |
| typedoc.config.mjs | Adds packages/coin to TypeDoc workspace entry points. |
| pnpm-lock.yaml | Adds the packages/coin importer and updates resolved dependency snapshots. |
| packages/shell/src/barrel.ts | Re-exports the coin namespace from @ckb-ccc/coin. |
| packages/shell/package.json | Adds @ckb-ccc/coin as a workspace dependency. |
| packages/coin/vitest.config.ts | Adds package-local Vitest configuration for tests and coverage. |
| packages/coin/typedoc.json | Adds package-local TypeDoc configuration for API generation. |
| packages/coin/tsdown.config.mts | Adds tsdown build configuration for ESM+CJS outputs and copy-basedirs. |
| packages/coin/tsconfig.json | Adds TypeScript build configuration for the coin package. |
| packages/coin/src/index.ts | Exposes barrel exports and a coin namespace export. |
| packages/coin/src/coin/index.ts | Implements the Coin class (balance/info helpers and transaction completion helpers). |
| packages/coin/src/coin/index.test.ts | Adds Vitest coverage for Coin behaviors (inputs completion, change handling, balance/info helpers). |
| packages/coin/src/coin/error.ts | Adds ErrorCoinInsufficient error type for insufficient Coin balance conditions. |
| packages/coin/src/coin/coinInfo.ts | Adds CoinInfo aggregation helper used by the Coin implementation. |
| packages/coin/src/barrel.ts | Re-exports the coin module surface from src/coin. |
| packages/coin/README.md | Adds package documentation and usage examples. |
| packages/coin/prettier.config.cjs | Adds package-local Prettier configuration. |
| packages/coin/package.json | Defines the new @ckb-ccc/coin package metadata, exports, and scripts. |
| packages/coin/misc/basedirs/dist/package.json | Ensures emitted ESM dist is treated as "type": "module". |
| packages/coin/misc/basedirs/dist.commonjs/package.json | Ensures emitted CJS dist is treated as "type": "commonjs". |
| packages/coin/eslint.config.mjs | Adds package-local ESLint + typescript-eslint configuration. |
| packages/coin/.prettierignore | Adds package-local Prettier ignore rules. |
| packages/coin/.npmignore | Adds package-local npm publish ignore rules. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
9481017 to
982850a
Compare
0c73462 to
497132a
Compare
b32dcb5 to
7c07413
Compare
ecb7009 to
c7d8463
Compare
|
/canary |
|
❌ Canary version deployment failed. View workflow run |
|
/canary |
|
❌ Canary version deployment failed. View workflow run |
|
/canary |
|
🚀 Canary version published successfully! View workflow run The following packages have been published to npm:
|
|
/canary |
|
🚀 Canary version published successfully! View workflow run The following packages have been published to npm:
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis change introduces the ChangesCoin package
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Caller
participant Coin
participant Client
participant Transaction
participant CoBuild
Caller->>Coin: create transfer or complete transaction
Coin->>Client: query matching cells
Client-->>Coin: return Coin cells and cell dependencies
Coin->>Transaction: add inputs and change output
Coin->>CoBuild: record transfer, mint, or burn action
CoBuild-->>Coin: return action witness index
Coin-->>Caller: return completed transaction
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
packages/coin/vitest.config.tsParsing error: "parserOptions.project" has been provided for Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/coin/README.md`:
- Around line 123-128: Update the “Change to a specific address” README example
so it is self-contained by defining tx, or explicitly label the snippet as
continuing from the preceding Send example and add that clarification in the
code block. Preserve the existing signer.getRecommendedAddressObj and
coin.completeChangeToLock calls.
In `@packages/coin/src/coin/coin.test.ts`:
- Around line 1538-1555: Update the test around completeInputsByAmount so it
passes the negative amountTweak value of -100 when invoking the method. Preserve
the existing transaction setup and addedCount expectation, ensuring the test
specifically verifies that the negative tweak cancels the output requirement.
In `@packages/coin/src/coin/coinInfo.ts`:
- Around line 49-52: Update CoinInfo.from so that when infoLike is already a
CoinInfo, it returns infoLike.clone() rather than the original reference.
Preserve the existing conversion behavior for other CoinInfoLike inputs and
maintain the documented guarantee that from always returns a new instance.
In `@packages/coin/src/xUdt/args.ts`:
- Around line 42-60: Update CoinXUdtArgsCodec and CoinXUdtArgs so xUDT extension
payloads are preserved during decode and re-encode, modeling extensionData
explicitly if supported; otherwise validate flags and encoded lengths and reject
unsupported extension data instead of silently emitting only the owner hash and
four flag bytes.
In `@packages/coin/src/xUdt/coinXUdt.ts`:
- Around line 99-115: Update the CoinXUdt constructor’s argument handling around
CoinXUdtArgs.fromBytes so extended bytes from options.script.args are preserved
when rebuilding the script, rather than always replacing them with
args.toBytes(). Keep canonical serialization for options.xUdtArgs while
retaining the original script argument bytes for the script-derived path, and
add a regression test using the extended arguments covered in xUdt.test.ts.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b179b27b-6b93-41c2-8a16-7fa1df39ce82
⛔ Files ignored due to path filters (2)
packages/coin/misc/basedirs/dist/package.jsonis excluded by!**/dist/**pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (28)
.changeset/early-oranges-lick.mdpackages/coin/.npmignorepackages/coin/.prettierignorepackages/coin/README.mdpackages/coin/eslint.config.mjspackages/coin/misc/basedirs/dist.commonjs/package.jsonpackages/coin/package.jsonpackages/coin/prettier.config.cjspackages/coin/src/barrel.tspackages/coin/src/coBuild.tspackages/coin/src/coin/coin.test.tspackages/coin/src/coin/coin.tspackages/coin/src/coin/coinInfo.tspackages/coin/src/coin/error.tspackages/coin/src/coin/index.tspackages/coin/src/index.tspackages/coin/src/xUdt/args.tspackages/coin/src/xUdt/coinXUdt.tspackages/coin/src/xUdt/index.tspackages/coin/src/xUdt/xUdt.test.tspackages/coin/tsconfig.jsonpackages/coin/tsdown.config.mtspackages/coin/typedoc.jsonpackages/coin/vitest.config.tspackages/shell/package.jsonpackages/shell/src/barrel.tstypedoc.config.mjsvitest.config.mts
| ### Change to a specific address | ||
|
|
||
| ```ts | ||
| const { script: changeLock } = await signer.getRecommendedAddressObj(); | ||
| const { tx: completedTx } = await coin.completeChangeToLock(tx, changeLock); | ||
| ``` |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the change-address example self-contained or explicitly mark it as a continuation.
This snippet uses tx without defining it in the same example. Readers who copy only this section will encounter an undefined identifier; either include the transaction construction or label the section as continuing from the previous example.
Suggested clarification
-### Change to a specific address
+### Change to a specific address (using the `tx` from the Send example above)
```ts
+// `tx` is created in the Send example above.
const { script: changeLock } = await signer.getRecommendedAddressObj();
const { tx: completedTx } = await coin.completeChangeToLock(tx, changeLock);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ### Change to a specific address | |
| ```ts | |
| const { script: changeLock } = await signer.getRecommendedAddressObj(); | |
| const { tx: completedTx } = await coin.completeChangeToLock(tx, changeLock); | |
| ``` | |
| ### Change to a specific address (using the `tx` from the Send example above) | |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/coin/README.md` around lines 123 - 128, Update the “Change to a
specific address” README example so it is self-contained by defining tx, or
explicitly label the snippet as continuing from the preceding Send example and
add that clarification in the code block. Preserve the existing
signer.getRecommendedAddressObj and coin.completeChangeToLock calls.
| it("should add no inputs when amountTweak exactly cancels the output requirement", async () => { | ||
| // Output needs 100, but amountTweak is -100 (negative tweak zeroing requirement) | ||
| const tx = ccc.Transaction.from({ | ||
| inputs: [ | ||
| { | ||
| previousOutput: { | ||
| txHash: `0x${"c".repeat(63)}0`, | ||
| index: 0, | ||
| }, | ||
| }, | ||
| ], | ||
| outputs: [{ lock, type }], | ||
| outputsData: [ccc.numLeToBytes(100, 16)], | ||
| }); | ||
|
|
||
| // Already have 1 input (amount 100) matching output exactly → no more needed | ||
| const { addedCount } = await coin.completeInputsByAmount(tx); | ||
| expect(addedCount).toBe(0); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Actually exercise the negative amountTweak.
The existing input already balances the output, and the call omits -100, so this test passes without covering its stated edge case.
Proposed fix
const tx = ccc.Transaction.from({
- inputs: [
- {
- previousOutput: {
- txHash: `0x${"c".repeat(63)}0`,
- index: 0,
- },
- },
- ],
outputs: [{ lock, type }],
outputsData: [ccc.numLeToBytes(100, 16)],
});
-const { addedCount } = await coin.completeInputsByAmount(tx);
+const { addedCount } = await coin.completeInputsByAmount(tx, -100);
expect(addedCount).toBe(0);
+expect(tx.inputs).toHaveLength(0);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| it("should add no inputs when amountTweak exactly cancels the output requirement", async () => { | |
| // Output needs 100, but amountTweak is -100 (negative tweak zeroing requirement) | |
| const tx = ccc.Transaction.from({ | |
| inputs: [ | |
| { | |
| previousOutput: { | |
| txHash: `0x${"c".repeat(63)}0`, | |
| index: 0, | |
| }, | |
| }, | |
| ], | |
| outputs: [{ lock, type }], | |
| outputsData: [ccc.numLeToBytes(100, 16)], | |
| }); | |
| // Already have 1 input (amount 100) matching output exactly → no more needed | |
| const { addedCount } = await coin.completeInputsByAmount(tx); | |
| expect(addedCount).toBe(0); | |
| it("should add no inputs when amountTweak exactly cancels the output requirement", async () => { | |
| // Output needs 100, but amountTweak is -100 (negative tweak zeroing requirement) | |
| const tx = ccc.Transaction.from({ | |
| outputs: [{ lock, type }], | |
| outputsData: [ccc.numLeToBytes(100, 16)], | |
| }); | |
| // Already have 1 input (amount 100) matching output exactly → no more needed | |
| const { addedCount } = await coin.completeInputsByAmount(tx, -100); | |
| expect(addedCount).toBe(0); | |
| expect(tx.inputs).toHaveLength(0); |
🧰 Tools
🪛 ESLint
[error] 1540-1551: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 1540-1540: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 1549-1549: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 1549-1549: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 1550-1550: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/coin/src/coin/coin.test.ts` around lines 1538 - 1555, Update the
test around completeInputsByAmount so it passes the negative amountTweak value
of -100 when invoking the method. Preserve the existing transaction setup and
addedCount expectation, ensuring the test specifically verifies that the
negative tweak cancels the output requirement.
| static from(infoLike?: CoinInfoLike) { | ||
| if (infoLike instanceof CoinInfo) { | ||
| return infoLike; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Avoid aliasing the input from CoinInfo.from.
Line 50 returns an existing CoinInfo by reference, despite the documentation promising a new instance. CoinInfo.from(existing).addAssign(...) can therefore mutate the caller’s object. Return infoLike.clone() or explicitly document and test the identity-preserving behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/coin/src/coin/coinInfo.ts` around lines 49 - 52, Update
CoinInfo.from so that when infoLike is already a CoinInfo, it returns
infoLike.clone() rather than the original reference. Preserve the existing
conversion behavior for other CoinInfoLike inputs and maintain the documented
guarantee that from always returns a new instance.
| /** | ||
| * Optional xUDT flags encoded as a little-endian uint32. | ||
| * | ||
| * Defaults to `0`. Use {@link CoinXUdtFlags} for the standard owner-mode | ||
| * flags. Lower bits are accepted and preserved without local interpretation. | ||
| */ | ||
| flags?: number | null; | ||
| }; | ||
|
|
||
| const CoinXUdtArgsCodec = ccc.Codec.from({ | ||
| encode(value) { | ||
| const args = CoinXUdtArgs.from(value); | ||
| const ownerScriptHash = ccc.bytesFrom(args.ownerScriptHash); | ||
|
|
||
| if (args.flags === 0) { | ||
| return ownerScriptHash; | ||
| } | ||
|
|
||
| return ccc.bytesConcat(ownerScriptHash, ccc.numLeToBytes(args.flags, 4)); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Preserve xUDT extension data or reject unsupported flags. CoinXUdtArgsCodec only round-trips the owner hash and 4 flag bytes, so xUDT args that need extension payloads will be changed on re-encode. Either model extensionData explicitly or fail on unsupported flags/lengths.
🧰 Tools
🪛 ESLint
[error] 51-51: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 53-53: Unsafe argument of type any assigned to a parameter of type CoinXUdtArgsLike.
(@typescript-eslint/no-unsafe-argument)
[error] 54-54: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 54-54: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 57-57: Unsafe return of a value of type error.
(@typescript-eslint/no-unsafe-return)
[error] 60-60: Unsafe return of a value of type error.
(@typescript-eslint/no-unsafe-return)
[error] 60-60: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 60-60: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/coin/src/xUdt/args.ts` around lines 42 - 60, Update
CoinXUdtArgsCodec and CoinXUdtArgs so xUDT extension payloads are preserved
during decode and re-encode, modeling extensionData explicitly if supported;
otherwise validate flags and encoded lengths and reject unsupported extension
data instead of silently emitting only the owner hash and four flag bytes.
| let args; | ||
| if (options.xUdtArgs != null) { | ||
| args = CoinXUdtArgs.from(options.xUdtArgs); | ||
| } else if (options.script?.args != null) { | ||
| args = CoinXUdtArgs.fromBytes(options.script.args); | ||
| } else { | ||
| throw new Error( | ||
| "Either xUdtArgs or script.args must be provided for CoinXUdt", | ||
| ); | ||
| } | ||
|
|
||
| super({ | ||
| ...options, | ||
| script: { | ||
| ...(options.script ?? {}), | ||
| args: args.toBytes(), | ||
| }, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Preserve existing script.args extension bytes.
fromBytes accepts extended args, but args.toBytes() strips those bytes. This changes the type script identity, causing balance discovery and transactions to target a different xUDT.
Proposed fix
constructor(options: CoinXUdtOptions) {
let args;
+ let scriptArgs;
if (options.xUdtArgs != null) {
args = CoinXUdtArgs.from(options.xUdtArgs);
+ scriptArgs = args.toBytes();
} else if (options.script?.args != null) {
args = CoinXUdtArgs.fromBytes(options.script.args);
+ scriptArgs = options.script.args;
} else {
throw new Error(
"Either xUdtArgs or script.args must be provided for CoinXUdt",
);
}
super({
...options,
script: {
...(options.script ?? {}),
- args: args.toBytes(),
+ args: scriptArgs,
},Add a constructor regression test using the extended args already covered in packages/coin/src/xUdt/xUdt.test.ts Lines 63-68.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let args; | |
| if (options.xUdtArgs != null) { | |
| args = CoinXUdtArgs.from(options.xUdtArgs); | |
| } else if (options.script?.args != null) { | |
| args = CoinXUdtArgs.fromBytes(options.script.args); | |
| } else { | |
| throw new Error( | |
| "Either xUdtArgs or script.args must be provided for CoinXUdt", | |
| ); | |
| } | |
| super({ | |
| ...options, | |
| script: { | |
| ...(options.script ?? {}), | |
| args: args.toBytes(), | |
| }, | |
| let args; | |
| let scriptArgs; | |
| if (options.xUdtArgs != null) { | |
| args = CoinXUdtArgs.from(options.xUdtArgs); | |
| scriptArgs = args.toBytes(); | |
| } else if (options.script?.args != null) { | |
| args = CoinXUdtArgs.fromBytes(options.script.args); | |
| scriptArgs = options.script.args; | |
| } else { | |
| throw new Error( | |
| "Either xUdtArgs or script.args must be provided for CoinXUdt", | |
| ); | |
| } | |
| super({ | |
| ...options, | |
| script: { | |
| ...(options.script ?? {}), | |
| args: scriptArgs, | |
| }, |
🧰 Tools
🪛 ESLint
[error] 103-103: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 103-103: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
[error] 112-115: Unsafe assignment of an any value.
(@typescript-eslint/no-unsafe-assignment)
[error] 114-114: Unsafe assignment of an error typed value.
(@typescript-eslint/no-unsafe-assignment)
[error] 114-114: Unsafe call of a type that could not be resolved.
(@typescript-eslint/no-unsafe-call)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/coin/src/xUdt/coinXUdt.ts` around lines 99 - 115, Update the
CoinXUdt constructor’s argument handling around CoinXUdtArgs.fromBytes so
extended bytes from options.script.args are preserved when rebuilding the
script, rather than always replacing them with args.toBytes(). Keep canonical
serialization for options.xUdtArgs while retaining the original script argument
bytes for the script-derived path, and add a regression test using the extended
arguments covered in xUdt.test.ts.
