diff --git a/packages/ccc/package.json b/packages/ccc/package.json index 6d47bd3e..dfe643d6 100644 --- a/packages/ccc/package.json +++ b/packages/ccc/package.json @@ -57,6 +57,7 @@ "@ckb-ccc/nip07": "workspace:*", "@ckb-ccc/okx": "workspace:*", "@ckb-ccc/uni-sat": "workspace:*", - "@ckb-ccc/utxo-global": "workspace:*" + "@ckb-ccc/utxo-global": "workspace:*", + "@ckb-ccc/rei": "workspace:*" } } diff --git a/packages/ccc/src/assets/rei.svg.ts b/packages/ccc/src/assets/rei.svg.ts new file mode 100644 index 00000000..1d994b61 --- /dev/null +++ b/packages/ccc/src/assets/rei.svg.ts @@ -0,0 +1,5 @@ +import { encodeSvgToImgSrc } from "./utils.js"; + +export const REI_SVG = encodeSvgToImgSrc( + '', +); diff --git a/packages/ccc/src/barrel.ts b/packages/ccc/src/barrel.ts index f2c98541..f0ac8f42 100644 --- a/packages/ccc/src/barrel.ts +++ b/packages/ccc/src/barrel.ts @@ -3,6 +3,7 @@ export * from "@ckb-ccc/eip6963"; export * from "@ckb-ccc/joy-id"; export * from "@ckb-ccc/nip07"; export * from "@ckb-ccc/okx"; +export * from "@ckb-ccc/rei"; export * from "@ckb-ccc/uni-sat"; export * from "@ckb-ccc/utxo-global"; export * from "./signersController.js"; diff --git a/packages/ccc/src/signersController.ts b/packages/ccc/src/signersController.ts index a2031be5..ed4ac6e1 100644 --- a/packages/ccc/src/signersController.ts +++ b/packages/ccc/src/signersController.ts @@ -3,6 +3,7 @@ import { Eip6963 } from "@ckb-ccc/eip6963"; import { JoyId } from "@ckb-ccc/joy-id"; import { Nip07 } from "@ckb-ccc/nip07"; import { Okx } from "@ckb-ccc/okx"; +import { Rei } from "@ckb-ccc/rei"; import { UniSat } from "@ckb-ccc/uni-sat"; import { UtxoGlobal } from "@ckb-ccc/utxo-global"; import { ETH_SVG } from "./assets/eth.svg.js"; @@ -10,6 +11,7 @@ import { JOY_ID_SVG } from "./assets/joy-id.svg.js"; import { METAMASK_SVG } from "./assets/metamask.svg.js"; import { NOSTR_SVG } from "./assets/nostr.svg.js"; import { OKX_SVG } from "./assets/okx.svg.js"; +import { REI_SVG } from "./assets/rei.svg.js"; import { UNI_SAT_SVG } from "./assets/uni-sat.svg.js"; import { UTXO_GLOBAL_SVG } from "./assets/utxo-global.svg.js"; @@ -97,6 +99,15 @@ export class SignersController { configs, ); + await this.addSigners( + wallets, + "Rei Wallet", + REI_SVG, + Rei.getReiSigners(client), + onUpdate, + configs, + ); + await this.addSigners( wallets, "JoyID Passkey", diff --git a/packages/rei/.npmignore b/packages/rei/.npmignore new file mode 100644 index 00000000..0e812402 --- /dev/null +++ b/packages/rei/.npmignore @@ -0,0 +1,12 @@ +node_modules/ +misc/ + +tsconfig.json +tsconfig.*.json +eslint.config.mjs +.prettierrc +.prettierignore + +tsconfig.tsbuildinfo +tsconfig.*.tsbuildinfo +.github/ diff --git a/packages/rei/.prettierignore b/packages/rei/.prettierignore new file mode 100644 index 00000000..7d789074 --- /dev/null +++ b/packages/rei/.prettierignore @@ -0,0 +1,13 @@ +node_modules/ + +dist/ +dist.commoonjs/ + +.npmignore +.prettierrc +tsconfig.json +eslint.config.mjs +.prettierrc + +tsconfig.tsbuildinfo +.github/ diff --git a/packages/rei/.prettierrc b/packages/rei/.prettierrc new file mode 100644 index 00000000..6390af08 --- /dev/null +++ b/packages/rei/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": false, + "trailingComma": "all", + "plugins": ["prettier-plugin-organize-imports"] +} diff --git a/packages/rei/README.md b/packages/rei/README.md new file mode 100644 index 00000000..72da5e16 --- /dev/null +++ b/packages/rei/README.md @@ -0,0 +1,45 @@ +

+ + Logo + +

+ +

+ CCC's support for REI Wallet +

+ +

+ NPM Version + GitHub commit activity + GitHub last commit + GitHub deployments + Demo +

+ +

+ "CCC - CKBers' Codebase" is the next step of "Common Chains Connector". +
+ Empower yourself with CCC to discover the unlimited potential of CKB. +
+ Interoperate with wallets from different chain ecosystems. +
+ Fully enabling CKB's Turing completeness and cryptographic freedom power. +

+ +## Preview + +

+ + + +

+ +This project is still under active development, and we are looking forward to your feedback. [Try its demo now here](https://app.ckbccc.com/). It showcases how to use CCC for some basic scenarios in CKB. + +

+ Read more about CCC on our website or GitHub Repo. +

diff --git a/packages/rei/eslint.config.mjs b/packages/rei/eslint.config.mjs new file mode 100644 index 00000000..b42a690d --- /dev/null +++ b/packages/rei/eslint.config.mjs @@ -0,0 +1,29 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier"; + +import { fileURLToPath } from "url"; +import { dirname } from "path"; + +export default tseslint.config({ + files: ["./src/**/*.ts"], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ], + rules: { + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-redundant-type-constituents": "off", + "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], + "@typescript-eslint/unbound-method": ["error", { "ignoreStatic": true }], + }, + plugins: { prettier: eslintPluginPrettierRecommended }, + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: dirname(fileURLToPath(import.meta.url)), + }, + }, +}); diff --git a/packages/rei/misc/basedirs/dist.commonjs/package.json b/packages/rei/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/packages/rei/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/rei/misc/basedirs/dist/package.json b/packages/rei/misc/basedirs/dist/package.json new file mode 100644 index 00000000..aead43de --- /dev/null +++ b/packages/rei/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/rei/package.json b/packages/rei/package.json new file mode 100644 index 00000000..7016507e --- /dev/null +++ b/packages/rei/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ckb-ccc/rei", + "version": "0.0.12-alpha.0", + "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Rei", + "license": "MIT", + "private": false, + "homepage": "https://github.com/ckb-ecofund/ccc", + "repository": { + "type": "git", + "url": "git://github.com/ckb-ecofund/ccc.git" + }, + "main": "dist.commonjs/index.js", + "module": "dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "default": "./dist.commonjs/index.js" + }, + "./barrel": { + "import": "./dist/barrel.js", + "default": "./dist.commonjs/barrel.js" + }, + "./advanced": { + "import": "./dist/advanced.js", + "default": "./dist.commonjs/advanced.js" + }, + "./advancedBarrel": { + "import": "./dist/advancedBarrel.js", + "default": "./dist.commonjs/advancedBarrel.js" + } + }, + "scripts": { + "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json && copyfiles -u 2 misc/basedirs/**/* .", + "lint": "eslint", + "format": "prettier --write . && eslint --fix" + }, + "devDependencies": { + "@eslint/js": "^9.1.1", + "copyfiles": "^2.4.1", + "eslint": "^9.1.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "prettier-plugin-organize-imports": "^3.2.4", + "rimraf": "^5.0.5", + "typescript": "^5.4.5", + "typescript-eslint": "^7.7.0" + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@ckb-ccc/core": "workspace:*" + } +} diff --git a/packages/rei/src/advanced.ts b/packages/rei/src/advanced.ts new file mode 100644 index 00000000..755a25d9 --- /dev/null +++ b/packages/rei/src/advanced.ts @@ -0,0 +1 @@ +export * as ReiA from "./advancedBarrel.js"; diff --git a/packages/rei/src/advancedBarrel.ts b/packages/rei/src/advancedBarrel.ts new file mode 100644 index 00000000..4af13dde --- /dev/null +++ b/packages/rei/src/advancedBarrel.ts @@ -0,0 +1,103 @@ +import { ccc } from "@ckb-ccc/core"; + +/** + * Interface representing a provider for interacting with accounts and signing messages. + * @interface + */ +export interface Provider { + /** + * Sends a request to the provider. + * @type {RequestMethod} + */ + request: RequestMethod; + + /** + * Adds an event listener to the provider. + * @type {OnMethod} + */ + on: OnMethod; + + /** + * Checks if rei wallet is connected. + * @returns A promise that resolves to true if connected, false otherwise. + */ + isConnected(): Promise; + + /** + * Removes an event listener from the provider. + * @param {string} eventName - The name of the event to remove the listener from. + * @param {(...args: unknown[]) => unknown} listener - The listener function to remove. + * @returns {Provider} The provider instance. + */ + off(eventName: string, listener: (...args: unknown[]) => unknown): void; +} + +/** + * Interface representing a method to add event listeners to the provider. + * @interface + */ +export interface OnMethod { + /** + * Adds an event listener to the provider. + * @param {string} eventName - The name of the event. + * @param {(...args: unknown[]) => unknown} listener - The listener function. + * @returns {Provider} The provider instance. + */ + (eventName: string, listener: (...args: unknown[]) => unknown): void; +} + +export interface RequestMethod { + /** + * Requests the accounts from the provider. + * @param request - The request object. + * @param request.method - The method name. + * @param request.data - The optional method parameters. + * @returns A promise that resolves to an array of account addresses. + */ + (request: { + method: "ckb_requestAccounts"; + data?: undefined; + }): Promise; + + /** + * Signs a message with the personal account. + * @param request - The request object. + * @param request.method - The method name. + * @param request.data - The method parameters. + * @returns A promise that resolves to the signed message. + */ + (request: { + method: "ckb_signMessage"; + data: { message: string }; + }): Promise; + + /** + * Gets the identity of the signer. + * @param request - The request object. + * @param request.method - The method name. + * @returns A promise that resolves to the signed message. + */ + (request: { method: "ckb_getPublicKey" }): Promise; + + /** + * Get network. + * @param request - The request object. + * @param request.method - The method name. + * @param request.data - The method parameters. + * @returns A promise that resolves to the signed message. + */ + (request: { + method: "ckb_switchNetwork"; + data: string; + }): Promise<{ type: string; network: string }>; + + /** + * Signs transaction . + * @param request - The request object. + * @param request.method - The method name. + * @param request.data - The method parameters. + * @returns A promise that resolves to the signed message. + */ + + (request: { method: "ckb_signTransaction"; data: any }): Promise; +} diff --git a/packages/rei/src/barrel.ts b/packages/rei/src/barrel.ts new file mode 100644 index 00000000..5bad6479 --- /dev/null +++ b/packages/rei/src/barrel.ts @@ -0,0 +1,2 @@ +export * from "./signer.js"; +export * from "./signersFactory.js"; diff --git a/packages/rei/src/index.ts b/packages/rei/src/index.ts new file mode 100644 index 00000000..7195b783 --- /dev/null +++ b/packages/rei/src/index.ts @@ -0,0 +1 @@ +export * as Rei from "./barrel.js"; diff --git a/packages/rei/src/signer.ts b/packages/rei/src/signer.ts new file mode 100644 index 00000000..1518039d --- /dev/null +++ b/packages/rei/src/signer.ts @@ -0,0 +1,180 @@ +import { ccc } from "@ckb-ccc/core"; +import { cccA } from "@ckb-ccc/core/advanced"; + +import { Provider } from "./advancedBarrel.js"; + +/** + * Class representing a CKB signer that extends Signer from @ckb-ccc/core. + * @public + */ +export class ReiSigner extends ccc.Signer { + /** + * Creates an instance of Signer. + * @param client - The client instance. + * @param provider - The provider instance. + */ + constructor( + client: ccc.Client, + public readonly provider: Provider, + ) { + super(client); + } + + /** + * Register a listener to be called when this signer is replaced. + * + * @returns A function for unregister + */ + + onReplaced(listener: () => void): () => void { + const stop: (() => void)[] = []; + const replacer = async () => { + listener(); + stop[0]?.(); + }; + stop.push(() => { + this.provider.off("accountsChanged", replacer); + this.provider.off("chainChanged", replacer); + }); + this.provider.on("accountsChanged", replacer); + this.provider.on("chainChanged", replacer); + + return stop[0]; + } + + /** + * Gets the signer type. + * @returns The type of the signer. + */ + get type(): ccc.SignerType { + return ccc.SignerType.CKB; + } + + /** + * Gets the sign type. + * @returns The sign type. + */ + get signType(): ccc.SignerSignType { + return ccc.SignerSignType.CkbSecp256k1; + } + + /** + * Connects to the provider by requesting authentication. + * @returns A promise that resolves when the connection is established. + */ + async connect(): Promise { + const prefixClient = this.client.addressPrefix; + const netChange = await this._getNetworkChanged(); + if (netChange) { + const data = prefixClient === "ckb" ? "mainnet" : "testnet"; + await this.provider.request({ method: "ckb_switchNetwork", data }); + } + } + + /** + * Checks if the signer is connected. + * @returns A promise that resolves to true if connected, false otherwise. + */ + async isConnected(): Promise { + const connected = await this.provider.isConnected(); + if (!connected || (await this._getNetworkChanged())) { + return false; + } + return connected; + } + + async _getNetworkChanged(): Promise { + const prefixClient = this.client.addressPrefix; + + const address = await this.getInternalAddress(); + const { prefix } = cccA.addressPayloadFromString(address); + return prefixClient !== prefix; + } + + /** + * Gets the internal address. + * @returns A promise that resolves to the internal address. + */ + async getInternalAddress(): Promise { + return this.provider.request({ method: "ckb_requestAccounts" }); + } + + /** + * Gets the address object. + * @returns A promise that resolves to the address object. + */ + async getAddressObj(): Promise { + return ccc.Address.fromString(await this.getInternalAddress(), this.client); + } + + /** + * Gets the address objects. + * @returns A promise that resolves to an array of address objects. + */ + async getAddressObjs(): Promise { + return [await this.getAddressObj()]; + } + + /** + * Gets the identity of the signer. + * @returns A promise that resolves to the identity. + */ + async getIdentity(): Promise { + return this.provider.request({ + method: "ckb_getPublicKey", + }); + } + + /** + * Signs a raw message with the personal account. + * @param message - The message to sign. + * @returns A promise that resolves to the signed message. + */ + async signMessageRaw(message: string): Promise { + return this.provider.request({ + method: "ckb_signMessage", + data: { message }, + }); + } + + /** + * prepare a transaction before signing. + * + * @param txLike - The transaction to prepare, represented as a TransactionLike object. + * @returns A promise that resolves to the prepared Transaction object. + */ + + async prepareTransaction( + txLike: ccc.TransactionLike, + ): Promise { + const tx = ccc.Transaction.from(txLike); + await tx.addCellDepsOfKnownScripts( + this.client, + ccc.KnownScript.Secp256k1Blake160, + ); + return ccc.reduceAsync( + await this.getAddressObjs(), + (tx: ccc.Transaction, { script }) => + tx.prepareSighashAllWitness(script, 65, this.client), + tx, + ); + } + + /** + * Signs a transaction without preparing information for it. + * + * @param txLike - The transaction to sign, represented as a TransactionLike object. + * @returns A promise that resolves to the signed Transaction object. + */ + + async signOnlyTransaction( + txLike: ccc.TransactionLike, + ): Promise { + const txFormat = cccA.JsonRpcTransformers.transactionFrom(txLike); + + return this.provider.request({ + method: "ckb_signTransaction", + data: { txSkeleton: txFormat }, + }); + } +} diff --git a/packages/rei/src/signersFactory.ts b/packages/rei/src/signersFactory.ts new file mode 100644 index 00000000..e64015ec --- /dev/null +++ b/packages/rei/src/signersFactory.ts @@ -0,0 +1,23 @@ +import { ccc } from "@ckb-ccc/core"; +import { Provider } from "./advancedBarrel.js"; +import { ReiSigner } from "./signer.js"; + +/** + * Retrieves the Rei signer if available. + * @param client - The client instance. + * @returns The Signer instance if the Rei provider is available, otherwise undefined. + */ +export function getReiSigners(client: ccc.Client): ccc.SignerInfo[] { + const windowRef = window as { ckb?: Provider }; + + if (typeof windowRef.ckb === "undefined") { + return []; + } + + return [ + { + signer: new ReiSigner(client, windowRef.ckb), + name: "CKB", + }, + ]; +} diff --git a/packages/rei/tsconfig.base.json b/packages/rei/tsconfig.base.json new file mode 100644 index 00000000..7e5ac952 --- /dev/null +++ b/packages/rei/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "incremental": true, + "allowJs": true, + "importHelpers": false, + "declaration": true, + "declarationMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/rei/tsconfig.commonjs.json b/packages/rei/tsconfig.commonjs.json new file mode 100644 index 00000000..76a25e98 --- /dev/null +++ b/packages/rei/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/rei/tsconfig.json b/packages/rei/tsconfig.json new file mode 100644 index 00000000..df22faec --- /dev/null +++ b/packages/rei/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + } +} diff --git a/packages/rei/typedoc.json b/packages/rei/typedoc.json new file mode 100644 index 00000000..0162c4f2 --- /dev/null +++ b/packages/rei/typedoc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["./src/index.ts", "./src/advanced.ts"], + "extends": ["../../typedoc.base.json"], + "name": "@ckb-ccc rei" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2884b945..4f954d84 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: '@ckb-ccc/okx': specifier: workspace:* version: link:../okx + '@ckb-ccc/rei': + specifier: workspace:* + version: link:../rei '@ckb-ccc/uni-sat': specifier: workspace:* version: link:../uni-sat @@ -690,6 +693,43 @@ importers: specifier: ^7.7.0 version: 7.7.0(eslint@9.1.0)(typescript@5.4.5) + packages/rei: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@eslint/js': + specifier: ^9.1.1 + version: 9.1.1 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + eslint: + specifier: ^9.1.0 + version: 9.1.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.1.0) + eslint-plugin-prettier: + specifier: ^5.1.3 + version: 5.1.3(@types/eslint@9.6.0)(eslint-config-prettier@9.1.0(eslint@9.1.0))(eslint@9.1.0)(prettier@3.2.5) + prettier: + specifier: ^3.2.5 + version: 3.2.5 + prettier-plugin-organize-imports: + specifier: ^3.2.4 + version: 3.2.4(prettier@3.2.5)(typescript@5.4.5) + rimraf: + specifier: ^5.0.5 + version: 5.0.5 + typescript: + specifier: ^5.4.5 + version: 5.4.5 + typescript-eslint: + specifier: ^7.7.0 + version: 7.7.0(eslint@9.1.0)(typescript@5.4.5) + packages/uni-sat: dependencies: '@ckb-ccc/core':