Skip to content
Open
Show file tree
Hide file tree
Changes from 12 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
6 changes: 6 additions & 0 deletions .changeset/mean-yaks-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ckb-ccc/spore": minor
---

Migrate dob-render-sdk directly into spore module

3 changes: 2 additions & 1 deletion packages/ccc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@
"@ckb-ccc/shell": "workspace:*",
"@ckb-ccc/uni-sat": "workspace:*",
"@ckb-ccc/utxo-global": "workspace:*",
"@ckb-ccc/xverse": "workspace:*"
"@ckb-ccc/xverse": "workspace:*",
"@ckb-ccc/dob-render": "workspace:*"
},
"packageManager": "pnpm@10.8.1"
}
1 change: 1 addition & 0 deletions packages/ccc/src/barrel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "@ckb-ccc/dob-render";
export * from "@ckb-ccc/eip6963";
export * from "@ckb-ccc/joy-id";
export * from "@ckb-ccc/nip07";
Expand Down
42 changes: 42 additions & 0 deletions packages/dob-render/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# @ckb-ccc/render
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 package name in the README title is @ckb-ccc/render, but the actual package name is @ckb-ccc/dob-render. This should be corrected to avoid confusion.

Suggested change
# @ckb-ccc/render
# @ckb-ccc/dob-render

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 package name in the main heading is @ckb-ccc/render, but the actual package name is @ckb-ccc/dob-render. This should be corrected for consistency and to avoid confusion for developers.

Suggested change
# @ckb-ccc/render
# @ckb-ccc/dob-render


CCC - CKBer's Codebase. Common Chains Connector's render SDK for DOB protocol.

This package provides rendering capabilities for DOB (Decentralized Object) protocol, allowing you to render DOB tokens as SVG images.

## Installation

```bash
npm install @ckb-ccc/dob-render
```

## Usage

```typescript
import {
renderByTokenKey,
renderByDobDecodeResponse,
} from "@ckb-ccc/dob-render";

// Render by token key
const svg = await renderByTokenKey("your-token-key");

// Render by DOB decode response
const svg = renderByDobDecodeResponse(renderOutput);
```

## API

### `renderByTokenKey(tokenKey: string, options?: RenderOptions)`

Renders a DOB token by its key.

### `renderByDobDecodeResponse(renderOutput: RenderOutput | string, props?: RenderProps)`
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 function signature for renderByDobDecodeResponse in the API documentation is incorrect. The renderOutput parameter is typed as RenderOutput | string, but the implementation only accepts RenderOutput. The | string part should be removed to accurately reflect the function's signature.

Suggested change
### `renderByDobDecodeResponse(renderOutput: RenderOutput | string, props?: RenderProps)`
### `renderByDobDecodeResponse(renderOutput: RenderOutput, props?: RenderProps)`


Renders a DOB token from a decoded response.

## Dependencies

- `satori` - SVG to image conversion
- `svgson` - SVG parsing
- `axios` - HTTP client for API calls
62 changes: 62 additions & 0 deletions packages/dob-render/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// @ts-check

import eslint from "@eslint/js";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import tseslint from "typescript-eslint";

import { dirname } from "path";
import { fileURLToPath } from "url";

export default [
...tseslint.config({
files: ["**/*.ts"],
extends: [
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
],
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{
args: "all",
argsIgnorePattern: "^_",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^_",
destructuredArrayIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true,
},
],
"@typescript-eslint/unbound-method": ["error", { ignoreStatic: true }],
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/only-throw-error": [
"error",
{
allowThrowingAny: true,
allowThrowingUnknown: true,
allowRethrowing: true,
},
],
"@typescript-eslint/prefer-promise-reject-errors": [
"error",
{
allowThrowingAny: true,
allowThrowingUnknown: true,
},
],
"no-empty": "off",
"prefer-const": [
"error",
{ ignoreReadBeforeAssign: true, destructuring: "all" },
],
},
languageOptions: {
parserOptions: {
project: true,
tsconfigRootDir: dirname(fileURLToPath(import.meta.url)),
},
},
}),
eslintPluginPrettierRecommended,
];
58 changes: 58 additions & 0 deletions packages/dob-render/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "@ckb-ccc/dob-render",
"version": "1.0.1",
"description": "CCC - CKBer's Codebase. Common Chains Connector's render SDK for DOB protocol",
"author": "ashuralyk <ashuralyk@live.com>",
"license": "MIT",
"private": false,
"homepage": "https://github.com/ckb-devrel/ccc",
"repository": {
"type": "git",
"url": "git://github.com/ckb-devrel/ccc.git"
},
"sideEffects": false,
"main": "dist.commonjs/index.js",
"module": "dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist.commonjs/index.js",
"default": "./dist.commonjs/index.js"
},
"./barrel": {
"import": "./dist/barrel.js",
"require": "./dist.commonjs/barrel.js",
"default": "./dist.commonjs/barrel.js"
}
},
"scripts": {
"build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json",
"lint": "eslint ./src",
"format": "prettier --write . && eslint --fix ./src"
},
"devDependencies": {
"@eslint/js": "^9.34.0",
"@types/node": "^24.3.0",
"eslint": "^9.34.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"prettier": "^3.6.2",
"prettier-plugin-organize-imports": "^4.2.0",
"rimraf": "^6.0.1",
"typescript": "^5.9.2",
"typescript-eslint": "^8.41.0"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"@ckb-ccc/spore": "workspace:*",
"axios": "^1.11.0",
"satori": "^0.10.13",
"svgson": "^5.3.1"
},
"peerDependencies": {
"satori": "^0.10.13"
},
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

satori is listed as both a direct dependency and a peer dependency. This is generally not recommended. If it's a peer dependency, the consumer of this package is responsible for providing it, and it shouldn't be included as a direct dependency to avoid potential version conflicts. Please clarify the intention and remove it from either dependencies or peerDependencies.

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 satori package is listed as both a dependency and a peerDependency. A package should typically be one or the other. If users of @ckb-ccc/dob-render are expected to provide their own version of satori, it should only be a peerDependency and removed from dependencies. If it's a direct dependency required by the package, it should be removed from peerDependencies. Please clarify the intention and remove the redundant entry to avoid confusion and potential dependency issues.

"packageManager": "pnpm@10.8.1"
}
11 changes: 11 additions & 0 deletions packages/dob-render/prettier.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* @see https://prettier.io/docs/configuration
* @type {import("prettier").Config}
*/
const config = {
singleQuote: false,
trailingComma: "all",
plugins: [require.resolve("prettier-plugin-organize-imports")],
};

module.exports = config;
2 changes: 2 additions & 0 deletions packages/dob-render/src/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./renderDobDecode.js";
export * from "./renderToken.js";
35 changes: 35 additions & 0 deletions packages/dob-render/src/api/renderDobDecode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Key } from "../config/constants.js";
import { renderTextParamsParser } from "../core/parsers/textParamsParser.js";
import { traitsParser } from "../core/parsers/traitsParser.js";
import { renderDob1Svg } from "../core/renderers/dob1Render.js";
import { renderImageSvg } from "../core/renderers/imageRender.js";
import type { RenderProps } from "../core/renderers/textRender.js";
import { renderTextSvg } from "../core/renderers/textRender.js";
import type { RenderOutput } from "../types/external.js";

export function renderByDobDecodeResponse(
renderOutput: RenderOutput | string,
props?: Pick<RenderProps, "font"> & {
outputType?: "svg";
},
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 outputType property within the props object is defined but not used anywhere in the function. This appears to be dead code or an incomplete feature. It should be removed to avoid confusion, or if intended for future use, a // TODO comment should be added to clarify its purpose.

) {
let renderData: RenderOutput;
if (typeof renderOutput === "string") {
renderData = JSON.parse(renderOutput) as RenderOutput;
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 type assertion as RenderOutput is unsafe as it doesn't guarantee the parsed JSON matches the RenderOutput type. This could lead to runtime errors if the input string is malformed or has an unexpected structure. Consider using a validation library (like Zod) or a type guard function to safely parse and validate the data.

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 result of JSON.parse is cast to RenderOutput without any validation. If the renderOutput string is not a valid JSON or does not conform to the RenderOutput type, this will lead to runtime errors or unexpected behavior. It's highly recommended to add a type guard or a validation function to ensure the parsed object is of the correct type before casting.

} else {
renderData = renderOutput;
}

const { traits, indexVarRegister } = traitsParser(renderData);
for (const trait of traits) {
if (trait.name === String(Key.Type) && trait.value === "image") {
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 String() wrapper around Key.Type is redundant because TypeScript enum members with string initializers are already strings. You can compare trait.name directly with Key.Type. This improves code clarity and removes an unnecessary function call.

Suggested change
if (trait.name === String(Key.Type) && trait.value === "image") {
if (trait.name === Key.Type && trait.value === "image") {

return renderImageSvg(traits);
}
// TODO: multiple images
if (trait.name === String(Key.Image) && trait.value instanceof Promise) {
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 comment on line 22, the String() wrapper around Key.Image is redundant and can be removed for cleaner code.

Suggested change
if (trait.name === String(Key.Image) && trait.value instanceof Promise) {
if (trait.name === Key.Image && trait.value instanceof Promise) {

return renderDob1Svg(trait.value);
}
}
const renderOptions = renderTextParamsParser(traits, indexVarRegister);
return renderTextSvg({ ...renderOptions, font: props?.font });
}
18 changes: 18 additions & 0 deletions packages/dob-render/src/api/renderToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { dob } from "@ckb-ccc/spore";
import { config } from "../config.js";
import type { RenderProps } from "../core/renderers/textRender.js";
import { renderByDobDecodeResponse } from "./renderDobDecode.js";

export async function renderByTokenKey(
tokenKey: string,
options?: Pick<RenderProps, "font"> & {
outputType?: "svg";
},
) {
const renderOutput = await dob.decodeDobBySporeId(
tokenKey,
config.dobDecodeServerURL,
);

return renderByDobDecodeResponse(renderOutput, options);
}
5 changes: 5 additions & 0 deletions packages/dob-render/src/barrel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./api/index.js";
export * from "./config/index.js";
export * from "./core/index.js";
export * from "./types/index.js";
export * from "./utils/index.js";
101 changes: 101 additions & 0 deletions packages/dob-render/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
export type FileServerResult =
| string
| {
content: string;
content_type: string;
};

export type BtcFsResult = FileServerResult;
export type IpfsResult = FileServerResult;
export type CkbFsResult = FileServerResult;

export type BtcFsURI = `btcfs://${string}`;
export type IpfsURI = `ipfs://${string}`;
export type CkbFsURI = `ckbfs://${string}`;

export type QueryBtcFsFn = (uri: BtcFsURI) => Promise<BtcFsResult>;
export type QueryIpfsFn = (uri: IpfsURI) => Promise<IpfsResult>;
export type QueryCkbFsFn = (uri: CkbFsURI) => Promise<CkbFsResult>;
export type QueryUrlFn = (uri: string) => Promise<FileServerResult>;

export class Config {
private _dobDecodeServerURL = "https://dob-decoder.ckbccc.com";

private _queryBtcFsFn: QueryBtcFsFn = async (uri) => {
console.log("dob-render-sdk requiring", uri);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

console.log statements are present in the code (also on line 37). These should be removed or replaced with a proper logging mechanism for production code to avoid polluting the console output.

const response = await fetch(
`https://dob-decoder.ckbccc.com/restful/dob_extract_image?uri=${uri}&encode=base64`,
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 URL https://dob-decoder.ckbccc.com is hardcoded here, but it's already defined as a configurable property _dobDecodeServerURL. To improve maintainability and avoid inconsistencies, you should use this._dobDecodeServerURL instead.

Suggested change
`https://dob-decoder.ckbccc.com/restful/dob_extract_image?uri=${uri}&encode=base64`,
`${this._dobDecodeServerURL}/restful/dob_extract_image?uri=${uri}&encode=base64`,

);
const text = await response.text();
return {
content: text,
content_type: "",
};
Copy link
Contributor

Choose a reason for hiding this comment

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

high

This function has a couple of issues:

  1. The content_type is hardcoded to an empty string. It should be dynamically determined from the Content-Type header of the HTTP response.
  2. There's no error handling for the fetch call. If the request fails (e.g., network error, 404 status), it will result in an unhandled promise rejection.
Suggested change
const response = await fetch(
`https://dob-decoder.ckbccc.com/restful/dob_extract_image?uri=${uri}&encode=base64`,
);
const text = await response.text();
return {
content: text,
content_type: "",
};
const response = await fetch(
`https://dob-decoder.ckbccc.com/restful/dob_extract_image?uri=${uri}&encode=base64`,
);
if (!response.ok) {
throw new Error(`Failed to fetch from BtcFs: ${response.statusText}`);
}
const text = await response.text();
return {
content: text,
content_type: response.headers.get('content-type') || '',
};

};

private _queryUrlFn = async (url: string) => {
console.log("dob-render-sdk requiring", url);
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 use of console.log in library code is generally discouraged as it can pollute the console of the consuming application. It's better to use a dedicated, configurable logger for debugging purposes.

const response = await fetch(url);
const blob = await response.blob();
return new Promise<IpfsResult>((resolve, reject) => {
const reader = new FileReader();

reader.onload = function () {
const base64 = this.result as string;
resolve(base64);
};
reader.onerror = (error) => {
reject(new Error(`FileReader error: ${error.type}`));
};
reader.readAsDataURL(blob);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The FileReader API is only available in browsers and will cause this code to crash in a Node.js environment. To ensure this library is isomorphic (runs in both browser and Node.js), you should provide a Node.js-compatible implementation for fetching and base64-encoding the content.

    if (typeof window !== 'undefined' && typeof FileReader !== 'undefined') {
      // Browser environment
      return new Promise<IpfsResult>((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = function () {
          const dataUrl = this.result as string;
          resolve(dataUrl);
        };
        reader.onerror = (error) => {
          reject(new Error(`FileReader error: ${error.type}`));
        };
        reader.readAsDataURL(blob);
      });
    } else {
      // Node.js environment
      const buffer = await response.arrayBuffer();
      const base64 = Buffer.from(buffer).toString('base64');
      const mimeType = response.headers.get('content-type') || 'application/octet-stream';
      return `data:${mimeType};base64,${base64}`;
    }

Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The use of FileReader makes this function browser-specific and will cause it to fail in a Node.js environment. For a universal library, you should provide an environment-agnostic implementation. In Node.js, you can use Buffer.from(await blob.arrayBuffer()).toString('base64') to achieve the same result.

Copy link
Contributor

Choose a reason for hiding this comment

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

critical

The use of FileReader is a browser-specific API and will cause this code to fail in a Node.js environment. Since this package seems to be intended for both environments, you should provide a Node.js-compatible implementation for converting a blob (or stream) to a base64 string, for example by using Buffer.

};

private _queryIpfsFn = async (uri: IpfsURI) => {
const key = uri.substring("ipfs://".length);
const url = `https://ipfs.io/ipfs/${key}`;
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 IPFS gateway URL https://ipfs.io/ipfs/ is hardcoded. Public gateways can be unreliable or slow. It would be better to make this configurable, for instance by adding a setQueryIpfsGateway method to the Config class, to allow consumers to use their own or preferred IPFS gateway.

return this._queryUrlFn(url);
};

private _queryCkbFsFn: QueryCkbFsFn = async (_uri: CkbFsURI) => {
throw new Error("CkbFs is not supported");
};

get dobDecodeServerURL() {
return this._dobDecodeServerURL;
}

setDobDecodeServerURL(dobDecodeServerURL: string): void {
this._dobDecodeServerURL = dobDecodeServerURL;
}

setQueryBtcFsFn(fn: QueryBtcFsFn): void {
this._queryBtcFsFn = fn;
}

setQueryIpfsFn(fn: QueryIpfsFn): void {
this._queryIpfsFn = fn;
}

setQueryCkbFsFn(fn: QueryCkbFsFn): void {
this._queryCkbFsFn = fn;
}

get queryBtcFsFn(): QueryBtcFsFn {
return this._queryBtcFsFn;
}

get queryIpfsFn(): QueryIpfsFn {
return this._queryIpfsFn;
}

get queryUrlFn(): QueryUrlFn {
return this._queryUrlFn;
}

get queryCkbFsFn(): QueryCkbFsFn {
return this._queryCkbFsFn;
}
}

export const config = new Config();
12 changes: 12 additions & 0 deletions packages/dob-render/src/config/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export enum Key {
Bg = "prev.bg",
Type = "prev.type",
BgColor = "prev.bgcolor",
Prev = "prev",
Image = "IMAGE",
}

export const ARRAY_REG = /\(%(.*?)\):(\[.*?\])/;
export const ARRAY_INDEX_REG = /(\d+)<_>$/;
export const GLOBAL_TEMPLATE_REG = /^prev<(.*?)>/;
export const TEMPLATE_REG = /^(.*?)<(.*?)>/;
9 changes: 9 additions & 0 deletions packages/dob-render/src/config/fonts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import SpaceGroteskBoldBase64 from "../fonts/spaceGroteskBold.base64.js";
import TurretRoadBoldBase64 from "../fonts/turretRoadBold.base64.js";
import TurretRoadMediumBase64 from "../fonts/turretRoadMedium.base64.js";

export const FONTS = {
SpaceGroteskBold: SpaceGroteskBoldBase64,
TurretRoadBold: TurretRoadBoldBase64,
TurretRoadMedium: TurretRoadMediumBase64,
} as const;
3 changes: 3 additions & 0 deletions packages/dob-render/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { config } from "../config.js";
export * from "./constants.js";
export * from "./fonts.js";
2 changes: 2 additions & 0 deletions packages/dob-render/src/core/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./parsers/index.js";
export * from "./renderers/index.js";
Loading