diff --git a/.changeset/tall-mangos-kneel.md b/.changeset/tall-mangos-kneel.md new file mode 100644 index 00000000..30eb7392 --- /dev/null +++ b/.changeset/tall-mangos-kneel.md @@ -0,0 +1,9 @@ +--- +"@ckb-ccc/xverse": patch +"@ckb-ccc/ccc": patch +"ckb-ccc": patch +"@ckb-ccc/connector": patch +"@ckb-ccc/connector-react": patch +--- + +feat: support Xverse diff --git a/lerna.json b/lerna.json deleted file mode 100644 index 995ee60d..00000000 --- a/lerna.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.0.12-alpha.6", - "npmClient": "pnpm" -} diff --git a/packages/ccc/package.json b/packages/ccc/package.json index d1e6fc07..00bf0618 100644 --- a/packages/ccc/package.json +++ b/packages/ccc/package.json @@ -58,6 +58,7 @@ "@ckb-ccc/okx": "workspace:*", "@ckb-ccc/uni-sat": "workspace:*", "@ckb-ccc/utxo-global": "workspace:*", - "@ckb-ccc/rei": "workspace:*" + "@ckb-ccc/rei": "workspace:*", + "@ckb-ccc/xverse": "workspace:*" } } diff --git a/packages/ccc/src/advancedBarrel.ts b/packages/ccc/src/advancedBarrel.ts index fcebf6cb..e971a818 100644 --- a/packages/ccc/src/advancedBarrel.ts +++ b/packages/ccc/src/advancedBarrel.ts @@ -4,3 +4,4 @@ export * from "@ckb-ccc/nip07/advanced"; export * from "@ckb-ccc/okx/advanced"; export * from "@ckb-ccc/uni-sat/advanced"; export * from "@ckb-ccc/utxo-global/advanced"; +export * from "@ckb-ccc/xverse/advanced"; diff --git a/packages/ccc/src/barrel.ts b/packages/ccc/src/barrel.ts index f0ac8f42..9720265f 100644 --- a/packages/ccc/src/barrel.ts +++ b/packages/ccc/src/barrel.ts @@ -6,4 +6,5 @@ export * from "@ckb-ccc/okx"; export * from "@ckb-ccc/rei"; export * from "@ckb-ccc/uni-sat"; export * from "@ckb-ccc/utxo-global"; +export * from "@ckb-ccc/xverse"; export * from "./signersController.js"; diff --git a/packages/ccc/src/signersController.ts b/packages/ccc/src/signersController.ts index 719dbcf8..7858db7c 100644 --- a/packages/ccc/src/signersController.ts +++ b/packages/ccc/src/signersController.ts @@ -6,6 +6,7 @@ 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 { Xverse } from "@ckb-ccc/xverse"; import { ETH_SVG } from "./assets/eth.svg.js"; import { JOY_ID_SVG } from "./assets/joy-id.svg.js"; import { METAMASK_SVG } from "./assets/metamask.svg.js"; @@ -146,6 +147,11 @@ export class SignersController { context, ); + await Promise.all(Xverse.getXverseSigners(client, preferredNetworks).map( + ({ wallet, signerInfo }) => + this.addSigner(wallet.name, wallet.icon, signerInfo, context), + )); + const nostrSigner = Nip07.getNip07Signer(client); if (nostrSigner) { await this.addSigner( diff --git a/packages/xverse/.npmignore b/packages/xverse/.npmignore new file mode 100644 index 00000000..0e812402 --- /dev/null +++ b/packages/xverse/.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/xverse/.prettierignore b/packages/xverse/.prettierignore new file mode 100644 index 00000000..e7ce6f62 --- /dev/null +++ b/packages/xverse/.prettierignore @@ -0,0 +1,13 @@ +node_modules/ + +dist/ +dist.commonjs/ + +.npmignore +.prettierrc +tsconfig.json +eslint.config.mjs +.prettierrc + +tsconfig.tsbuildinfo +.github/ diff --git a/packages/xverse/.prettierrc b/packages/xverse/.prettierrc new file mode 100644 index 00000000..6390af08 --- /dev/null +++ b/packages/xverse/.prettierrc @@ -0,0 +1,5 @@ +{ + "singleQuote": false, + "trailingComma": "all", + "plugins": ["prettier-plugin-organize-imports"] +} diff --git a/packages/xverse/README.md b/packages/xverse/README.md new file mode 100644 index 00000000..3426b91d --- /dev/null +++ b/packages/xverse/README.md @@ -0,0 +1,51 @@ +

+ + Logo + +

+ +

+ CCC's support for Xverse +

+ +

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

+ +

+ "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/xverse/eslint.config.mjs b/packages/xverse/eslint.config.mjs new file mode 100644 index 00000000..b42a690d --- /dev/null +++ b/packages/xverse/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/xverse/misc/basedirs/dist.commonjs/package.json b/packages/xverse/misc/basedirs/dist.commonjs/package.json new file mode 100644 index 00000000..5bbefffb --- /dev/null +++ b/packages/xverse/misc/basedirs/dist.commonjs/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/packages/xverse/misc/basedirs/dist/package.json b/packages/xverse/misc/basedirs/dist/package.json new file mode 100644 index 00000000..aead43de --- /dev/null +++ b/packages/xverse/misc/basedirs/dist/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} \ No newline at end of file diff --git a/packages/xverse/package.json b/packages/xverse/package.json new file mode 100644 index 00000000..6d7d0efc --- /dev/null +++ b/packages/xverse/package.json @@ -0,0 +1,57 @@ +{ + "name": "@ckb-ccc/xverse", + "version": "0.0.14", + "description": "CCC - CKBer's Codebase. Common Chains Connector's support for Xverse", + "author": "Hanssen0 ", + "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:*", + "valibot": "^0.42.1" + } +} diff --git a/packages/xverse/src/advanced.ts b/packages/xverse/src/advanced.ts new file mode 100644 index 00000000..571cf74c --- /dev/null +++ b/packages/xverse/src/advanced.ts @@ -0,0 +1 @@ +export * as XverseA from "./advancedBarrel.js"; diff --git a/packages/xverse/src/advancedBarrel.ts b/packages/xverse/src/advancedBarrel.ts new file mode 100644 index 00000000..95d6442e --- /dev/null +++ b/packages/xverse/src/advancedBarrel.ts @@ -0,0 +1 @@ +export * from "./sat-connect-core/advanced.js"; diff --git a/packages/xverse/src/barrel.ts b/packages/xverse/src/barrel.ts new file mode 100644 index 00000000..5bad6479 --- /dev/null +++ b/packages/xverse/src/barrel.ts @@ -0,0 +1,2 @@ +export * from "./signer.js"; +export * from "./signersFactory.js"; diff --git a/packages/xverse/src/index.ts b/packages/xverse/src/index.ts new file mode 100644 index 00000000..e51152de --- /dev/null +++ b/packages/xverse/src/index.ts @@ -0,0 +1 @@ +export * as Xverse from "./barrel.js"; diff --git a/packages/xverse/src/sat-connect-core/advanced.ts b/packages/xverse/src/sat-connect-core/advanced.ts new file mode 100644 index 00000000..f05747d8 --- /dev/null +++ b/packages/xverse/src/sat-connect-core/advanced.ts @@ -0,0 +1,4 @@ +export * from "./btcMethods.advanced"; +export * from "./provider.advanced"; +export * from "./requests.advanced"; +export * from "./types.advanced"; diff --git a/packages/xverse/src/sat-connect-core/btcMethods.advanced.ts b/packages/xverse/src/sat-connect-core/btcMethods.advanced.ts new file mode 100644 index 00000000..147d37a2 --- /dev/null +++ b/packages/xverse/src/sat-connect-core/btcMethods.advanced.ts @@ -0,0 +1,304 @@ +// From https://github.com/secretkeylabs/sats-connect-core/ + +/** + * Represents the types and interfaces related to BTC methods. + */ + +import * as v from "valibot"; +import { + MethodParamsAndResult, + rpcRequestMessageSchema, +} from "./types.advanced"; +import { walletTypeSchema } from "./walletMethods.advanced"; + +export enum AddressPurpose { + Ordinals = "ordinals", + Payment = "payment", + Stacks = "stacks", +} + +export enum AddressType { + p2pkh = "p2pkh", + p2sh = "p2sh", + p2wpkh = "p2wpkh", + p2wsh = "p2wsh", + p2tr = "p2tr", + stacks = "stacks", +} + +export const addressSchema = v.object({ + address: v.string(), + publicKey: v.string(), + purpose: v.enum(AddressPurpose), + addressType: v.enum(AddressType), +}); +export type Address = v.InferOutput; + +export const getInfoMethodName = "getInfo"; +export const getInfoParamsSchema = v.nullish(v.null()); +export type GetInfoParams = v.InferOutput; +export const getInfoResultSchema = v.object({ + /** + * Version of the wallet. + */ + version: v.string(), + + /** + * [WBIP](https://wbips.netlify.app/wbips/WBIP002) methods supported by the wallet. + */ + methods: v.optional(v.array(v.string())), + + /** + * List of WBIP standards supported by the wallet. Not currently used. + */ + supports: v.array(v.string()), +}); +export type GetInfoResult = v.InferOutput; +export const getInfoRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(getInfoMethodName), + params: getInfoParamsSchema, + id: v.string(), + }).entries, +}); +export type GetInfoRequestMessage = v.InferOutput< + typeof getInfoRequestMessageSchema +>; +export type GetInfo = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; + +export const getAddressesMethodName = "getAddresses"; +export const getAddressesParamsSchema = v.object({ + /** + * The purposes for which to generate addresses. See + * {@linkcode AddressPurpose} for available purposes. + */ + purposes: v.array(v.enum(AddressPurpose)), + /** + * A message to be displayed to the user in the request prompt. + */ + message: v.optional(v.string()), +}); +export type GetAddressesParams = v.InferOutput; +export const getAddressesResultSchema = v.object({ + /** + * The addresses generated for the given purposes. + */ + addresses: v.array(addressSchema), +}); +export type GetAddressesResult = v.InferOutput; +export const getAddressesRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(getAddressesMethodName), + params: getAddressesParamsSchema, + id: v.string(), + }).entries, +}); +export type GetAddressesRequestMessage = v.InferOutput< + typeof getAddressesRequestMessageSchema +>; +export type GetAddresses = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; + +export const signMessageMethodName = "signMessage"; + +export enum MessageSigningProtocols { + ECDSA = "ECDSA", + BIP322 = "BIP322", +} + +export const signMessageParamsSchema = v.object({ + /** + * The address used for signing. + **/ + address: v.string(), + /** + * The message to sign. + **/ + message: v.string(), + /** + * The protocol to use for signing the message. + */ + protocol: v.optional(v.enum(MessageSigningProtocols)), +}); +export type SignMessageParams = v.InferOutput; +export const signMessageResultSchema = v.object({ + /** + * The signature of the message. + */ + signature: v.string(), + /** + * hash of the message. + */ + messageHash: v.string(), + /** + * The address used for signing. + */ + address: v.string(), + /** + * The protocol to use for signing the message. + */ + protocol: v.enum(MessageSigningProtocols), +}); +export type SignMessageResult = v.InferOutput; +export const signMessageRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(signMessageMethodName), + params: signMessageParamsSchema, + id: v.string(), + }).entries, +}); +export type SignMessageRequestMessage = v.InferOutput< + typeof signMessageRequestMessageSchema +>; +export type SignMessage = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; + +type Recipient = { + /** + * The recipient's address. + **/ + address: string; + /** + * The amount to send to the recipient in satoshis. + */ + amount: number; +}; + +export type SendTransferParams = { + /** + * Array of recipients to send to. + * The amount to send to each recipient is in satoshis. + */ + recipients: Array; +}; +type SendTransferResult = { + /** + * The transaction id as a hex-encoded string. + */ + txid: string; +}; + +export type SendTransfer = MethodParamsAndResult< + SendTransferParams, + SendTransferResult +>; + +export type SignPsbtParams = { + /** + * The base64 encoded PSBT to sign. + */ + psbt: string; + /** + * The inputs to sign. + * The key is the address and the value is an array of indexes of the inputs to sign. + */ + signInputs: Record; + /** + * the sigHash type to use for signing. + * will default to the sighash type of the input if not provided. + **/ + allowedSignHash?: number; + /** + * Whether to broadcast the transaction after signing. + **/ + broadcast?: boolean; +}; + +export type SignPsbtResult = { + /** + * The base64 encoded PSBT after signing. + */ + psbt: string; + /** + * The transaction id as a hex-encoded string. + * This is only returned if the transaction was broadcast. + **/ + txid?: string; +}; + +export type SignPsbt = MethodParamsAndResult; + +export const getAccountsMethodName = "getAccounts"; +export const getAccountsParamsSchema = v.object({ + /** + * The purposes for which to generate addresses. See + * {@linkcode AddressPurpose} for available purposes. + */ + purposes: v.array(v.enum(AddressPurpose)), + /** + * A message to be displayed to the user in the request prompt. + */ + message: v.optional(v.string()), +}); +export type GetAccountsParams = v.InferOutput; + +export const getAccountsResultSchema = v.array( + v.object({ + ...addressSchema.entries, + ...v.object({ + walletType: walletTypeSchema, + }).entries, + }), +); +export type GetAccountsResult = v.InferOutput; +export const getAccountsRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(getAccountsMethodName), + params: getAccountsParamsSchema, + id: v.string(), + }).entries, +}); +export type GetAccountsRequestMessage = v.InferOutput< + typeof getAccountsRequestMessageSchema +>; +export type GetAccounts = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; + +export const getBalanceMethodName = "getBalance"; +export const getBalanceParamsSchema = v.nullish(v.null()); +export const getBalanceResultSchema = v.object({ + /** + * The confirmed balance of the wallet in sats. Using a string due to chrome + * messages not supporting bigint + * (https://issues.chromium.org/issues/40116184). + */ + confirmed: v.string(), + + /** + * The unconfirmed balance of the wallet in sats. Using a string due to chrome + * messages not supporting bigint + * (https://issues.chromium.org/issues/40116184). + */ + unconfirmed: v.string(), + + /** + * The total balance (both confirmed and unconfrimed UTXOs) of the wallet in + * sats. Using a string due to chrome messages not supporting bigint + * (https://issues.chromium.org/issues/40116184). + */ + total: v.string(), +}); +export const getBalanceRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(getBalanceMethodName), + id: v.string(), + }).entries, +}); +export type GetBalance = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; diff --git a/packages/xverse/src/sat-connect-core/provider.advanced.ts b/packages/xverse/src/sat-connect-core/provider.advanced.ts new file mode 100644 index 00000000..0ded4006 --- /dev/null +++ b/packages/xverse/src/sat-connect-core/provider.advanced.ts @@ -0,0 +1,61 @@ +import * as v from "valibot"; +import { Requests, Params } from "./requests.advanced"; +import { RpcResponse } from "./types.advanced"; + +// accountChange +export const accountChangeEventName = "accountChange"; +export const accountChangeSchema = v.object({ + type: v.literal(accountChangeEventName), +}); +export type AccountChangeEvent = v.InferOutput; + +// networkChange +export const networkChangeEventName = "networkChange"; +export const networkChangeSchema = v.object({ + type: v.literal(networkChangeEventName), +}); +export type NetworkChangeEvent = v.InferOutput; + +// disconnect +export const disconnectEventName = "disconnect"; +export const disconnectSchema = v.object({ + type: v.literal(disconnectEventName), +}); +export type DisconnectEvent = v.InferOutput; + +export const walletEventSchema = v.variant("type", [ + accountChangeSchema, + networkChangeSchema, + disconnectSchema, +]); + +export type WalletEvent = v.InferOutput; +export type AddListener = ( + eventName: WalletEventName, + cb: (event: Extract) => void, +) => () => void; + +/** + * Interface representing a provider for interacting with accounts and signing messages. + */ +export interface BtcProvider { + request: ( + method: Method, + options: Params, + providerId?: string, + ) => Promise>; + + addListener: AddListener; +} + +export interface Provider { + id: string; + name: string; + icon: string; + webUrl?: string; + chromeWebStoreUrl?: string; + mozillaAddOnsUrl?: string; + googlePlayStoreUrl?: string; + iOSAppStoreUrl?: string; + methods?: string[]; +} diff --git a/packages/xverse/src/sat-connect-core/requests.advanced.ts b/packages/xverse/src/sat-connect-core/requests.advanced.ts new file mode 100644 index 00000000..153df1f9 --- /dev/null +++ b/packages/xverse/src/sat-connect-core/requests.advanced.ts @@ -0,0 +1,41 @@ +import { + GetAccounts, + GetAddresses, + GetBalance, + GetInfo, + SendTransfer, + SignMessage, + SignPsbt, +} from "./btcMethods.advanced"; +import { + GetWalletType, + RenouncePermissions, + RequestPermissions, +} from "./walletMethods.advanced"; + +export interface BtcRequests { + getInfo: GetInfo; + getAddresses: GetAddresses; + getAccounts: GetAccounts; + getBalance: GetBalance; + signMessage: SignMessage; + sendTransfer: SendTransfer; + signPsbt: SignPsbt; +} + +export type BtcRequestMethod = keyof BtcRequests; + +export interface WalletRequests { + wallet_requestPermissions: RequestPermissions; + wallet_renouncePermissions: RenouncePermissions; + wallet_getWalletType: GetWalletType; +} + +export type Requests = BtcRequests & WalletRequests; + +export type Return = Method extends keyof Requests + ? Requests[Method]["result"] + : never; +export type Params = Method extends keyof Requests + ? Requests[Method]["params"] + : never; diff --git a/packages/xverse/src/sat-connect-core/types.advanced.ts b/packages/xverse/src/sat-connect-core/types.advanced.ts new file mode 100644 index 00000000..04bb366b --- /dev/null +++ b/packages/xverse/src/sat-connect-core/types.advanced.ts @@ -0,0 +1,159 @@ +// From https://github.com/secretkeylabs/sats-connect-core/ + +import * as v from "valibot"; +import type { BtcProvider } from "./provider.advanced"; +import type { Requests, Return } from "./requests.advanced"; + +export enum BitcoinNetworkType { + Mainnet = "Mainnet", + Testnet = "Testnet", + Signet = "Signet", +} + +export interface BitcoinNetwork { + type: BitcoinNetworkType; + address?: string; +} + +export interface RequestPayload { + network: BitcoinNetwork; +} + +export interface RequestOptions { + onFinish: (response: Response) => void; + onCancel: () => void; + payload: Payload; + getProvider?: () => Promise; +} + +// RPC Request and Response types + +export const RpcIdSchema = v.optional( + v.union([v.string(), v.number(), v.null()]), +); +export type RpcId = v.InferOutput; +export const rpcRequestMessageSchema = v.object({ + jsonrpc: v.literal("2.0"), + method: v.string(), + params: v.optional( + v.union([ + v.array(v.unknown()), + v.looseObject({}), + // Note: This is to support current incorrect usage of RPC 2.0. Params need + // to be either an array or an object when provided. Changing this now would + // be a breaking change, so accepting null values for now. Tracking in + // https://linear.app/xverseapp/issue/ENG-4538. + v.null(), + ]), + ), + id: RpcIdSchema, +}); +export type RpcRequestMessage = v.InferOutput; + +export interface RpcBase { + jsonrpc: "2.0"; + id: RpcId; +} +export interface RpcRequest extends RpcBase { + method: T; + params: U; +} + +export interface MethodParamsAndResult { + params: TParams; + result: TResult; +} + +/** + * @enum {number} RpcErrorCode + * @description JSON-RPC error codes + * @see https://www.jsonrpc.org/specification#error_object + */ +export enum RpcErrorCode { + /** + * Parse error Invalid JSON + **/ + PARSE_ERROR = -32700, + /** + * The JSON sent is not a valid Request object. + **/ + INVALID_REQUEST = -32600, + /** + * The method does not exist/is not available. + **/ + METHOD_NOT_FOUND = -32601, + /** + * Invalid method parameter(s). + */ + INVALID_PARAMS = -32602, + /** + * Internal JSON-RPC error. + * This is a generic error, used when the server encounters an error in performing the request. + **/ + INTERNAL_ERROR = -32603, + /** + * user rejected/canceled the request + */ + USER_REJECTION = -32000, + /** + * method is not supported for the address provided + */ + METHOD_NOT_SUPPORTED = -32001, + /** + * The client does not have permission to access the requested resource. + */ + ACCESS_DENIED = -32002, +} + +export const rpcSuccessResponseMessageSchema = v.object({ + jsonrpc: v.literal("2.0"), + result: v.nonOptional(v.unknown()), + id: RpcIdSchema, +}); +export type RpcSuccessResponseMessage = v.InferOutput< + typeof rpcSuccessResponseMessageSchema +>; + +export const rpcErrorResponseMessageSchema = v.object({ + jsonrpc: v.literal("2.0"), + error: v.nonOptional(v.unknown()), + id: RpcIdSchema, +}); +export type RpcErrorResponseMessage = v.InferOutput< + typeof rpcErrorResponseMessageSchema +>; +export const rpcResponseMessageSchema = v.union([ + rpcSuccessResponseMessageSchema, + rpcErrorResponseMessageSchema, +]); +export type RpcResponseMessage = v.InferOutput; + +export interface RpcError { + code: number | RpcErrorCode; + message: string; + data?: any; +} + +export interface RpcErrorResponse + extends RpcBase { + error: TError; +} + +export interface RpcSuccessResponse + extends RpcBase { + result: Return; +} + +export type RpcResponse = + | RpcSuccessResponse + | RpcErrorResponse; + +export type RpcResult = + | { + result: RpcSuccessResponse["result"]; + status: "success"; + } + | { + error: RpcErrorResponse["error"]; + status: "error"; + }; diff --git a/packages/xverse/src/sat-connect-core/walletMethods.advanced.ts b/packages/xverse/src/sat-connect-core/walletMethods.advanced.ts new file mode 100644 index 00000000..8812f3c7 --- /dev/null +++ b/packages/xverse/src/sat-connect-core/walletMethods.advanced.ts @@ -0,0 +1,58 @@ +// From https://github.com/secretkeylabs/sats-connect-core/ + +import * as v from "valibot"; +import { + MethodParamsAndResult, + rpcRequestMessageSchema, +} from "./types.advanced.js"; + +export const walletTypes = ["software", "ledger"] as const; +export const walletTypeSchema = v.picklist(walletTypes); +export type WalletType = v.InferOutput; + +export const requestPermissionsMethodName = "wallet_requestPermissions"; +export const requestPermissionsParamsSchema = v.undefined(); +export const requestPermissionsResultSchema = v.literal(true); +export const requestPermissionsRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(requestPermissionsMethodName), + params: requestPermissionsParamsSchema, + id: v.string(), + }).entries, +}); +export type RequestPermissions = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; + +export const renouncePermissionsMethodName = "wallet_renouncePermissions"; +export const renouncePermissionsParamsSchema = v.undefined(); +export const renouncePermissionsResultSchema = v.literal(true); +export const renouncePermissionsRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(renouncePermissionsMethodName), + params: renouncePermissionsParamsSchema, + id: v.string(), + }).entries, +}); +export type RenouncePermissions = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; + +export const getWalletTypeMethodName = "wallet_getWalletType"; +export const getWalletTypeParamsSchema = v.nullish(v.null()); +export const getWalletTypeResultSchema = walletTypeSchema; +export const getWalletTypeRequestMessageSchema = v.object({ + ...rpcRequestMessageSchema.entries, + ...v.object({ + method: v.literal(getWalletTypeMethodName), + id: v.string(), + }).entries, +}); +export type GetWalletType = MethodParamsAndResult< + v.InferOutput, + v.InferOutput +>; diff --git a/packages/xverse/src/signer.ts b/packages/xverse/src/signer.ts new file mode 100644 index 00000000..88b0eae2 --- /dev/null +++ b/packages/xverse/src/signer.ts @@ -0,0 +1,170 @@ +import { ccc } from "@ckb-ccc/core"; +import * as v from "valibot"; +import { + Address, + AddressPurpose, + BtcProvider, + MessageSigningProtocols, + Requests, + Return, + RpcErrorCode, + RpcResponse, + rpcErrorResponseMessageSchema, + rpcSuccessResponseMessageSchema, +} from "./advancedBarrel.js"; + +async function checkResponse( + response: Promise>, +): Promise> { + const res = await response; + if (v.is(rpcErrorResponseMessageSchema, res)) { + throw res.error; + } + + if (v.is(rpcSuccessResponseMessageSchema, res)) { + return res.result as Return; + } + + throw { + code: RpcErrorCode.INTERNAL_ERROR, + message: "Received unknown response from provider.", + data: res, + }; +} + +/** + * Class representing a Bitcoin signer that extends SignerBtc + * @public + */ +export class Signer extends ccc.SignerBtc { + private addressCache: Promise
| undefined; + + /** + * Creates an instance of Signer. + * @param client - The client instance. + * @param provider - The provider instance. + */ + constructor( + client: ccc.Client, + public readonly provider: BtcProvider, + private readonly preferredNetworks: ccc.NetworkPreference[] = [ + { + addressPrefix: "ckb", + signerType: ccc.SignerType.BTC, + network: "btc", + }, + { + addressPrefix: "ckt", + signerType: ccc.SignerType.BTC, + network: "btcTestnet", + }, + ], + ) { + super(client); + } + + async assertAddress(): Promise
{ + this.addressCache = + this.addressCache ?? + (async () => { + if (!(await this.isConnected())) { + return; + } + + return ( + await checkResponse( + this.provider.request("getAddresses", { + purposes: [AddressPurpose.Payment], + }), + ) + ).addresses[0]; + })(); + const address = await this.addressCache; + + if (address) { + return address; + } + throw Error("Not connected"); + } + + /** + * Gets the Bitcoin account address. + * @returns A promise that resolves to the Bitcoin account address. + */ + async getBtcAccount(): Promise { + return (await this.assertAddress()).address; + } + + /** + * Gets the Bitcoin public key. + * @returns A promise that resolves to the Bitcoin public key. + */ + async getBtcPublicKey(): Promise { + return ccc.hexFrom((await this.assertAddress()).publicKey); + } + + /** + * Connects to the provider by requesting accounts. + * @returns A promise that resolves when the connection is established. + */ + async connect(): Promise { + if (await this.isConnected()) { + return; + } + + await checkResponse( + this.provider.request("wallet_requestPermissions", undefined), + ); + } + + async disconnect(): Promise { + this.addressCache = undefined; + } + + onReplaced(listener: () => void): () => void { + const stop: (() => void)[] = []; + const replacer = async () => { + listener(); + stop[0]?.(); + }; + stop.push( + this.provider.addListener("accountChange", replacer), + this.provider.addListener("networkChange", replacer), + ); + + return stop[0]; + } + + /** + * Checks if the signer is connected. + * @returns A promise that resolves to true if connected, false otherwise. + */ + async isConnected(): Promise { + try { + await checkResponse(this.provider.request("getBalance", undefined)); + return true; + } catch (error) { + return false; + } + } + + /** + * Signs a raw message with the Bitcoin account. + * @param message - The message to sign. + * @returns A promise that resolves to the signed message. + */ + async signMessageRaw(message: string | ccc.BytesLike): Promise { + const challenge = + typeof message === "string" ? message : ccc.hexFrom(message).slice(2); + + return ( + await checkResponse( + this.provider.request("signMessage", { + message: challenge, + address: (await this.assertAddress()).address, + protocol: MessageSigningProtocols.ECDSA, + }), + ) + ).signature; + } +} diff --git a/packages/xverse/src/signersFactory.ts b/packages/xverse/src/signersFactory.ts new file mode 100644 index 00000000..2f0a06c4 --- /dev/null +++ b/packages/xverse/src/signersFactory.ts @@ -0,0 +1,50 @@ +import { ccc } from "@ckb-ccc/core"; +import { BtcProvider, Provider } from "./advancedBarrel.js"; +import { Signer } from "./signer.js"; + +function getProviderById(providerId: string) { + return providerId?.split(".").reduce((acc: any, part) => acc?.[part], window); +} + +/** + * Retrieves the Xverse signer if available. + * @public + * + * @param client - The client instance. + * @returns All Xverse Signer instances + */ +export function getXverseSigners( + client: ccc.Client, + preferredNetworks?: ccc.NetworkPreference[], +): { wallet: ccc.Wallet; signerInfo: ccc.SignerInfo }[] { + const windowRef = window as { + BitcoinProvider?: BtcProvider; + XverseProviders?: { + BitcoinProvider?: BtcProvider; + }; + btc_providers?: Provider[]; + }; + + const signers = (() => { + if (windowRef.btc_providers) { + return windowRef.btc_providers.map((provider) => ({ + wallet: { + name: provider.name, + icon: provider.icon, + }, + signerInfo: { + name: "BTC", + signer: new Signer( + client, + getProviderById(provider.id), + preferredNetworks, + ), + }, + })); + } + + return []; + })(); + + return signers; +} diff --git a/packages/xverse/tsconfig.base.json b/packages/xverse/tsconfig.base.json new file mode 100644 index 00000000..7e5ac952 --- /dev/null +++ b/packages/xverse/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/xverse/tsconfig.commonjs.json b/packages/xverse/tsconfig.commonjs.json new file mode 100644 index 00000000..76a25e98 --- /dev/null +++ b/packages/xverse/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/xverse/tsconfig.json b/packages/xverse/tsconfig.json new file mode 100644 index 00000000..df22faec --- /dev/null +++ b/packages/xverse/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "./dist", + } +} diff --git a/packages/xverse/typedoc.json b/packages/xverse/typedoc.json new file mode 100644 index 00000000..a623779f --- /dev/null +++ b/packages/xverse/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 xverse" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index df4dfb3a..de20f288 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: '@ckb-ccc/utxo-global': specifier: workspace:* version: link:../utxo-global + '@ckb-ccc/xverse': + specifier: workspace:* + version: link:../xverse devDependencies: '@eslint/js': specifier: ^9.1.1 @@ -807,6 +810,46 @@ importers: specifier: ^7.7.0 version: 7.7.0(eslint@9.1.0)(typescript@5.4.5) + packages/xverse: + dependencies: + '@ckb-ccc/core': + specifier: workspace:* + version: link:../core + valibot: + specifier: ^0.42.1 + version: 0.42.1(typescript@5.4.5) + 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: '@aashutoshrathi/word-wrap@1.2.6': @@ -4898,6 +4941,14 @@ packages: resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} engines: {node: '>=10.12.0'} + valibot@0.42.1: + resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + validator@13.12.0: resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} engines: {node: '>= 0.10'} @@ -10148,6 +10199,10 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@0.42.1(typescript@5.4.5): + optionalDependencies: + typescript: 5.4.5 + validator@13.12.0: {} varuint-bitcoin@1.1.2: diff --git a/typedoc.json b/typedoc.json index 670f48dc..d03e5464 100644 --- a/typedoc.json +++ b/typedoc.json @@ -10,6 +10,7 @@ "packages/rei", "packages/joy-id", "packages/okx", + "packages/xverse", "packages/uni-sat", "packages/nip07", "packages/eip6963",