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 @@
+
+
+
+
+
+
+
+ CCC's support for Xverse
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "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.
+
+
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",