diff --git a/packages/ccc/package.json b/packages/ccc/package.json index 61219eb9e..e4750d899 100644 --- a/packages/ccc/package.json +++ b/packages/ccc/package.json @@ -62,6 +62,7 @@ "@ckb-ccc/okx": "workspace:*", "@ckb-ccc/rei": "workspace:*", "@ckb-ccc/shell": "workspace:*", + "@ckb-ccc/superise": "workspace:*", "@ckb-ccc/uni-sat": "workspace:*", "@ckb-ccc/utxo-global": "workspace:*", "@ckb-ccc/xverse": "workspace:*" diff --git a/packages/ccc/src/assets/superise.svg.ts b/packages/ccc/src/assets/superise.svg.ts new file mode 100644 index 000000000..3d6f493d6 --- /dev/null +++ b/packages/ccc/src/assets/superise.svg.ts @@ -0,0 +1,85 @@ +import { encodeSvgToImgSrc } from "./utils.js"; + +export const SUPERISE_SVG = encodeSvgToImgSrc(` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +`); diff --git a/packages/ccc/src/barrel.ts b/packages/ccc/src/barrel.ts index 8b46c112d..a47eae2df 100644 --- a/packages/ccc/src/barrel.ts +++ b/packages/ccc/src/barrel.ts @@ -4,6 +4,7 @@ export * from "@ckb-ccc/nip07"; export * from "@ckb-ccc/okx"; export * from "@ckb-ccc/rei"; export * from "@ckb-ccc/shell/barrel"; +export * from "@ckb-ccc/superise"; export * from "@ckb-ccc/uni-sat"; export * from "@ckb-ccc/utxo-global"; export * from "@ckb-ccc/xverse"; diff --git a/packages/ccc/src/signersController.ts b/packages/ccc/src/signersController.ts index 05e58deaa..b92b17161 100644 --- a/packages/ccc/src/signersController.ts +++ b/packages/ccc/src/signersController.ts @@ -4,6 +4,7 @@ import { Nip07 } from "@ckb-ccc/nip07"; import { Okx } from "@ckb-ccc/okx"; import { Rei } from "@ckb-ccc/rei"; import { ccc } from "@ckb-ccc/shell"; +import { SupeRISE } from "@ckb-ccc/superise"; import { UniSat } from "@ckb-ccc/uni-sat"; import { UtxoGlobal } from "@ckb-ccc/utxo-global"; import { Xverse } from "@ckb-ccc/xverse"; @@ -13,6 +14,7 @@ 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 { SUPERISE_SVG } from "./assets/superise.svg.js"; import { UNI_SAT_SVG } from "./assets/uni-sat.svg.js"; import { UTXO_GLOBAL_SVG } from "./assets/utxo-global.svg.js"; @@ -124,6 +126,14 @@ export class SignersController { async addRealSigners(context: SignersControllerRefreshContext) { const { appName, appIcon, client, preferredNetworks } = context; + + await this.addSigners( + "SupeRISE", + SUPERISE_SVG, + SupeRISE.getSupeRISESigners(client, preferredNetworks), + context, + ); + await this.addSigners( "UTXO Global Wallet", UTXO_GLOBAL_SVG, diff --git a/packages/core/src/signer/ckb/verifySuperise.ts b/packages/core/src/signer/ckb/verifySuperise.ts new file mode 100644 index 000000000..46e726187 --- /dev/null +++ b/packages/core/src/signer/ckb/verifySuperise.ts @@ -0,0 +1,40 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { BytesLike, bytesFrom } from "../../bytes/index.js"; +import { hashCkb } from "../../hasher/index.js"; +import { Hex, hexFrom } from "../../hex/index.js"; +import { numFrom } from "../../num/index.js"; + +const SUPERISE_MESSAGE_PREFIX = "\x19SupeRISE Message:\n"; + +/** + * @public + */ +function messageHashCkbSuperise(message: string | BytesLike): Hex { + const msg = typeof message === "string" ? message : hexFrom(message); + const buffer = bytesFrom( + `${SUPERISE_MESSAGE_PREFIX}${msg.length}${msg}`, + "utf8", + ); + return hashCkb(buffer); +} + +/** + * @public + */ +export function verifyMessageSuperise( + message: string | BytesLike, + signature: string, + publicKey: string, +): boolean { + const signatureBytes = bytesFrom(signature); + return secp256k1.verify( + new secp256k1.Signature( + numFrom(signatureBytes.slice(0, 32)), + numFrom(signatureBytes.slice(32, 64)), + ) + .addRecoveryBit(Number(numFrom(signatureBytes.slice(64, 65)))) + .toBytes(), + bytesFrom(messageHashCkbSuperise(message)), + bytesFrom(publicKey), + ); +} diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index 1522b3353..e985a2684 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -13,6 +13,7 @@ import { Num } from "../../num/index.js"; import { verifyMessageBtcEcdsa } from "../btc/index.js"; import { verifyMessageCkbSecp256k1 } from "../ckb/verifyCkbSecp256k1.js"; import { verifyMessageJoyId } from "../ckb/verifyJoyId.js"; +import { verifyMessageSuperise } from "../ckb/verifySuperise.js"; import { verifyMessageDogeEcdsa } from "../doge/verify.js"; import { verifyMessageEvmPersonal } from "../evm/verify.js"; import { verifyMessageNostrEvent } from "../nostr/verify.js"; @@ -28,6 +29,7 @@ export enum SignerSignType { NostrEvent = "NostrEvent", CkbSecp256k1 = "CkbSecp256k1", DogeEcdsa = "DogeEcdsa", + SupeRISE = "SupeRISE", } /** @@ -152,6 +154,12 @@ export abstract class Signer { signature.signature, signature.identity, ); + case SignerSignType.SupeRISE: + return verifyMessageSuperise( + message, + signature.signature, + signature.identity, + ); case SignerSignType.Unknown: throw new Error("Unknown signer sign type"); } diff --git a/packages/superise/.npmignore b/packages/superise/.npmignore new file mode 100644 index 000000000..7a88408aa --- /dev/null +++ b/packages/superise/.npmignore @@ -0,0 +1,21 @@ +node_modules/ +misc/ + +*test.js +*test.ts +*test.d.ts +*test.d.ts.map +*spec.js +*spec.ts +*spec.d.ts +*spec.d.ts.map + +tsconfig.json +tsconfig.*.json +eslint.config.mjs +.prettierrc +.prettierignore + +tsconfig.tsbuildinfo +tsconfig.*.tsbuildinfo +.github/ diff --git a/packages/superise/.prettierignore b/packages/superise/.prettierignore new file mode 100644 index 000000000..aef5d239c --- /dev/null +++ b/packages/superise/.prettierignore @@ -0,0 +1,15 @@ +node_modules/ + +dist/ +dist.commonjs/ + +.npmignore +.prettierrc +tsconfig.json +eslint.config.mjs +prettier.config.* + +tsconfig.tsbuildinfo +.github/ + +CHANGELOG.md diff --git a/packages/superise/README.md b/packages/superise/README.md new file mode 100644 index 000000000..d4fb74223 --- /dev/null +++ b/packages/superise/README.md @@ -0,0 +1,41 @@ +

+ + Logo + +

+ +

+ CCC's support for SupeRISE +

+ +

+ NPM Version + GitHub commit activity + GitHub last commit + GitHub branch check runs + Playground + App + Docs +

+ +

+ CCC - CKBers' Codebase is a one-stop solution for your CKB JS/TS ecosystem development. +
+ 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. +

+ +

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

diff --git a/packages/superise/eslint.config.mjs b/packages/superise/eslint.config.mjs new file mode 100644 index 000000000..b6132c277 --- /dev/null +++ b/packages/superise/eslint.config.mjs @@ -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, +]; diff --git a/packages/superise/misc/basedirs/dist.commonjs/package.json b/packages/superise/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/packages/superise/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/superise/misc/basedirs/dist/package.json b/packages/superise/misc/basedirs/dist/package.json new file mode 100644 index 000000000..aead43de3 --- /dev/null +++ b/packages/superise/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/superise/package.json b/packages/superise/package.json new file mode 100644 index 000000000..df6492def --- /dev/null +++ b/packages/superise/package.json @@ -0,0 +1,52 @@ +{ + "name": "@ckb-ccc/superise", + "version": "1.0.29", + "description": "Common Chains Connector's support for SupeRISE", + "author": "rivjc", + "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 && copyfiles -u 2 misc/basedirs/**/* .", + "lint": "eslint ./src", + "format": "prettier --write . && eslint --fix ./src" + }, + "devDependencies": { + "@eslint/js": "^9.34.0", + "copyfiles": "^2.4.1", + "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/core": "workspace:*" + }, + "packageManager": "pnpm@10.8.1" +} diff --git a/packages/superise/prettier.config.cjs b/packages/superise/prettier.config.cjs new file mode 100644 index 000000000..5e1810363 --- /dev/null +++ b/packages/superise/prettier.config.cjs @@ -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; diff --git a/packages/superise/src/advanced.ts b/packages/superise/src/advanced.ts new file mode 100644 index 000000000..5bf9d080e --- /dev/null +++ b/packages/superise/src/advanced.ts @@ -0,0 +1 @@ +export * as SupeRISEA from "./advancedBarrel.js"; diff --git a/packages/superise/src/advancedBarrel.ts b/packages/superise/src/advancedBarrel.ts new file mode 100644 index 000000000..300bc39b4 --- /dev/null +++ b/packages/superise/src/advancedBarrel.ts @@ -0,0 +1,17 @@ +export interface CkbConnection { + publicKey: string; + address: string; +} + +export interface Bridge { + version: string; + + connectCkb: () => Promise; + + signCkbMessage: (message: string) => Promise<{ signature: string }>; + + signCkbTransaction: ( + transaction: string, + witnessIndexes: number[], + ) => Promise<{ signedTransaction: string }>; +} diff --git a/packages/superise/src/barrel.ts b/packages/superise/src/barrel.ts new file mode 100644 index 000000000..b60ec7b8e --- /dev/null +++ b/packages/superise/src/barrel.ts @@ -0,0 +1,2 @@ +export * from "./ckb/index.js"; +export * from "./signersFactory.js"; diff --git a/packages/superise/src/ckb/index.ts b/packages/superise/src/ckb/index.ts new file mode 100644 index 000000000..858ef3ae5 --- /dev/null +++ b/packages/superise/src/ckb/index.ts @@ -0,0 +1,176 @@ +import { ccc } from "@ckb-ccc/core"; +import type { Bridge, CkbConnection } from "../advancedBarrel"; + +export class CkbSigner extends ccc.Signer { + get type() { + return ccc.SignerType.CKB; + } + + get signType() { + return ccc.SignerSignType.CkbSecp256k1; + } + + private connectionStorageKey = "superise-ckb-connection"; + + private connection?: CkbConnection; + + constructor( + private readonly bridge: Bridge, + client: ccc.Client, + ) { + super(client); + } + + private saveConnection() { + localStorage.setItem( + this.connectionStorageKey, + JSON.stringify(this.connection), + ); + } + + private restoreConnection() { + const connection = localStorage.getItem(this.connectionStorageKey); + if (!connection) return; + try { + this.connection = JSON.parse(connection) as CkbConnection; + } catch (error) { + console.error("Failed to restore superise connection:", error); + } + } + + private async getConnection() { + if (!this.connection) this.restoreConnection(); + if (!this.connection) throw new Error("Not connected"); + + return this.connection; + } + + async connect() { + this.connection = await this.bridge.connectCkb(); + this.saveConnection(); + } + + async isConnected() { + if (this.connection) return true; + + this.restoreConnection(); + return !!this.connection; + } + + override async disconnect() { + this.connection = undefined; + localStorage.removeItem(this.connectionStorageKey); + } + + override async getInternalAddress() { + return (await this.getConnection()).address; + } + + override async getIdentity() { + return (await this.getConnection()).publicKey; + } + + async getAddressObj() { + return await ccc.Address.fromString( + await this.getInternalAddress(), + this.client, + ); + } + + override async getAddressObjs() { + return [await this.getAddressObj()]; + } + + override async signMessageRaw(message: string) { + const sign = await this.bridge.signCkbMessage(message); + return ccc.hexFrom(sign.signature); + } + + async getRelatedScripts( + txLike: ccc.TransactionLike, + ): Promise<{ script: ccc.Script; cellDeps: ccc.CellDepInfo[] }[]> { + const tx = ccc.Transaction.from(txLike); + + const addressObj = await this.getAddressObj(); + const acp = await ccc.Script.fromKnownScript( + this.client, + ccc.KnownScript.AnyoneCanPay, + addressObj.script.args, + ); + + const scripts: { script: ccc.Script; cellDeps: ccc.CellDepInfo[] }[] = []; + for (const input of tx.inputs) { + const { + cellOutput: { lock }, + } = await input.getCell(this.client); + + if (scripts.some(({ script }) => script.eq(lock))) { + continue; + } + + if (lock.eq(addressObj.script)) { + scripts.push({ + script: lock, + cellDeps: ( + await this.client.getKnownScript(ccc.KnownScript.Secp256k1Blake160) + ).cellDeps, + }); + } else if ( + lock.codeHash === acp.codeHash && + lock.hashType === acp.hashType && + lock.args.startsWith(acp.args) + ) { + scripts.push({ + script: lock, + cellDeps: ( + await this.client.getKnownScript(ccc.KnownScript.AnyoneCanPay) + ).cellDeps, + }); + } + } + + return scripts; + } + + override async prepareTransaction(txLike: ccc.TransactionLike) { + const tx = ccc.Transaction.from(txLike); + + const scripts = await this.getRelatedScripts(tx); + + await Promise.all( + scripts.map(async ({ script, cellDeps }) => { + await tx.prepareSighashAllWitness(script, 65, this.client); + await tx.addCellDepInfos(this.client, cellDeps); + }), + ); + + return tx; + } + + override async signOnlyTransaction(txLike: ccc.TransactionLike) { + const tx = ccc.Transaction.from(txLike); + + const witnessIndexes = await ccc.reduceAsync( + await this.getRelatedScripts(tx), + async (indexes, scriptInfo) => { + const index = await tx.findInputIndexByLock( + scriptInfo.script, + this.client, + ); + if (typeof index !== "number") return; + + indexes.push(index); + }, + [] as number[], + ); + + const result = await this.bridge.signCkbTransaction( + ccc.stringify(tx), + witnessIndexes, + ); + const signedTx = JSON.parse( + result.signedTransaction, + ) as ccc.TransactionLike; + return ccc.Transaction.from(signedTx); + } +} diff --git a/packages/superise/src/index.ts b/packages/superise/src/index.ts new file mode 100644 index 000000000..46a02e3c3 --- /dev/null +++ b/packages/superise/src/index.ts @@ -0,0 +1 @@ +export * as SupeRISE from "./barrel.js"; diff --git a/packages/superise/src/signersFactory.ts b/packages/superise/src/signersFactory.ts new file mode 100644 index 000000000..dc16a5436 --- /dev/null +++ b/packages/superise/src/signersFactory.ts @@ -0,0 +1,26 @@ +import { ccc } from "@ckb-ccc/core"; +import type { Bridge } from "./advancedBarrel"; +import { CkbSigner } from "./ckb/index.js"; + +/** + * @public + */ +export function getSupeRISESigners( + client: ccc.Client, + _preferredNetworks?: ccc.NetworkPreference[], +): ccc.SignerInfo[] { + const windowRef = window as { + superise?: Bridge; + }; + + if (typeof windowRef.superise === "undefined") { + return []; + } + + return [ + { + name: "CKB", + signer: new CkbSigner(windowRef.superise, client), + }, + ]; +} diff --git a/packages/superise/tsconfig.base.json b/packages/superise/tsconfig.base.json new file mode 100644 index 000000000..7e5ac952b --- /dev/null +++ b/packages/superise/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/superise/tsconfig.commonjs.json b/packages/superise/tsconfig.commonjs.json new file mode 100644 index 000000000..76a25e98b --- /dev/null +++ b/packages/superise/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/superise/tsconfig.json b/packages/superise/tsconfig.json new file mode 100644 index 000000000..df22faeca --- /dev/null +++ b/packages/superise/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + } +} diff --git a/packages/superise/typedoc.json b/packages/superise/typedoc.json new file mode 100644 index 000000000..b663440e9 --- /dev/null +++ b/packages/superise/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 superise" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78369fd1c..3d01d660c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: '@ckb-ccc/shell': specifier: workspace:* version: link:../shell + '@ckb-ccc/superise': + specifier: workspace:* + version: link:../superise '@ckb-ccc/uni-sat': specifier: workspace:* version: link:../uni-sat @@ -1083,6 +1086,43 @@ importers: specifier: ^8.41.0 version: 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + packages/superise: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@eslint/js': + specifier: ^9.34.0 + version: 9.34.0 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 + eslint: + specifier: ^9.34.0 + version: 9.34.0(jiti@2.5.1) + eslint-config-prettier: + specifier: ^10.1.8 + version: 10.1.8(eslint@9.34.0(jiti@2.5.1)) + eslint-plugin-prettier: + specifier: ^5.5.4 + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.34.0(jiti@2.5.1)))(eslint@9.34.0(jiti@2.5.1))(prettier@3.6.2) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + prettier-plugin-organize-imports: + specifier: ^4.2.0 + version: 4.2.0(prettier@3.6.2)(typescript@5.9.2) + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + typescript: + specifier: ^5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: ^8.41.0 + version: 8.41.0(eslint@9.34.0(jiti@2.5.1))(typescript@5.9.2) + packages/tests: devDependencies: '@ckb-ccc/ccc':