Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/examples/src/registerUdtMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ccc } from "@ckb-ccc/ccc";
import { render, signer } from "@ckb-ccc/playground";

// Prepare the UDT trait
const type = ccc.Script.from({
codeHash:
"0x8b887e59f396f99302996ee8911b31f73fb2e2be4d9cade3104f017a871b8ed3",
hashType: "type",
// Equal to the TypeId args of metadata cell, UDT would use it to search metadata cell
// for extracting the metadata. It can be empty if no metadata cell deployed on the chain.
args: "0x1e370b8965e12faf572a0f7ca7bf585027404ee23c32a82ef049965d5ebb8ff6",
});

const code = ccc.OutPoint.from({
// SSRI-UDT script deployment tx hash on Testnet
txHash: "0x1fecfac56696b38d76304f9e2dc1db39406679f3a6e517d5ed16bddbd8fdd7ab",
index: 0,
});

const executor = new ccc.ssri.ExecutorJsonRpc("http://localhost:9090"); // Linking to your native SSRI-Server
const udt = new ccc.udt.UdtRegister(code, type, {
executor,
});

// Register the UDT with metadata
const { tx: registerTx, tokenHash: metadataTypeIdArgs } = await udt.register(
signer,
{
name: "SSRI UDT",
symbol: "WSS",
decimals: 8,
icon: "",
},
);

// Get the metadata TypeId args, then you can use it to mint UDT tokens
console.log("metadataTypeIdArgs =", metadataTypeIdArgs);

const tx = registerTx.res;
await render(tx);

// Complete missing parts: Pay fee
await tx.completeFeeBy(signer);
await render(tx);

const txHash = await signer.sendTransaction(tx);
console.log("tx hash =", txHash);
1 change: 1 addition & 0 deletions packages/udt/src/barrel.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./udt/index.js";
export * from "./udtPausable/index.js";
export * from "./udtRegister/index.js";
130 changes: 130 additions & 0 deletions packages/udt/src/udtRegister/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { ccc } from "@ckb-ccc/core";
import { ssri } from "@ckb-ccc/ssri";
import { Udt, UdtConfigLike } from "../udt/index.js";

/**
* The basic metadata of a UDT token.
*
* @example
* ```typescript
* const metadataMolecule = UdtMetadata.encode({
* name: "My UDT",
* symbol: "MYUDT",
* decimals: 8,
* icon: "https://example.com/icon.png",
* });
* ```
*
* @public
* @category UDT
*/
export const UdtMetadata = ccc.mol.table({
name: ccc.mol.String,
symbol: ccc.mol.String,
decimals: ccc.mol.Uint8,
icon: ccc.mol.String,
});

/**
* Represents a UDT (User Defined Token) with separated SSRI metadata functionality.
* @extends {Udt} This must be a SSRI UDT that does not fallback to xUDT.
* @public
*/
export class UdtRegister extends Udt {
constructor(
code: ccc.OutPointLike,
script: ccc.ScriptLike,
config: UdtConfigLike & { executor: ssri.Executor },
) {
super(code, script, config);
}

/**
* Registers (creates) a new UDT with on-chain metadata.
* This method creates a new unique UDT instance (usually with a TypeId pattern),
* assigns on-chain metadata (name, symbol, decimals, icon), and returns a transaction
* to instantiate the new token. Often used by the deployer/owner.
*
* @param signer - The signer (owner) who will initialize and own the new UDT
* @param metadata - Object containing UDT metadata (name, symbol, decimals, icon)
* @param tx - Optional existing transaction to build upon
* @returns Promise resolving to `{ tx, tokenHash }`, where `tx` is the deployment transaction,
* and `tokenHash` is the computed TypeId/Token hash of the new UDT
*
* @example
* ```typescript
* const udt = new Udt(codeOutPoint, scriptConfig);
* const { tx, tokenHash } = await udt.register(
Comment on lines +56 to +57
Copy link
Contributor

Choose a reason for hiding this comment

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

high

The example is incorrect. It instantiates Udt instead of UdtRegister, but the register method is part of the UdtRegister class. Additionally, the UdtRegister constructor requires an executor in its configuration, which is missing from the example.

Suggested change
* const udt = new Udt(codeOutPoint, scriptConfig);
* const { tx, tokenHash } = await udt.register(
* const udtRegister = new UdtRegister(codeOutPoint, scriptConfig, { executor });
* const { tx, tokenHash } = await udtRegister.register(

* signer,
* { name: "My UDT", symbol: "MYT", decimals: 8, icon: "https://..." }
* );
* // Send tx.res or complete tx.res and send the transaction
* ```
*
* @remarks
* - Uses SSRI executor if available for advanced/SSRI-compliant registration.
* - Falls back to legacy registration (TypeId pattern) if no executor is present.
* - The token hash can be used as the args for the UDT type script.
*/
async register(
signer: ccc.Signer,
metadata: {
name: string;
symbol: string;
decimals: number;
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 decimals property is encoded as a Uint8, meaning its value must be an integer between 0 and 255. The current type number is too broad and doesn't communicate this important constraint. Adding a JSDoc comment will help developers use this method correctly and prevent potential runtime errors.

Suggested change
decimals: number;
/** Must be an integer between 0 and 255. */
decimals: number;

icon: string;
},
tx?: ccc.TransactionLike | null,
): Promise<{
tx: ssri.ExecutorResponse<ccc.Transaction>;
tokenHash: ccc.Hex;
}> {
const owner = await signer.getRecommendedAddressObj();
const register = ccc.Transaction.from(tx ?? {});
if (register.inputs.length === 0) {
await register.completeInputsAtLeastOne(signer); // For `TypeId` calcuclation
}
const tokenHash = ccc.hashTypeId(
register.inputs[0],
register.outputs.length,
);

let resTx;
if (this.executor) {
const res = await this.executor.runScriptTry(
this.code,
"SSRIUDT.create",
[
register.toBytes(),
ccc.Script.from(owner.script).toBytes(),
UdtMetadata.encode(metadata),
],
);
if (res) {
resTx = res.map((res) => ccc.Transaction.fromBytes(res));
}
}
Comment on lines +93 to +106
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 UdtRegister constructor requires an executor to be present in the config, making this.executor always defined within this class. The if (this.executor) check is therefore redundant. You can simplify the code by removing this conditional check. The SSRI path will always be attempted, and the fallback logic will still be correctly triggered if runScriptTry returns undefined.

    const res = await this.executor.runScriptTry(
      this.code,
      "SSRIUDT.create",
      [
        register.toBytes(),
        ccc.Script.from(owner.script).toBytes(),
        UdtMetadata.encode(metadata),
      ],
    );
    if (res) {
      resTx = res.map((res) => ccc.Transaction.fromBytes(res));
    }


// Fallback logic
if (!resTx) {
register.addOutput(
{
lock: owner.script,
type: {
codeHash:
"00000000000000000000000000000000000000000000000000545950455f4944",
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 codeHash for the Type ID script is hardcoded here. This "magic string" reduces readability and maintainability. It's better to define this as a constant with a descriptive name (e.g., TYPE_ID_CODE_HASH) in a shared location and reference it here.

hashType: "type",
args: tokenHash,
},
},
UdtMetadata.encode(metadata),
);
resTx = ssri.ExecutorResponse.new(register);
}

return {
tx: resTx.map((tx) => this.addCellDeps(tx)),
tokenHash,
};
}
}