From 0e37e2bf602790e08a6afdd2bff5d8ccfc98c18f Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Tue, 10 Dec 2024 19:11:27 +0800 Subject: [PATCH 1/2] feat: support doge signer --- .../connector/src/assets/chains/doge.svg.ts | 5 + packages/connector/src/assets/chains/index.ts | 1 + .../connector/src/scenes/selecting/signers.ts | 2 + packages/core/package.json | 2 + packages/core/src/ckb/transactionLumos.ts | 3 +- packages/core/src/signer/btc/signerBtc.ts | 5 +- packages/core/src/signer/btc/verify.ts | 33 ++++- packages/core/src/signer/doge/index.ts | 4 + packages/core/src/signer/doge/signerDoge.ts | 116 ++++++++++++++++++ .../signer/doge/signerDogeAddressReadonly.ts | 52 ++++++++ .../src/signer/doge/signerDogePrivateKey.ts | 97 +++++++++++++++ packages/core/src/signer/doge/verify.ts | 38 ++++++ packages/core/src/signer/index.ts | 1 + packages/core/src/signer/nostr/index.ts | 1 + packages/core/src/signer/signer/index.ts | 9 ++ pnpm-lock.yaml | 35 +++++- 16 files changed, 392 insertions(+), 12 deletions(-) create mode 100644 packages/connector/src/assets/chains/doge.svg.ts create mode 100644 packages/core/src/signer/doge/index.ts create mode 100644 packages/core/src/signer/doge/signerDoge.ts create mode 100644 packages/core/src/signer/doge/signerDogeAddressReadonly.ts create mode 100644 packages/core/src/signer/doge/signerDogePrivateKey.ts create mode 100644 packages/core/src/signer/doge/verify.ts diff --git a/packages/connector/src/assets/chains/doge.svg.ts b/packages/connector/src/assets/chains/doge.svg.ts new file mode 100644 index 00000000..a8eb4466 --- /dev/null +++ b/packages/connector/src/assets/chains/doge.svg.ts @@ -0,0 +1,5 @@ +import { encodeSvgToImgSrc } from "../utils.js"; + +export const DOGE_SVG = encodeSvgToImgSrc( + 'Dogecoin (DOGE)', +); diff --git a/packages/connector/src/assets/chains/index.ts b/packages/connector/src/assets/chains/index.ts index 98734bab..293e70a4 100644 --- a/packages/connector/src/assets/chains/index.ts +++ b/packages/connector/src/assets/chains/index.ts @@ -1,4 +1,5 @@ export * from "./btc.svg.js"; export * from "./ckb.svg.js"; +export * from "./doge.svg.js"; export * from "./eth.svg.js"; export * from "./nostr.svg.js"; diff --git a/packages/connector/src/scenes/selecting/signers.ts b/packages/connector/src/scenes/selecting/signers.ts index 15892d77..ad334a89 100644 --- a/packages/connector/src/scenes/selecting/signers.ts +++ b/packages/connector/src/scenes/selecting/signers.ts @@ -4,6 +4,7 @@ import { repeat } from "lit/directives/repeat.js"; import { BTC_SVG, CKB_SVG, + DOGE_SVG, ETH_SVG, NOSTR_SVG, } from "../../assets/chains/index.js"; @@ -14,6 +15,7 @@ export function signerTypeToIcon(type: ccc.SignerType): string { [ccc.SignerType.EVM]: ETH_SVG, [ccc.SignerType.CKB]: CKB_SVG, [ccc.SignerType.Nostr]: NOSTR_SVG, + [ccc.SignerType.Doge]: DOGE_SVG, }[type]; } diff --git a/packages/core/package.json b/packages/core/package.json index eac66acb..8915d743 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,6 +10,7 @@ "type": "git", "url": "git://github.com/ckb-devrel/ccc.git" }, + "type": "module", "main": "./dist.commonjs/index.js", "module": "./dist/index.js", "exports": { @@ -65,6 +66,7 @@ "abort-controller": "^3.0.0", "bech32": "^2.0.0", "bitcoinjs-message": "^2.2.0", + "bs58check": "^4.0.0", "buffer": "^6.0.3", "cross-fetch": "^4.0.0", "ethers": "^6.13.1", diff --git a/packages/core/src/ckb/transactionLumos.ts b/packages/core/src/ckb/transactionLumos.ts index 2873a0e6..2134a782 100644 --- a/packages/core/src/ckb/transactionLumos.ts +++ b/packages/core/src/ckb/transactionLumos.ts @@ -1,4 +1,5 @@ -import { CellDepLike, CellOutputLike, HexLike, OutPointLike } from "../barrel"; +import { CellDepLike, CellOutputLike, OutPointLike } from "../ckb/index.js"; +import { HexLike } from "../hex/index.js"; export interface LumosTransactionSkeletonType { cellDeps: { diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index bc33a822..64112a74 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -1,5 +1,3 @@ -import { ripemd160 } from "@noble/hashes/ripemd160"; -import { sha256 } from "@noble/hashes/sha256"; import { Address } from "../../address/index.js"; import { bytesConcat, bytesFrom } from "../../bytes/index.js"; import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; @@ -7,6 +5,7 @@ import { KnownScript } from "../../client/index.js"; import { HexLike, hexFrom } from "../../hex/index.js"; import { numToBytes } from "../../num/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; +import { btcEcdsaPublicKeyHash } from "./verify.js"; /** * An abstract class extending the Signer class for Bitcoin-like signing operations. @@ -62,7 +61,7 @@ export abstract class SignerBtc extends Signer { */ async getAddressObjs(): Promise { const publicKey = await this.getBtcPublicKey(); - const hash = ripemd160(sha256(bytesFrom(publicKey))); + const hash = btcEcdsaPublicKeyHash(publicKey); return [ await Address.fromKnownScript( diff --git a/packages/core/src/signer/btc/verify.ts b/packages/core/src/signer/btc/verify.ts index 2fce769e..fb2f12fb 100644 --- a/packages/core/src/signer/btc/verify.ts +++ b/packages/core/src/signer/btc/verify.ts @@ -1,7 +1,36 @@ import { secp256k1 } from "@noble/curves/secp256k1"; +import { ripemd160 } from "@noble/hashes/ripemd160"; +import { sha256 } from "@noble/hashes/sha256"; import { magicHash } from "bitcoinjs-message"; -import { BytesLike, bytesFrom } from "../../bytes/index.js"; -import { hexFrom } from "../../hex/index.js"; +import bs58check from "bs58check"; +import { Bytes, BytesLike, bytesConcat, bytesFrom } from "../../bytes/index.js"; +import { Hex, hexFrom } from "../../hex/index.js"; + +/** + * @public + */ +export function btcEcdsaPublicKeyHash(publicKey: BytesLike): Bytes { + return ripemd160(sha256(bytesFrom(publicKey))); +} + +/** + * @public + */ +export function btcP2pkhAddressFromPublicKey( + publicKey: BytesLike, + network: number, +): string { + return bs58check.encode( + bytesConcat([network], btcEcdsaPublicKeyHash(publicKey)), + ); +} + +/** + * @public + */ +export function btcPublicKeyFromP2pkhAddress(address: string): Hex { + return hexFrom(bs58check.decode(address).slice(1)); +} /** * @public diff --git a/packages/core/src/signer/doge/index.ts b/packages/core/src/signer/doge/index.ts new file mode 100644 index 00000000..b38a6890 --- /dev/null +++ b/packages/core/src/signer/doge/index.ts @@ -0,0 +1,4 @@ +export * from "./signerDoge.js"; +export * from "./signerDogeAddressReadonly.js"; +export * from "./signerDogePrivateKey.js"; +export * from "./verify.js"; diff --git a/packages/core/src/signer/doge/signerDoge.ts b/packages/core/src/signer/doge/signerDoge.ts new file mode 100644 index 00000000..11506af1 --- /dev/null +++ b/packages/core/src/signer/doge/signerDoge.ts @@ -0,0 +1,116 @@ +import bs58check from "bs58check"; +import { Address } from "../../address/index.js"; +import { bytesConcat, bytesFrom } from "../../bytes/index.js"; +import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; +import { KnownScript } from "../../client/index.js"; +import { hexFrom } from "../../hex/index.js"; +import { numToBytes } from "../../num/index.js"; +import { Signer, SignerSignType, SignerType } from "../signer/index.js"; + +/** + * An abstract class extending the Signer class for Dogecoin-like signing operations. + * This class provides methods to get Doge account, public key, and internal address, + * as well as signing transactions. + * @public + */ +export abstract class SignerDoge extends Signer { + get type(): SignerType { + return SignerType.Doge; + } + + get signType(): SignerSignType { + return SignerSignType.DogeEcdsa; + } + + /** + * Gets the Doge address associated with the signer. + * + * @returns A promise that resolves to a string representing the Doge account. + */ + abstract getDogeAddress(): Promise; + + /** + * Gets the internal address, which is the Doge account in this case. + * + * @returns A promise that resolves to a string representing the internal address. + */ + async getInternalAddress(): Promise { + return this.getDogeAddress(); + } + + /** + * Gets the identity, which is the Doge address in this case. + * + * @returns A promise that resolves to a string representing the identity + */ + async getIdentity(): Promise { + return this.getDogeAddress(); + } + + /** + * Gets an array of Address objects representing the known script addresses for the signer. + * + * @returns A promise that resolves to an array of Address objects. + */ + async getAddressObjs(): Promise { + const hash = bs58check.decode(await this.getDogeAddress()).slice(1); + + return [ + await Address.fromKnownScript( + this.client, + KnownScript.OmniLock, + hexFrom([0x05, ...hash, 0x00]), + ), + ]; + } + + /** + * prepare a transaction before signing. This method is not implemented and should be overridden by subclasses. + * + * @param txLike - The transaction to prepare, represented as a TransactionLike object. + * @returns A promise that resolves to the prepared Transaction object. + */ + async prepareTransaction(txLike: TransactionLike): Promise { + const tx = Transaction.from(txLike); + const { script } = await this.getRecommendedAddressObj(); + await tx.addCellDepsOfKnownScripts(this.client, KnownScript.OmniLock); + await tx.prepareSighashAllWitness(script, 85, this.client); + return tx; + } + + /** + * Signs a transaction without modifying it. + * + * @param txLike - The transaction to sign, represented as a TransactionLike object. + * @returns A promise that resolves to a signed Transaction object. + */ + async signOnlyTransaction(txLike: TransactionLike): Promise { + const tx = Transaction.from(txLike); + const { script } = await this.getRecommendedAddressObj(); + const info = await tx.getSignHashInfo(script, this.client); + if (!info) { + return tx; + } + + const signature = bytesFrom( + await this.signMessageRaw(info.message.slice(2)), + "base64", + ); + signature[0] = 31 + ((signature[0] - 27) % 4); + + const witness = WitnessArgs.fromBytes(tx.witnesses[info.position]); + witness.lock = hexFrom( + bytesConcat( + numToBytes(5 * 4 + signature.length, 4), + numToBytes(4 * 4, 4), + numToBytes(5 * 4 + signature.length, 4), + numToBytes(5 * 4 + signature.length, 4), + numToBytes(signature.length, 4), + signature, + ), + ); + + tx.setWitnessArgsAt(info.position, witness); + return tx; + } +} diff --git a/packages/core/src/signer/doge/signerDogeAddressReadonly.ts b/packages/core/src/signer/doge/signerDogeAddressReadonly.ts new file mode 100644 index 00000000..dc7cc9d3 --- /dev/null +++ b/packages/core/src/signer/doge/signerDogeAddressReadonly.ts @@ -0,0 +1,52 @@ +import { Client } from "../../client/index.js"; +import { SignerDoge } from "./signerDoge.js"; + +/** + * A class extending SignerDoge that provides read-only access to a Doge address. + * This class does not support signing operations. + * @public + */ +export class SignerDogeAddressReadonly extends SignerDoge { + /** + * Creates an instance of SignerDogeAddressReadonly. + * + * @param client - The client instance used for communication. + * @param address - The Doge address with the signer. + */ + constructor( + client: Client, + private readonly address: string, + ) { + super(client); + } + + /** + * Connects to the client. This implementation does nothing as the class is read-only. + * + * @returns A promise that resolves when the connection is complete. + */ + async connect(): Promise {} + + /** + * Check if the signer is connected. + * + * @returns A promise that resolves the connection status. + */ + async isConnected(): Promise { + return true; + } + + /** + * Gets the Doge address associated with the signer. + * + * @returns A promise that resolves to a string representing the Doge address. + * + * @example + * ```typescript + * const account = await signer.getDogeAddress(); // Outputs the Doge address + * ``` + */ + async getDogeAddress(): Promise { + return this.address; + } +} diff --git a/packages/core/src/signer/doge/signerDogePrivateKey.ts b/packages/core/src/signer/doge/signerDogePrivateKey.ts new file mode 100644 index 00000000..b6d58ef8 --- /dev/null +++ b/packages/core/src/signer/doge/signerDogePrivateKey.ts @@ -0,0 +1,97 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { magicHash } from "bitcoinjs-message"; +import { + Bytes, + bytesConcat, + bytesFrom, + BytesLike, + bytesTo, +} from "../../bytes/index.js"; +import { Client } from "../../client/index.js"; +import { Hex, hexFrom } from "../../hex/index.js"; +import { btcP2pkhAddressFromPublicKey } from "../btc/verify.js"; +import { SignerDoge } from "./signerDoge.js"; + +/** + * A class extending SignerDoge that provides access to a Doge address. + * @public + */ +export class SignerDogePrivateKey extends SignerDoge { + private readonly privateKey: Bytes; + + /** + * Creates an instance of SignerDogePrivateKey + * + * @param client - The client instance used for communication. + * @param privateKey - The Doge private key with the signer. + */ + constructor( + client: Client, + privateKey: BytesLike, + public readonly dogeNetwork = 0x1e, + ) { + super(client); + this.privateKey = bytesFrom(privateKey); + if (this.privateKey.length !== 32) { + throw new Error("Private key must be 32 bytes!"); + } + } + + /** + * Connects to the client. This implementation does nothing as the class is always connected. + * + * @returns A promise that resolves when the connection is complete. + */ + async connect(): Promise {} + + /** + * Check if the signer is connected. + * + * @returns A promise that resolves the connection status. + */ + async isConnected(): Promise { + return true; + } + + async getDogePublicKey(): Promise { + return hexFrom(secp256k1.getPublicKey(this.privateKey, true)); + } + + /** + * Gets the Doge address associated with the signer. + * + * @returns A promise that resolves to a string representing the Doge address. + * + * @example + * ```typescript + * const account = await signer.getDogeAddress(); // Outputs the Doge address + * ``` + */ + async getDogeAddress(): Promise { + return btcP2pkhAddressFromPublicKey( + await this.getDogePublicKey(), + this.dogeNetwork, + ); + } + + /** + * Signs a message and returns signature only. + * + * @param msg - The message to sign, as a string or BytesLike object. + * @returns A promise that resolves to the signature as a string. + * @throws Will throw an error if not implemented. + */ + async signMessageRaw(msg: string | BytesLike): Promise { + const challenge = typeof msg === "string" ? msg : hexFrom(msg).slice(2); + + const signature = secp256k1.sign( + magicHash(challenge, "\x19Dogecoin Signed Message:\n"), + this.privateKey, + ); + + return bytesTo( + bytesConcat([31 + signature.recovery], signature.toCompactRawBytes()), + "base64", + ); + } +} diff --git a/packages/core/src/signer/doge/verify.ts b/packages/core/src/signer/doge/verify.ts new file mode 100644 index 00000000..96454a8a --- /dev/null +++ b/packages/core/src/signer/doge/verify.ts @@ -0,0 +1,38 @@ +import { secp256k1 } from "@noble/curves/secp256k1"; +import { magicHash } from "bitcoinjs-message"; +import { bytesFrom, BytesLike } from "../../bytes/index.js"; +import { hexFrom } from "../../hex/index.js"; +import { + btcEcdsaPublicKeyHash, + btcPublicKeyFromP2pkhAddress, +} from "../btc/verify.js"; + +/** + * @public + */ +export function verifyMessageDogeEcdsa( + message: string | BytesLike, + signature: string, + address: string, +): boolean { + const challenge = + typeof message === "string" ? message : hexFrom(message).slice(2); + const [recoveryBit, ...rawSign] = bytesFrom(signature, "base64"); + + const sig = secp256k1.Signature.fromCompact( + hexFrom(rawSign).slice(2), + ).addRecoveryBit(recoveryBit - 31); + + return ( + btcPublicKeyFromP2pkhAddress(address) === + hexFrom( + btcEcdsaPublicKeyHash( + sig + .recoverPublicKey( + magicHash(challenge, "\x19Dogecoin Signed Message:\n"), + ) + .toHex(), + ), + ) + ); +} diff --git a/packages/core/src/signer/index.ts b/packages/core/src/signer/index.ts index 43d01362..350b487e 100644 --- a/packages/core/src/signer/index.ts +++ b/packages/core/src/signer/index.ts @@ -1,5 +1,6 @@ export * from "./btc/index.js"; export * from "./ckb/index.js"; +export * from "./doge/index.js"; export * from "./dummy/index.js"; export * from "./evm/index.js"; export * from "./nostr/index.js"; diff --git a/packages/core/src/signer/nostr/index.ts b/packages/core/src/signer/nostr/index.ts index 88648d63..d5cace5d 100644 --- a/packages/core/src/signer/nostr/index.ts +++ b/packages/core/src/signer/nostr/index.ts @@ -1 +1,2 @@ export * from "./signerNostr.js"; +export * from "./verify.js"; diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index 7f94e731..fdc372a3 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -12,6 +12,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 { verifyMessageDogeEcdsa } from "../doge/verify.js"; import { verifyMessageEvmPersonal } from "../evm/verify.js"; import { verifyMessageNostrEvent } from "../nostr/verify.js"; @@ -25,6 +26,7 @@ export enum SignerSignType { JoyId = "JoyId", NostrEvent = "NostrEvent", CkbSecp256k1 = "CkbSecp256k1", + DogeEcdsa = "DogeEcdsa", } /** @@ -36,6 +38,7 @@ export enum SignerType { BTC = "BTC", CKB = "CKB", Nostr = "Nostr", + Doge = "Doge", } /** @@ -141,6 +144,12 @@ export abstract class Signer { signature.signature, signature.identity, ); + case SignerSignType.DogeEcdsa: + return verifyMessageDogeEcdsa( + message, + signature.signature, + signature.identity, + ); case SignerSignType.Unknown: throw new Error("Unknown signer sign type"); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58c54904..5ed38cb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,6 +237,9 @@ importers: bitcoinjs-message: specifier: ^2.2.0 version: 2.2.0 + bs58check: + specifier: ^4.0.0 + version: 4.0.0 buffer: specifier: ^6.0.3 version: 6.0.3 @@ -2296,6 +2299,9 @@ packages: base-x@4.0.0: resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} + base-x@5.0.0: + resolution: {integrity: sha512-sMW3VGSX1QWVFA6l8U62MLKz29rRfpTlYdCqLdpLo1/Yd4zZwSbnUaDfciIAowAqvq7YFnWq9hrhdg1KYgc1lQ==} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -2370,9 +2376,15 @@ packages: bs58@5.0.0: resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + bs58check@2.1.2: resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} + bs58check@4.0.0: + resolution: {integrity: sha512-FsGDOnFg9aVI9erdriULkd/JjEWONV/lQE5aYziB5PoBsXRind56lh8doIZIc9X4HoxT5x4bLjMWN1/NB8Zp5g==} + bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} @@ -7507,6 +7519,8 @@ snapshots: base-x@4.0.0: {} + base-x@5.0.0: {} + base64-js@1.5.1: {} bech32@1.1.4: {} @@ -7614,12 +7628,21 @@ snapshots: dependencies: base-x: 4.0.0 + bs58@6.0.0: + dependencies: + base-x: 5.0.0 + bs58check@2.1.2: dependencies: bs58: 4.0.1 create-hash: 1.2.0 safe-buffer: 5.2.1 + bs58check@4.0.0: + dependencies: + '@noble/hashes': 1.4.0 + bs58: 6.0.0 + bser@2.1.1: dependencies: node-int64: 0.4.0 @@ -8202,7 +8225,7 @@ snapshots: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) @@ -8229,12 +8252,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -8246,14 +8269,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.5.4) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -8267,7 +8290,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 From d2388a1e0935035383464b6e82d27720a7b9184a Mon Sep 17 00:00:00 2001 From: "son.pham@nexm.io" Date: Wed, 11 Dec 2024 08:54:47 +0700 Subject: [PATCH 2/2] feat: add doge signer to utxo global --- .changeset/pre.json | 2 +- .changeset/pretty-coins-explain.md | 8 ++ packages/ccc/src/signersController.ts | 10 ++ packages/core/src/client/client.ts | 2 +- packages/utxo-global/src/doge/index.ts | 114 +++++++++++++++++++++ packages/utxo-global/src/signersFactory.ts | 10 ++ 6 files changed, 144 insertions(+), 2 deletions(-) create mode 100644 .changeset/pretty-coins-explain.md create mode 100644 packages/utxo-global/src/doge/index.ts diff --git a/.changeset/pre.json b/.changeset/pre.json index 9f9ea579..1483a616 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "alpha", "initialVersions": { "@ckb-ccc/ccc": "0.0.15", diff --git a/.changeset/pretty-coins-explain.md b/.changeset/pretty-coins-explain.md new file mode 100644 index 00000000..48136da1 --- /dev/null +++ b/.changeset/pretty-coins-explain.md @@ -0,0 +1,8 @@ +--- +"@ckb-ccc/utxo-global": patch +"@ckb-ccc/connector": patch +"@ckb-ccc/core": patch +"@ckb-ccc/ccc": patch +--- + +feat: support doge signer diff --git a/packages/ccc/src/signersController.ts b/packages/ccc/src/signersController.ts index 298c62b5..55171e33 100644 --- a/packages/ccc/src/signersController.ts +++ b/packages/ccc/src/signersController.ts @@ -72,6 +72,16 @@ export class SignersController { signerType: ccc.SignerType.BTC, network: "btcTestnet", }, + { + addressPrefix: "ckb", + signerType: ccc.SignerType.Doge, + network: "doge", + }, + { + addressPrefix: "ckt", + signerType: ccc.SignerType.Doge, + network: "dogeTestnet", + }, ]; return { diff --git a/packages/core/src/client/client.ts b/packages/core/src/client/client.ts index f72592d0..14b70f98 100644 --- a/packages/core/src/client/client.ts +++ b/packages/core/src/client/client.ts @@ -517,7 +517,7 @@ export abstract class Client { async waitTransaction( txHash: HexLike, confirmations: number = 0, - timeout: number = 30000, + timeout: number = 60000, interval: number = 2000, ): Promise { const startTime = Date.now(); diff --git a/packages/utxo-global/src/doge/index.ts b/packages/utxo-global/src/doge/index.ts new file mode 100644 index 00000000..2aa496c3 --- /dev/null +++ b/packages/utxo-global/src/doge/index.ts @@ -0,0 +1,114 @@ +import { ccc } from "@ckb-ccc/core"; +import { Provider } from "../advancedBarrel.js"; + +/** + * @public + */ +export class SignerDoge extends ccc.SignerDoge { + private accountCache: string | undefined; + + constructor( + client: ccc.Client, + public readonly provider: Provider, + private readonly preferredNetworks: ccc.NetworkPreference[] = [ + { + addressPrefix: "ckb", + signerType: ccc.SignerType.Doge, + network: "doge", + }, + { + addressPrefix: "ckt", + signerType: ccc.SignerType.Doge, + network: "dogeTestnet", + }, + ], + ) { + super(client); + } + + async getDogeAddress(): Promise { + const accounts = await this.provider.getAccount(); + this.accountCache = accounts[0]; + return this.accountCache; + } + + /** + * Ensure the BTC network is the same as CKB network. + */ + async ensureNetwork(): Promise { + const network = await this._getNetworkToChange(); + if (!network) { + return; + } + + const chain = { + doge: "dogecoin", + dogeTestnet: "dogecoin_testnet", + }[network]; + + if (chain) { + await this.provider.switchNetwork(chain); + return; + } + + throw new Error( + `UTXO Global Doge wallet doesn't support the requested chain ${network}`, + ); + } + + async _getNetworkToChange(): Promise { + const currentNetwork = { + dogecoin: "doge", + dogecoin_testnet: "dogeTestnet", + }[await this.provider.getNetwork()]; + + const { network } = this.matchNetworkPreference( + this.preferredNetworks, + currentNetwork, + ) ?? { network: currentNetwork }; + if (network === currentNetwork) { + return; + } + + return network; + } + + onReplaced(listener: () => void): () => void { + const stop: (() => void)[] = []; + const replacer = async () => { + listener(); + stop[0]?.(); + }; + stop.push(() => { + this.provider.removeListener("accountsChanged", replacer); + this.provider.removeListener("networkChanged", replacer); + }); + + this.provider.on("accountsChanged", replacer); + this.provider.on("networkChanged", replacer); + + return stop[0]; + } + + async connect(): Promise { + await this.provider.connect(); + await this.ensureNetwork(); + } + + async isConnected(): Promise { + if ((await this._getNetworkToChange()) !== undefined) { + return false; + } + + return await this.provider.isConnected(); + } + + async signMessageRaw(message: string | ccc.BytesLike): Promise { + const challenge = + typeof message === "string" ? message : ccc.hexFrom(message).slice(2); + return this.provider.signMessage( + challenge, + this.accountCache ?? (await this.getDogeAddress()), + ); + } +} diff --git a/packages/utxo-global/src/signersFactory.ts b/packages/utxo-global/src/signersFactory.ts index 120ceeac..c9700d1b 100644 --- a/packages/utxo-global/src/signersFactory.ts +++ b/packages/utxo-global/src/signersFactory.ts @@ -2,6 +2,7 @@ import { ccc } from "@ckb-ccc/core"; import { Provider } from "./advancedBarrel.js"; import { SignerBtc } from "./btc/index.js"; import { SignerCkb } from "./ckb/index.js"; +import { SignerDoge } from "./doge/index.js"; /** * @public @@ -14,6 +15,7 @@ export function getUtxoGlobalSigners( utxoGlobal?: { bitcoinSigner: Provider; ckbSigner: Provider; + dogeSigner: Provider; }; }; @@ -34,5 +36,13 @@ export function getUtxoGlobalSigners( preferredNetworks, ), }, + { + name: "DOGE", + signer: new SignerDoge( + client, + windowRef.utxoGlobal.dogeSigner, + preferredNetworks, + ), + }, ]; }