cosmos-connect-core 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,7 @@ export declare class CosmostationWallet implements WalletAdapter {
34
34
  installed(): boolean;
35
35
  getUri(): string;
36
36
  onUpdate(callback: () => void): void;
37
+ connectViaWalletConnect(chain: Chain): Promise<Account>;
37
38
  connect(chain: Chain): Promise<Account>;
38
39
  disconnect(): Promise<void>;
39
40
  signTx(_bytes: Uint8Array): Promise<Uint8Array>;
@@ -37,6 +37,11 @@ export class CosmostationWallet {
37
37
  onUpdate(callback) {
38
38
  this.wc?.onUpdate(callback);
39
39
  }
40
+ async connectViaWalletConnect(chain) {
41
+ if (!this.wc)
42
+ throw new Error('WalletConnect not initialized for Cosmostation');
43
+ return this.wc.connect(chain);
44
+ }
40
45
  async connect(chain) {
41
46
  const cosmostation = window.cosmostation?.providers.keplr;
42
47
  if (!cosmostation) {
@@ -29,6 +29,7 @@ export declare class GalaxyStationWallet implements WalletAdapter {
29
29
  installed(): boolean;
30
30
  getUri(): string;
31
31
  onUpdate(callback: () => void): void;
32
+ connectViaWalletConnect(chain: Chain): Promise<Account>;
32
33
  connect(chain: Chain): Promise<Account>;
33
34
  disconnect(): Promise<void>;
34
35
  signTx(_bytes: Uint8Array): Promise<Uint8Array>;
@@ -44,6 +44,11 @@ export class GalaxyStationWallet {
44
44
  onUpdate(callback) {
45
45
  this.wc?.onUpdate(callback);
46
46
  }
47
+ async connectViaWalletConnect(chain) {
48
+ if (!this.wc)
49
+ throw new Error('WalletConnect not initialized for Galaxy Station');
50
+ return this.wc.connect(chain);
51
+ }
47
52
  async connect(chain) {
48
53
  const galaxyStation = window.galaxyStation;
49
54
  if (!galaxyStation) {
@@ -30,6 +30,7 @@ export declare class KeplrWallet implements WalletAdapter {
30
30
  installed(): boolean;
31
31
  getUri(): string;
32
32
  onUpdate(callback: () => void): void;
33
+ connectViaWalletConnect(chain: Chain): Promise<Account>;
33
34
  connect(chain: Chain): Promise<Account>;
34
35
  disconnect(): Promise<void>;
35
36
  signTx(_bytes: Uint8Array): Promise<Uint8Array>;
@@ -37,6 +37,11 @@ export class KeplrWallet {
37
37
  onUpdate(callback) {
38
38
  this.wc?.onUpdate(callback);
39
39
  }
40
+ async connectViaWalletConnect(chain) {
41
+ if (!this.wc)
42
+ throw new Error('WalletConnect not initialized for Keplr');
43
+ return this.wc.connect(chain);
44
+ }
40
45
  async connect(chain) {
41
46
  const keplr = window.keplr;
42
47
  if (!keplr) {
@@ -0,0 +1,48 @@
1
+ import { WalletAdapter, Chain, Account } from '../core/types.js';
2
+ export interface KeystoneWalletOptions {
3
+ /**
4
+ * Callback to show the QR scanner UI for reading CryptoMultiAccounts
5
+ * from the Keystone device (account sync).
6
+ * Must return the decoded UR data from the scanned QR code.
7
+ */
8
+ onScanAccountQR?: () => Promise<{
9
+ xfp: string;
10
+ keys: KeystoneKey[];
11
+ }>;
12
+ /**
13
+ * Callback to show the animated QR code for the Keystone device to scan,
14
+ * and then read the signed result QR back via webcam.
15
+ * @param ur - The UR-encoded sign request to display
16
+ * @returns The parsed signature from the scanned response QR
17
+ */
18
+ onSignQR?: (ur: {
19
+ type: string;
20
+ cbor: string;
21
+ }) => Promise<{
22
+ signature: string;
23
+ publicKey: string;
24
+ }>;
25
+ }
26
+ export interface KeystoneKey {
27
+ hdPath: string;
28
+ pubKey: Uint8Array;
29
+ index: number;
30
+ }
31
+ export declare class KeystoneWallet implements WalletAdapter {
32
+ id: string;
33
+ name: string;
34
+ icon: string;
35
+ private xfp;
36
+ private keys;
37
+ private currentAddress;
38
+ private currentPubKey;
39
+ private onScanAccountQR?;
40
+ private onSignQR?;
41
+ private _updateCallback?;
42
+ constructor(options?: KeystoneWalletOptions);
43
+ installed(): boolean;
44
+ onUpdate(callback: () => void): void;
45
+ connect(chain: Chain): Promise<Account>;
46
+ disconnect(): Promise<void>;
47
+ signTx(bytes: Uint8Array): Promise<Uint8Array>;
48
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Derive a Cosmos bech32 address from a compressed secp256k1 public key.
3
+ * Uses ripemd160(sha256(pubkey)) → bech32.encode(prefix, words)
4
+ */
5
+ async function pubKeyToAddress(compressedPubKey, prefix) {
6
+ // Use SubtleCrypto for SHA-256
7
+ const buf = new ArrayBuffer(compressedPubKey.byteLength);
8
+ new Uint8Array(buf).set(compressedPubKey);
9
+ const sha256Hash = new Uint8Array(await crypto.subtle.digest('SHA-256', buf));
10
+ // Manually compute RIPEMD-160 is complex — we'll dynamically import if available
11
+ // or use a lightweight approach
12
+ try {
13
+ const { ripemd160 } = await import('@noble/hashes/ripemd160');
14
+ const hash = ripemd160(sha256Hash);
15
+ const { bech32 } = await import('bech32');
16
+ return bech32.encode(prefix, bech32.toWords(hash));
17
+ }
18
+ catch {
19
+ // Fallback: try @cosmjs/crypto
20
+ try {
21
+ const cosmCrypto = await import('@cosmjs/crypto');
22
+ const cosmEncoding = await import('@cosmjs/encoding');
23
+ const sha = cosmCrypto.sha256(compressedPubKey);
24
+ const ripe = cosmCrypto.ripemd160(sha);
25
+ return cosmEncoding.toBech32(prefix, ripe);
26
+ }
27
+ catch {
28
+ throw new Error('Cannot derive address: install @noble/hashes + bech32, or @cosmjs/crypto + @cosmjs/encoding');
29
+ }
30
+ }
31
+ }
32
+ export class KeystoneWallet {
33
+ id = 'keystone';
34
+ name = 'Keystone';
35
+ icon = 'https://keyst.one/favicon.ico';
36
+ xfp = '';
37
+ keys = [];
38
+ currentAddress = '';
39
+ currentPubKey = new Uint8Array();
40
+ onScanAccountQR;
41
+ onSignQR;
42
+ _updateCallback;
43
+ constructor(options) {
44
+ this.onScanAccountQR = options?.onScanAccountQR;
45
+ this.onSignQR = options?.onSignQR;
46
+ }
47
+ installed() {
48
+ // Keystone is always available — it uses QR codes, no extension needed.
49
+ // Only requires a camera for scanning.
50
+ return (typeof navigator !== 'undefined' &&
51
+ typeof navigator.mediaDevices !== 'undefined');
52
+ }
53
+ onUpdate(callback) {
54
+ this._updateCallback = callback;
55
+ }
56
+ async connect(chain) {
57
+ // If we don't have keys yet, trigger the QR scan flow
58
+ if (this.keys.length === 0) {
59
+ if (!this.onScanAccountQR) {
60
+ throw new Error('Keystone requires onScanAccountQR callback to read account data from the device.');
61
+ }
62
+ const result = await this.onScanAccountQR();
63
+ this.xfp = result.xfp;
64
+ this.keys = result.keys;
65
+ }
66
+ if (this.keys.length === 0) {
67
+ throw new Error('No keys found from Keystone device.');
68
+ }
69
+ // Use the first key to derive the address for this chain's bech32 prefix
70
+ const key = this.keys[0];
71
+ this.currentPubKey = key.pubKey;
72
+ this.currentAddress = await pubKeyToAddress(key.pubKey, chain.bech32Prefix);
73
+ return {
74
+ address: this.currentAddress,
75
+ pubKey: this.currentPubKey,
76
+ algo: 'secp256k1',
77
+ name: 'Keystone',
78
+ };
79
+ }
80
+ async disconnect() {
81
+ this.xfp = '';
82
+ this.keys = [];
83
+ this.currentAddress = '';
84
+ this.currentPubKey = new Uint8Array();
85
+ }
86
+ async signTx(bytes) {
87
+ if (!this.onSignQR) {
88
+ throw new Error('Keystone requires onSignQR callback for transaction signing.');
89
+ }
90
+ if (this.keys.length === 0) {
91
+ throw new Error('Keystone not connected.');
92
+ }
93
+ try {
94
+ const { KeystoneCosmosSDK } = await import('@keystonehq/keystone-sdk');
95
+ const sdk = new KeystoneCosmosSDK();
96
+ // Generate the sign request UR
97
+ const signDataHex = Buffer.from(bytes).toString('hex');
98
+ const requestId = crypto.randomUUID();
99
+ const ur = sdk.generateSignRequest({
100
+ requestId,
101
+ signData: signDataHex,
102
+ dataType: 1,
103
+ accounts: [
104
+ {
105
+ path: this.keys[0].hdPath,
106
+ xfp: this.xfp,
107
+ address: this.currentAddress,
108
+ },
109
+ ],
110
+ });
111
+ // Show QR and wait for user to scan back the signed result
112
+ const result = await this.onSignQR(ur);
113
+ // Convert hex signature to bytes
114
+ const sigBytes = new Uint8Array(result.signature.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
115
+ return sigBytes;
116
+ }
117
+ catch (error) {
118
+ throw new Error(`Keystone signing failed: ${error.message}`);
119
+ }
120
+ }
121
+ }
@@ -28,6 +28,7 @@ export declare class LeapWallet implements WalletAdapter {
28
28
  installed(): boolean;
29
29
  getUri(): string;
30
30
  onUpdate(callback: () => void): void;
31
+ connectViaWalletConnect(chain: Chain): Promise<Account>;
31
32
  connect(chain: Chain): Promise<Account>;
32
33
  disconnect(): Promise<void>;
33
34
  signTx(_bytes: Uint8Array): Promise<Uint8Array>;
@@ -37,6 +37,11 @@ export class LeapWallet {
37
37
  onUpdate(callback) {
38
38
  this.wc?.onUpdate(callback);
39
39
  }
40
+ async connectViaWalletConnect(chain) {
41
+ if (!this.wc)
42
+ throw new Error('WalletConnect not initialized for Leap');
43
+ return this.wc.connect(chain);
44
+ }
40
45
  async connect(chain) {
41
46
  const leap = window.leap;
42
47
  if (!leap) {
@@ -0,0 +1,30 @@
1
+ import { WalletAdapter, Chain, Account } from '../core/types.js';
2
+ export interface LedgerWalletOptions {
3
+ /** Override coin type for specific chains */
4
+ coinTypePaths?: Record<string, number>;
5
+ }
6
+ export declare class LedgerWallet implements WalletAdapter {
7
+ id: string;
8
+ name: string;
9
+ icon: string;
10
+ private transport;
11
+ private signer;
12
+ private coinTypePaths;
13
+ constructor(options?: LedgerWalletOptions);
14
+ installed(): boolean;
15
+ connect(chain: Chain): Promise<Account>;
16
+ disconnect(): Promise<void>;
17
+ signTx(bytes: Uint8Array): Promise<Uint8Array>;
18
+ /**
19
+ * Get the CosmJS-compatible offline signer for use with SigningStargateClient.
20
+ * This is the recommended way to sign transactions with Ledger.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const signer = ledgerWallet.getOfflineSigner();
25
+ * const client = await SigningStargateClient.connectWithSigner(rpcUrl, signer);
26
+ * await client.sendTokens(sender, recipient, amount, fee);
27
+ * ```
28
+ */
29
+ getOfflineSigner(): any;
30
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * HD derivation paths by coin type.
3
+ * Most Cosmos chains use coin_type 118; some have their own.
4
+ */
5
+ const COIN_TYPE_PATHS = {
6
+ 'columbus-5': 330,
7
+ 'phoenix-1': 330,
8
+ 'rebel-2': 330,
9
+ 'secret-4': 529,
10
+ 'injective-1': 60,
11
+ 'evmos_9001-2': 60, // Evmos
12
+ };
13
+ const DEFAULT_COIN_TYPE = 118;
14
+ function makeHdPath(coinType, account = 0) {
15
+ // BIP44: m/44'/coinType'/account'/0/0
16
+ return [44, coinType, account, 0, 0];
17
+ }
18
+ export class LedgerWallet {
19
+ id = 'ledger';
20
+ name = 'Ledger';
21
+ icon = 'https://raw.githubusercontent.com/AstroportFinance/astroport-token-lists/main/logos/ledger.svg';
22
+ transport = null;
23
+ signer = null;
24
+ coinTypePaths;
25
+ constructor(options) {
26
+ this.coinTypePaths = {
27
+ ...COIN_TYPE_PATHS,
28
+ ...options?.coinTypePaths,
29
+ };
30
+ }
31
+ installed() {
32
+ // Ledger is available if browser supports WebHID
33
+ return typeof navigator !== 'undefined' && !!navigator?.hid;
34
+ }
35
+ async connect(chain) {
36
+ // Dynamically import Ledger packages to avoid bundling them for non-Ledger users
37
+ const [{ default: TransportWebHID }, { LedgerSigner }, { stringToPath }] = await Promise.all([
38
+ import('@ledgerhq/hw-transport-webhid'),
39
+ import('@cosmjs/ledger-amino'),
40
+ import('@cosmjs/crypto'),
41
+ ]);
42
+ // Check WebHID support
43
+ if (!(await TransportWebHID.isSupported())) {
44
+ // Try WebUSB as fallback
45
+ try {
46
+ const { default: TransportWebUSB } = await import('@ledgerhq/hw-transport-webusb');
47
+ if (await TransportWebUSB.isSupported()) {
48
+ this.transport = await TransportWebUSB.create();
49
+ }
50
+ else {
51
+ throw new Error('No Ledger transport available. Use Chrome or a Chromium-based browser.');
52
+ }
53
+ }
54
+ catch {
55
+ throw new Error('No Ledger transport available. Use Chrome or a Chromium-based browser.');
56
+ }
57
+ }
58
+ else {
59
+ this.transport = await TransportWebHID.create();
60
+ }
61
+ const coinType = this.coinTypePaths[chain.chainId] ?? DEFAULT_COIN_TYPE;
62
+ const hdPath = stringToPath(`m/44'/${coinType}'/0'/0/0`);
63
+ this.signer = new LedgerSigner(this.transport, {
64
+ hdPaths: [hdPath],
65
+ prefix: chain.bech32Prefix,
66
+ });
67
+ const accounts = await this.signer.getAccounts();
68
+ if (!accounts || accounts.length === 0) {
69
+ throw new Error('No accounts found on Ledger. Make sure the Cosmos app is open.');
70
+ }
71
+ const acc = accounts[0];
72
+ return {
73
+ address: acc.address,
74
+ pubKey: acc.pubkey,
75
+ algo: acc.algo || 'secp256k1',
76
+ name: 'Ledger',
77
+ isNanoLedger: true,
78
+ };
79
+ }
80
+ async disconnect() {
81
+ if (this.transport) {
82
+ try {
83
+ await this.transport.close();
84
+ }
85
+ catch {
86
+ // Ignore close errors
87
+ }
88
+ }
89
+ this.transport = null;
90
+ this.signer = null;
91
+ }
92
+ async signTx(bytes) {
93
+ if (!this.signer) {
94
+ throw new Error('Ledger not connected');
95
+ }
96
+ // Ledger uses Amino signing — the bytes should be an Amino-encoded SignDoc.
97
+ // For full tx signing, use getOfflineSigner() with SigningStargateClient.
98
+ throw new Error('Ledger signTx requires Amino signing. Use getOfflineSigner() with SigningStargateClient for full tx support.');
99
+ }
100
+ /**
101
+ * Get the CosmJS-compatible offline signer for use with SigningStargateClient.
102
+ * This is the recommended way to sign transactions with Ledger.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const signer = ledgerWallet.getOfflineSigner();
107
+ * const client = await SigningStargateClient.connectWithSigner(rpcUrl, signer);
108
+ * await client.sendTokens(sender, recipient, amount, fee);
109
+ * ```
110
+ */
111
+ getOfflineSigner() {
112
+ if (!this.signer) {
113
+ throw new Error('Ledger not connected. Call connect() first.');
114
+ }
115
+ return this.signer;
116
+ }
117
+ }
@@ -29,6 +29,7 @@ export declare class StationWallet implements WalletAdapter {
29
29
  installed(): boolean;
30
30
  getUri(): string;
31
31
  onUpdate(callback: () => void): void;
32
+ connectViaWalletConnect(chain: Chain): Promise<Account>;
32
33
  connect(chain: Chain): Promise<Account>;
33
34
  disconnect(): Promise<void>;
34
35
  signTx(_bytes: Uint8Array): Promise<Uint8Array>;
@@ -47,6 +47,11 @@ export class StationWallet {
47
47
  onUpdate(callback) {
48
48
  this.wc?.onUpdate(callback);
49
49
  }
50
+ async connectViaWalletConnect(chain) {
51
+ if (!this.wc)
52
+ throw new Error('WalletConnect not initialized for Station');
53
+ return this.wc.connect(chain);
54
+ }
50
55
  async connect(chain) {
51
56
  const station = window.station;
52
57
  if (!station) {
@@ -17,7 +17,7 @@ export class WalletConnectWallet {
17
17
  this.icon = icon;
18
18
  const details = mobileAppDetails || {
19
19
  name: 'Cosmos Connect',
20
- android: 'intent://wcV2#Intent;package=com.chainapsis.keplr;scheme=keplrwallet;end;', // Default to Keplr for android
20
+ android: 'intent://wcV2#Intent;package=com.chainapsis.keplr;scheme=keplrwallet;end;',
21
21
  ios: 'keplrwallet://wcV2', // Default to Keplr for ios
22
22
  };
23
23
  const metadata = signerMetadata || {
@@ -127,7 +127,7 @@ export class WalletConnectV2 {
127
127
  console.log(`WalletConnectV2: URI generated (attempt ${attempt + 1}/${maxRetries})`, uri);
128
128
  this._uri = uri;
129
129
  this.onUriCbs.forEach((cb) => cb(uri));
130
- console.log('WalletConnectV2: Waiting for approval...');
130
+ console.log(`WalletConnectV2: Waiting for approval (timeout: ${pairingTimeout}ms)...`);
131
131
  try {
132
132
  const approvalPromise = approval();
133
133
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Connection approval timed out')), pairingTimeout));
@@ -29,7 +29,7 @@ export function createClient(config) {
29
29
  function getWallet(walletId) {
30
30
  return wallets.find((w) => w.id === walletId);
31
31
  }
32
- async function connect(walletId, chainId) {
32
+ async function connect(walletId, chainId, options) {
33
33
  try {
34
34
  const chain = getChain(chainId);
35
35
  if (!chain)
@@ -41,7 +41,9 @@ export function createClient(config) {
41
41
  if (!wallet.installed() && !wallet.getUri) {
42
42
  throw new Error(`Wallet ${wallet.name} is not installed`);
43
43
  }
44
- const account = await wallet.connect(chain);
44
+ const account = options?.forceWalletConnect && wallet.connectViaWalletConnect
45
+ ? await wallet.connectViaWalletConnect(chain)
46
+ : await wallet.connect(chain);
45
47
  setState({
46
48
  currentChain: chain,
47
49
  currentWallet: wallet,
@@ -19,6 +19,7 @@ export interface WalletAdapter {
19
19
  icon?: string;
20
20
  installed(): boolean;
21
21
  connect(chain: Chain): Promise<Account>;
22
+ connectViaWalletConnect?(chain: Chain): Promise<Account>;
22
23
  disconnect(): Promise<void>;
23
24
  signTx(bytes: Uint8Array): Promise<Uint8Array>;
24
25
  signMsg?(msg: string): Promise<Uint8Array>;
@@ -47,7 +48,9 @@ export interface StorageAdapter {
47
48
  export type Listener<T> = (state: T) => void;
48
49
  export interface Client {
49
50
  readonly state: ClientState;
50
- connect(walletId: string, chainId: string): Promise<void>;
51
+ connect(walletId: string, chainId: string, options?: {
52
+ forceWalletConnect?: boolean;
53
+ }): Promise<void>;
51
54
  disconnect(): Promise<void>;
52
55
  signAndBroadcast(txBytes: Uint8Array): Promise<string>;
53
56
  subscribe(listener: Listener<ClientState>): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cosmos-connect-core",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Core SDK for Cosmos Connect",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",