From fadd13bc20ff67f4220d3c193d2073ae3ff2b142 Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 19 May 2026 11:16:40 +0100 Subject: [PATCH 1/8] fix(core)!: bs58 signature decode, drop sendTransaction alt overload, off-chain message prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P1 fixes to `@hermis/solana-headless-core`. 1. createTransactionSendingSignerFromWallet now bs58-decodes signature bytes instead of returning UTF-8 of the base58 string. Previously the returned 88-byte buffer of ASCII was never going to validate as a 64-byte Ed25519 signature for any downstream Kit consumer. 2. sendTransaction and signAndSendTransaction lose their 3-arg alternative form (transaction, wallet, options). The form silently created a hard-coded devnet Connection when the first argument looked like a transaction (`'instructions' in arg` etc.) — a Kit Rpc with an instructions field would route the call to devnet without telling the caller. The form was undocumented and unused by internal callers. 3. signMessage on local-key paths (Keypair, CryptoKeyPair, KeyPairSigner) now prepends the Solana off-chain message domain separator (0xff + "solana offchain" + version + format + length) before signing. Without it, a caller signing user-supplied bytes in a headless backend can be tricked into producing a valid transaction signature. External wallet adapters apply this header themselves; those paths are untouched. BREAKING CHANGE: signatures produced by in-SDK local-key signers no longer verify against the raw message bytes; verifiers must apply the same domain header. See docs/migration/v1-to-v2.mdx. BREAKING CHANGE: sendTransaction/signAndSendTransaction require the 4-arg form (connection, transaction, wallet, options). --- .../__tests__/kit-signers/factories.test.ts | 35 +++++ .../transaction/offchain-message.test.ts | 38 ++++++ packages/core/src/kit-signers/factories.ts | 7 +- packages/core/src/transaction/index.ts | 124 +++++------------- .../core/src/transaction/offchain-message.ts | 59 +++++++++ 5 files changed, 166 insertions(+), 97 deletions(-) create mode 100644 packages/core/__tests__/kit-signers/factories.test.ts create mode 100644 packages/core/__tests__/transaction/offchain-message.test.ts create mode 100644 packages/core/src/transaction/offchain-message.ts diff --git a/packages/core/__tests__/kit-signers/factories.test.ts b/packages/core/__tests__/kit-signers/factories.test.ts new file mode 100644 index 0000000..95ad01c --- /dev/null +++ b/packages/core/__tests__/kit-signers/factories.test.ts @@ -0,0 +1,35 @@ +import bs58 from 'bs58'; +import { createTransactionSendingSignerFromWallet } from '../../src/kit-signers/factories'; +import type { Address } from '@solana/kit'; + +describe('createTransactionSendingSignerFromWallet', () => { + const walletAddress = 'DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1' as Address; + const chain = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as const; + + it('returns 64-byte Ed25519 signature bytes (not UTF-8 of the base58 string)', async () => { + const knownSignatureBytes = new Uint8Array(64); + for (let i = 0; i < 64; i++) knownSignatureBytes[i] = i; + const knownSignatureBase58 = bs58.encode(knownSignatureBytes); + + const signer = createTransactionSendingSignerFromWallet( + walletAddress, + chain, + async () => knownSignatureBase58, + ); + + const [signatureBytes] = await signer.signAndSendTransactions([{}]); + + expect(signatureBytes.length).toBe(64); + expect(Array.from(signatureBytes)).toEqual(Array.from(knownSignatureBytes)); + }); + + it('rejects when more than one transaction is passed', async () => { + const signer = createTransactionSendingSignerFromWallet( + walletAddress, + chain, + async () => bs58.encode(new Uint8Array(64)), + ); + + await expect(signer.signAndSendTransactions([{}, {}])).rejects.toThrow(); + }); +}); diff --git a/packages/core/__tests__/transaction/offchain-message.test.ts b/packages/core/__tests__/transaction/offchain-message.test.ts new file mode 100644 index 0000000..39daeba --- /dev/null +++ b/packages/core/__tests__/transaction/offchain-message.test.ts @@ -0,0 +1,38 @@ +import { withOffchainMessagePrefix } from '../../src/transaction/offchain-message'; + +describe('withOffchainMessagePrefix', () => { + it('prepends the 0xff "solana offchain" domain separator', () => { + const out = withOffchainMessagePrefix(new Uint8Array([0xaa, 0xbb])); + + // 0xff || "solana offchain" || version(0) || format(1) || len(2 LE) || body + expect(Array.from(out.slice(0, 16))).toEqual([ + 0xff, + 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x20, // "solana " + 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, // "offchain" + ]); + expect(out[16]).toBe(0); // header version + expect(out[17]).toBe(1); // format: UTF-8 + expect(out[18]).toBe(2); // len LE low byte + expect(out[19]).toBe(0); // len LE high byte + expect(Array.from(out.slice(20))).toEqual([0xaa, 0xbb]); + }); + + it('produces bytes that cannot be confused with a Solana transaction', () => { + // Real Solana transactions start with a "number of required signatures" + // byte that's typically 1-8 and always less than 0x80. + const out = withOffchainMessagePrefix(new Uint8Array([1, 2, 3])); + expect(out[0]).toBe(0xff); + }); + + it('encodes message length as little-endian u16', () => { + const out = withOffchainMessagePrefix(new Uint8Array(0x0102)); + expect(out[18]).toBe(0x02); + expect(out[19]).toBe(0x01); + }); + + it('rejects messages longer than u16 max', () => { + expect(() => withOffchainMessagePrefix(new Uint8Array(0x10000))).toThrow( + /too large/i, + ); + }); +}); diff --git a/packages/core/src/kit-signers/factories.ts b/packages/core/src/kit-signers/factories.ts index fecfa4b..112dea6 100644 --- a/packages/core/src/kit-signers/factories.ts +++ b/packages/core/src/kit-signers/factories.ts @@ -6,6 +6,7 @@ */ import type { Address } from '@solana/kit' +import bs58 from 'bs58' import type { MessageModifyingSigner, TransactionSendingSigner, @@ -139,10 +140,8 @@ export function createTransactionSendingSignerFromWallet( // Send the transaction and get signature const signatureString = await sendTransactionFn(transaction) - // Convert base58 signature string to bytes - // For now, we'll keep it as Uint8Array from base58 decode - // In a full implementation, you'd use proper base58 decoder - const signatureBytes = new TextEncoder().encode(signatureString) as SignatureBytes + // Decode the base58 signature string into the 64-byte Ed25519 signature + const signatureBytes = bs58.decode(signatureString) as SignatureBytes return [signatureBytes] } diff --git a/packages/core/src/transaction/index.ts b/packages/core/src/transaction/index.ts index 0187308..7a7ea65 100644 --- a/packages/core/src/transaction/index.ts +++ b/packages/core/src/transaction/index.ts @@ -112,6 +112,7 @@ import { import * as ed25519 from '@noble/ed25519'; import bs58 from 'bs58'; import nacl from 'tweetnacl'; +import { withOffchainMessagePrefix } from './offchain-message.js'; // Import kit functions with correct names based on actual exports @@ -1161,21 +1162,7 @@ async function signAllTransactionsLegacy; - -/** - * Sends a transaction to the Solana network (supports both legacy and kit architectures) + * * @param connection The Solana connection to use (supports both Legacy Connection and Kit Rpc) * @param transaction The transaction to send * @param wallet The wallet to sign with (can be Keypair, Adapter, CryptoKeyPair, or Address) @@ -1186,44 +1173,9 @@ export async function sendTransaction( connection: DualConnection, transaction: DualTransaction, wallet: DualWallet, - options?: DualArchitectureOptions -): Promise; - -/** - * Implementation for sendTransaction with flexible parameter handling - */ -export async function sendTransaction( - connectionOrTransaction: DualConnection | DualTransaction, - transactionOrWallet?: DualTransaction | DualWallet, - walletOrOptions?: DualWallet | DualArchitectureOptions, - options: DualArchitectureOptions = {} + options: DualArchitectureOptions = {}, ): Promise { - let connection: DualConnection; - let transaction: DualTransaction; - let wallet: DualWallet; - let finalOptions: DualArchitectureOptions; - - // Parse parameters based on overload usage - // Check if first parameter is a connection (either legacy Connection or Kit Rpc) - // First check if it looks like a transaction - const looksLikeTransaction = typeof connectionOrTransaction === 'object' && connectionOrTransaction !== null && - ('recentBlockhash' in connectionOrTransaction || 'instructions' in connectionOrTransaction || 'message' in connectionOrTransaction); - - if (!looksLikeTransaction) { - // Standard usage: connection, transaction, wallet, options - connection = connectionOrTransaction as DualConnection; - transaction = transactionOrWallet as DualTransaction; - wallet = walletOrOptions as DualWallet; - finalOptions = options; - } else { - // Alternative usage: transaction, wallet, options (creates default connection) - transaction = connectionOrTransaction as DualTransaction; - wallet = transactionOrWallet as DualWallet; - finalOptions = (walletOrOptions as DualArchitectureOptions) || {}; - - // Create a default connection to devnet - connection = createConnection('https://api.devnet.solana.com'); - } + const finalOptions = options; try { // Handle kit architecture if (isKitWallet(wallet)) { @@ -1389,7 +1341,7 @@ async function sendTransactionLegacy( * @returns A promise that resolves to the signed message bytes */ export async function signMessage( - message: string | Uint8Array, + message: string | Uint8Array, wallet: DualWallet, options: DualArchitectureOptions = {} ): Promise { @@ -1401,23 +1353,27 @@ export async function signMessage( // Handle kit architecture if (isKitWallet(wallet)) { + // Kit local-key signers sign raw bytes — wrap with the off-chain message + // domain separator so the signature can't be re-used as a transaction + // signature. + const offchainBytes = withOffchainMessagePrefix(messageBytes); + if (isMessagePartialSigner(wallet)) { // It's already a MessagePartialSigner (KeyPairSigner) - return await signMessageKit(messageBytes, wallet); + return await signMessageKit(offchainBytes, wallet); } else if (isCryptoKeyPair(wallet)) { // Use kit's message signing capabilities const messageSigner = await convertCryptoKeyPairToMessageSigner(wallet); - return await signMessageKit(messageBytes, messageSigner); + return await signMessageKit(offchainBytes, messageSigner); } else if (typeof wallet === 'string') { // Address without private key - cannot sign throw new HermisError(HERMIS_ERROR__SIGNING__PRIVATE_KEY_UNAVAILABLE, {}); } - // else { - // throw new Error('Unsupported kit wallet type'); - // } } - // Everything else uses legacy implementation (Keypair, Adapter, Standard Wallet) + // Everything else uses legacy implementation (Keypair, Adapter, Standard Wallet). + // The Keypair branch inside signMessageLegacy applies the off-chain prefix + // itself; external wallets (Standard Wallet / Adapter) apply their own. return await signMessageLegacy(messageBytes, wallet as LegacyWallet); } catch (error) { console.error('Failed to sign message:', error); @@ -1692,11 +1648,13 @@ async function signMessageLegacy( messageBytes: Uint8Array, wallet: LegacyWallet ): Promise { - + // Handle different wallet types if ('secretKey' in wallet) { - // It's a Keypair - const signature = nacl.sign.detached(messageBytes, wallet.secretKey); + // It's a Keypair — local signing, wrap with the off-chain message domain + // separator so the signature is provably not a transaction signature. + const offchainBytes = withOffchainMessagePrefix(messageBytes); + const signature = nacl.sign.detached(offchainBytes, wallet.secretKey); return signature; } else if ("_connectedAccount" in wallet && "_wallet" in wallet) { // It's a Standard Wallet @@ -2034,40 +1992,20 @@ export async function signTransactionWithSigner( } /** - * Implementation for signAndSendTransaction with flexible parameter handling + * Sign and send a transaction (supports both legacy and kit architectures). + * + * @param connection The Solana connection to use (supports both legacy Connection and Kit Rpc) + * @param transaction The transaction to sign and send + * @param wallet The wallet to sign with (can be Keypair, Adapter, CryptoKeyPair, or Address) + * @param options Optional configuration for dual architecture behavior */ export async function signAndSendTransaction( - connectionOrTransaction: DualConnection | DualTransaction, - transactionOrWallet?: DualTransaction | DualWallet, - walletOrOptions?: DualWallet | DualArchitectureOptions, - options: DualArchitectureOptions = {} + connection: DualConnection, + transaction: DualTransaction, + wallet: DualWallet, + options: DualArchitectureOptions = {}, ): Promise { - let connection: DualConnection; - let transaction: DualTransaction; - let wallet: DualWallet; - let finalOptions: DualArchitectureOptions; - - // Parse parameters based on overload usage - // Check if first parameter is a connection (either legacy Connection or Kit Rpc) - // First check if it looks like a transaction - const looksLikeTransaction = typeof connectionOrTransaction === 'object' && connectionOrTransaction !== null && - ('recentBlockhash' in connectionOrTransaction || 'instructions' in connectionOrTransaction || 'message' in connectionOrTransaction); - - if (!looksLikeTransaction) { - // Standard usage: connection, transaction, wallet, options - connection = connectionOrTransaction as DualConnection; - transaction = transactionOrWallet as DualTransaction; - wallet = walletOrOptions as DualWallet; - finalOptions = options; - } else { - // Alternative usage: transaction, wallet, options (creates default connection) - transaction = connectionOrTransaction as DualTransaction; - wallet = transactionOrWallet as DualWallet; - finalOptions = (walletOrOptions as DualArchitectureOptions) || {}; - - // Create a default connection to devnet - connection = createConnection('https://api.devnet.solana.com'); - } + const finalOptions = options; try { // Handle kit architecture diff --git a/packages/core/src/transaction/offchain-message.ts b/packages/core/src/transaction/offchain-message.ts new file mode 100644 index 0000000..9bff576 --- /dev/null +++ b/packages/core/src/transaction/offchain-message.ts @@ -0,0 +1,59 @@ +/** + * Solana Off-Chain Message domain separator. + * + * Without a domain separator, raw `signMessage` output is structurally + * indistinguishable from a transaction signature — a caller can be tricked + * into producing a valid transaction signature by handing the signer bytes + * that happen to encode a Solana transaction message. + * + * Wallet adapters that implement Standard Wallet / wallet-adapter-base + * (Phantom, Backpack, Solflare, …) apply this prefix themselves. The in-SDK + * local-key signers (Keypair / CryptoKeyPair / KeyPairSigner) do not, so we + * wrap their inputs here. + * + * Layout (per Solana off-chain message spec): + * signing_domain: 0xff "solana offchain" (16 bytes) + * header_version: u8 (currently 0) + * format: u8 (1 = UTF-8) + * length: u16 little-endian + * message: variable + * + * The leading 0xff guarantees the buffer cannot be parsed as a Solana + * transaction (a real transaction's first byte is the number of required + * signatures, always < 0x80). + * + * @see https://github.com/solana-foundation/solana-improvement-documents/pull/48 + */ +const SIGNING_DOMAIN = new Uint8Array([ + 0xff, + 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x20, // "solana " + 0x6f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, // "offchain" +]); + +const HEADER_VERSION = 0; +const FORMAT_UTF8 = 1; +const HEADER_SIZE = SIGNING_DOMAIN.length + 1 + 1 + 2; // domain + version + format + len + +/** + * Wrap raw message bytes with the Solana off-chain message header so the + * resulting signature cannot be re-interpreted as a transaction signature. + * + * Verifiers must apply the same wrapping before checking the signature. + */ +export function withOffchainMessagePrefix(messageBytes: Uint8Array): Uint8Array { + if (messageBytes.length > 0xffff) { + throw new Error( + `Off-chain message too large: ${messageBytes.length} bytes (max ${0xffff})`, + ); + } + + const out = new Uint8Array(HEADER_SIZE + messageBytes.length); + out.set(SIGNING_DOMAIN, 0); + out[SIGNING_DOMAIN.length] = HEADER_VERSION; + out[SIGNING_DOMAIN.length + 1] = FORMAT_UTF8; + // length, little-endian u16 + out[SIGNING_DOMAIN.length + 2] = messageBytes.length & 0xff; + out[SIGNING_DOMAIN.length + 3] = (messageBytes.length >> 8) & 0xff; + out.set(messageBytes, HEADER_SIZE); + return out; +} From bff3d7f96bf060186ea5afee9843281ff06f594d Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 19 May 2026 11:20:37 +0100 Subject: [PATCH 2/8] fix!: require explicit network across adapter packages + perf hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes RPC-URL substring cluster inference (typosquat-friendly) and plumbs an explicit `network` argument end-to-end from HermisProvider through useStandardWalletAdapters into StandardWalletAdapter and the mobile adapter. Also bundles the related perf and correctness work in the same packages so a single review pass covers them: - StandardWalletAdapter / utils.ts no longer ship debug console.log calls that leaked unsigned tx bytes and wallet objects to the page console (extension-readable). Error reporting kept. - useStandardWalletAdapters and HermisProvider memoise the wallet set by name-joined key instead of `.length`, so swaps that keep the array length stable are no longer missed. - WalletContext.Provider value wrapped in useMemo so useWallet() consumers stop re-rendering on every parent render. - `wallet-standard:app-ready` re-dispatch loop reduced from 10×1s to a 4-shot backoff (100ms / 300ms / 1s / 3s). - useSolanaTransaction now stops polling on terminal status and uses exponential backoff (1s → 30s). Removed exports: - getInferredNetworkFromEndpoint (both adapter-base and wallet-standard) - detectClusterFromEndpoint (wallet-standard) New exports: - StandardWalletAdapter.setNetwork(network) - chain.ts: networkToChainId, SOLANA_*_CHAIN constants BREAKING CHANGE: getStandardWalletAdapters(adapters, endpoint, network) and createWalletConnectionManager(adapters, network, ...) now require an explicit `network` argument. StandardWalletAdapter.setRpcEndpoint no longer derives the cluster — call setNetwork(network) for that. See docs/migration/v1-to-v2.mdx. --- apps/sample-dapp/src/docs-validation-ui.ts | 4 +- .../__tests__/utils/environment.test.ts | 5 - packages/adapter-base/src/core/manager.ts | 12 +- .../adapter-base/src/utils/environment.ts | 17 -- .../src/components/ContextProvider.tsx | 10 +- .../src/components/HermisProvider.tsx | 34 ++-- .../src/hooks/useSolanaTransaction.ts | 44 ++++- .../src/hooks/useStandardWalletAdapters.ts | 39 ++-- .../src/providers/WalletProvider.tsx | 102 +++++++---- packages/wallet-standard/src/chain.ts | 26 +++ packages/wallet-standard/src/environment.ts | 17 -- packages/wallet-standard/src/utils.ts | 168 +++++++++--------- .../wallet-standard/src/wallet-adapter.ts | 45 ++--- .../wallet-standard/src/wallet-detection.ts | 47 ++--- 14 files changed, 326 insertions(+), 244 deletions(-) create mode 100644 packages/wallet-standard/src/chain.ts diff --git a/apps/sample-dapp/src/docs-validation-ui.ts b/apps/sample-dapp/src/docs-validation-ui.ts index 8c1c05d..1af184f 100644 --- a/apps/sample-dapp/src/docs-validation-ui.ts +++ b/apps/sample-dapp/src/docs-validation-ui.ts @@ -11,7 +11,7 @@ import { sortWalletAdapters, getAdaptersByReadyState } from '@hermis/solana-headless-adapter-base'; -import { Adapter, WalletReadyState } from '@hermis/solana-headless-core'; +import { Adapter, WalletAdapterNetwork, WalletReadyState } from '@hermis/solana-headless-core'; import { testBasicTransferWeb3, testBasicTransferKit, @@ -90,7 +90,7 @@ async function initializeApp() { async function initializeWalletManager() { try { // Single call - everything built-in with adapter management - manager = createWalletConnectionManager([], 'selectedWallet', 'https://api.devnet.solana.com'); + manager = createWalletConnectionManager([], WalletAdapterNetwork.Devnet, 'selectedWallet', 'https://api.devnet.solana.com'); // Subscribe to adapter changes (handles both initial load and dynamic updates) manager.onAdaptersChange((adapters) => { diff --git a/packages/adapter-base/__tests__/utils/environment.test.ts b/packages/adapter-base/__tests__/utils/environment.test.ts index a59cedb..f5572eb 100644 --- a/packages/adapter-base/__tests__/utils/environment.test.ts +++ b/packages/adapter-base/__tests__/utils/environment.test.ts @@ -114,11 +114,6 @@ namespace TestEnvironment { export const isMobileDevice = jest.fn((ua: string) => (ua.includes('iPhone') || ua.includes('Android')) && !ua.includes('wv)')); export const isIosAndRedirectable = jest.fn(() => true); - export const getInferredNetworkFromEndpoint = jest.fn((endpoint?: string) => { - if (endpoint?.includes('devnet')) return WalletAdapterNetwork.Devnet; - if (endpoint?.includes('testnet')) return WalletAdapterNetwork.Testnet; - return WalletAdapterNetwork.Mainnet; - }); export const getUriForAppIdentity = jest.fn(() => { if (typeof window === 'undefined') return undefined; diff --git a/packages/adapter-base/src/core/manager.ts b/packages/adapter-base/src/core/manager.ts index 2a2b8d0..de5b4df 100644 --- a/packages/adapter-base/src/core/manager.ts +++ b/packages/adapter-base/src/core/manager.ts @@ -1,4 +1,4 @@ -import { Adapter, WalletName, WalletReadyState, EventEmitter, WalletAdapterEvents } from '../types/index.js'; +import { Adapter, WalletName, WalletReadyState, EventEmitter, WalletAdapterEvents, WalletAdapterNetwork } from '../types/index.js'; import { PublicKey, Transaction, TransactionSignature } from '@solana/web3.js'; import { createLocalStorageUtility } from '../utils/storage.js'; import { addWalletAdapterEventListeners } from './adapters.js'; @@ -22,15 +22,21 @@ import { /** * Create a simple wallet connection manager with built-in adapter management + * + * The `network` parameter is required — wallet adapters now need an explicit + * cluster so their chain hints can't be spoofed by URL substring inference. + * * @param adapters Array of adapters to manage + * @param network Active Solana network for the cluster hint * @param localStorageKey Key to store the selected wallet name * @param endpoint Optional RPC endpoint for wallet detection * @returns Wallet connection manager object */ export function createWalletConnectionManager( adapters: Adapter[], + network: WalletAdapterNetwork, localStorageKey = 'walletName', - endpoint?: string + endpoint?: string, ): WalletConnectionManager { // Internal state for adapters let currentAdapters: Adapter[] = []; @@ -52,7 +58,7 @@ export function createWalletConnectionManager( // Initialize wallet detection and get initial adapters (async () => { - const detectedAdapters = await getStandardWalletAdapters(adapters, endpoint); + const detectedAdapters = await getStandardWalletAdapters(adapters, endpoint, network); currentAdapters = detectedAdapters; // Try to restore previously selected wallet diff --git a/packages/adapter-base/src/utils/environment.ts b/packages/adapter-base/src/utils/environment.ts index 50315d9..13d5fda 100644 --- a/packages/adapter-base/src/utils/environment.ts +++ b/packages/adapter-base/src/utils/environment.ts @@ -145,20 +145,3 @@ export function isIosAndRedirectable(): boolean { return isIos && isSafari; } -/** - * Infer Solana cluster from endpoint - * @param endpoint RPC endpoint URL - * @returns The inferred cluster/network - */ -export function getInferredNetworkFromEndpoint(endpoint?: string): WalletAdapterNetwork { - if (!endpoint) { - return WalletAdapterNetwork.Mainnet; - } - if (/devnet/i.test(endpoint)) { - return WalletAdapterNetwork.Devnet; - } else if (/testnet/i.test(endpoint)) { - return WalletAdapterNetwork.Testnet; - } else { - return WalletAdapterNetwork.Mainnet; - } -} \ No newline at end of file diff --git a/packages/react-core/src/components/ContextProvider.tsx b/packages/react-core/src/components/ContextProvider.tsx index 1702eeb..18dfb12 100644 --- a/packages/react-core/src/components/ContextProvider.tsx +++ b/packages/react-core/src/components/ContextProvider.tsx @@ -1,9 +1,8 @@ import React, { FC, ReactNode, useMemo } from 'react'; -import { Adapter, ConnectionConfig, WalletAdapterNetwork, WalletError } from '@hermis/solana-headless-core'; +import { Adapter, ConnectionConfig, WalletError } from '@hermis/solana-headless-core'; import { WalletProvider as BaseWalletProvider } from '../providers/WalletProvider.js'; import { ConnectionProvider } from '../providers/ConnectionProvider.js'; import { StorageProviderFactory } from '../hooks/useLocalStorage.js'; -import { getInferredNetworkFromEndpoint } from '@hermis/solana-headless-adapter-base'; import { TWalletAdapterNetwork } from '../index.js'; /** @@ -43,17 +42,12 @@ export const ContextProvider: FC = ({ children, adapters, rpcEndpoint, - network: explicitNetwork, + network, autoConnect = false, storageKey = 'walletName', storageFactory, onError, }) => { - - // Infer network if not explicitly provided - const network = explicitNetwork || getInferredNetworkFromEndpoint(rpcEndpoint); - - const connectionConfig = useMemo(() => ({ commitment: 'confirmed', }) as ConnectionConfig, []); diff --git a/packages/react-core/src/components/HermisProvider.tsx b/packages/react-core/src/components/HermisProvider.tsx index 070f3b8..1fa346c 100644 --- a/packages/react-core/src/components/HermisProvider.tsx +++ b/packages/react-core/src/components/HermisProvider.tsx @@ -4,6 +4,20 @@ import { ContextProvider } from './ContextProvider.js'; import { useStandardWalletAdapters } from '../hooks/useStandardWalletAdapters.js'; import { StorageProviderFactory } from '../hooks/useLocalStorage.js'; +/** + * Convert {@link TWalletAdapterNetwork} (which includes Localnet) into the + * upstream {@link WalletAdapterNetwork} enum understood by wallet adapters. + * Localnet is mapped to Devnet for cluster-aware features since wallets do + * not have a chain identifier for local validators. + */ +export function toWalletAdapterNetwork(network: TWalletAdapterNetwork): WalletAdapterNetwork { + if (network === TWalletAdapterNetwork.Localnet) { + return WalletAdapterNetwork.Devnet; + } + // Mainnet/Devnet/Testnet share string values with WalletAdapterNetwork. + return network as unknown as WalletAdapterNetwork; +} + /** * Props for the ContextProvider component */ @@ -55,19 +69,19 @@ export const HermisProvider = ({ }: HermisWalletProviderProps) => { const [adapters, setAdapters] = useState([]); - const memoizedAdapters = useMemo(() => - wallets, [ - - wallets.length, - ]); + // Identity-stable key over the wallet set. Memoising on `.length` would + // miss swaps where the array length stays the same but the wallets differ. + const walletNamesKey = useMemo( + () => wallets.map(w => w.name).join('|'), + [wallets], + ); useEffect(() => { - const baseAdapters: Adapter[] = []; - - setAdapters([...baseAdapters, ...wallets]); - }, [memoizedAdapters]); + setAdapters([...wallets]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [walletNamesKey]); - const allAdapters = useStandardWalletAdapters(adapters, endpoint); + const allAdapters = useStandardWalletAdapters(adapters, endpoint, toWalletAdapterNetwork(network)); return ( (null); useEffect(() => { - if (autoFetch && signature) { - fetchStatus(); + txStatusRef.current = txStatus; + }, [txStatus]); - const interval = setInterval(fetchStatus, 2000); - return () => clearInterval(interval); - } + // Poll with exponential backoff and stop on terminal status. + // Previously this was setInterval(fetchStatus, 2000) — it kept hitting + // the RPC twice every 2s forever, including after finalisation, which + // tripped rate limits on hosted RPC providers. + useEffect(() => { + if (!autoFetch || !signature) return; + + const BACKOFF_MS = [1000, 2000, 5000, 10000, 30000] as const; + let cancelled = false; + let timeoutId: ReturnType | null = null; + let attempt = 0; + + const tick = async () => { + if (cancelled) return; + await fetchStatus(); + if (cancelled) return; + + const current = txStatusRef.current; + if (current && (current.status === 'finalized' || current.status === 'failed')) { + return; + } + + const delay = BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]; + attempt += 1; + timeoutId = setTimeout(tick, delay); + }; + + void tick(); + + return () => { + cancelled = true; + if (timeoutId !== null) clearTimeout(timeoutId); + }; }, [autoFetch, fetchStatus, signature]); return { diff --git a/packages/react-core/src/hooks/useStandardWalletAdapters.ts b/packages/react-core/src/hooks/useStandardWalletAdapters.ts index 58b87de..ce07c35 100644 --- a/packages/react-core/src/hooks/useStandardWalletAdapters.ts +++ b/packages/react-core/src/hooks/useStandardWalletAdapters.ts @@ -1,32 +1,40 @@ -import { Adapter } from '@hermis/solana-headless-core'; -import { +import { Adapter, WalletAdapterNetwork } from '@hermis/solana-headless-core'; +import { getStandardWalletAdapters as getBaseStandardWalletAdapters, - subscribeToWalletAdapterChanges + subscribeToWalletAdapterChanges } from '@hermis/wallet-standard-base'; import { useEffect, useMemo, useState } from 'react'; /** * Hook for getting standard wallet adapters - * - * This hook integrates with @hermis/solana-headless-adapter-base's + * + * This hook integrates with @hermis/wallet-standard-base's * getStandardWalletAdapters function to discover and initialize * wallet adapters including standard wallets. - * + * * The base library handles all initialization and change detection automatically. - * + * + * `network` must be supplied explicitly — we do not infer the cluster from the + * RPC endpoint URL (substring matching is unsafe). + * * @param existingAdapters Existing adapters to include * @param endpoint Optional RPC endpoint for mobile wallet adapter + * @param network Active Solana network for the cluster hint * @returns Array of Adapters */ export function useStandardWalletAdapters( existingAdapters: Adapter[] = [], - endpoint?: string + endpoint: string | undefined, + network: WalletAdapterNetwork, ): Adapter[] { const [adapters, setAdapters] = useState(existingAdapters); - const memoizedAdapters = useMemo(() => existingAdapters, [ - existingAdapters.length, - ]); + // Identity-stable key over the adapter set. Memoising on `.length` would + // miss swaps where the array length stays the same but the adapters differ. + const adapterNamesKey = useMemo( + () => existingAdapters.map(a => a.name).join('|'), + [existingAdapters], + ); // Initial fetch of adapters useEffect(() => { @@ -37,7 +45,8 @@ export function useStandardWalletAdapters( // The base library handles initialization and change detection automatically const standardAdapters = await getBaseStandardWalletAdapters( existingAdapters, - endpoint + endpoint, + network, ); if (mounted) { @@ -57,7 +66,8 @@ export function useStandardWalletAdapters( return () => { mounted = false; }; - }, [memoizedAdapters, endpoint]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [adapterNamesKey, endpoint, network]); // Subscribe to dynamic adapter changes useEffect(() => { @@ -82,7 +92,8 @@ export function useStandardWalletAdapters( mounted = false; unsubscribe(); }; - }, [existingAdapters]); // Add existingAdapters as dependency + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [adapterNamesKey]); return adapters; } \ No newline at end of file diff --git a/packages/react-core/src/providers/WalletProvider.tsx b/packages/react-core/src/providers/WalletProvider.tsx index 6f8321e..7ddb447 100644 --- a/packages/react-core/src/providers/WalletProvider.tsx +++ b/packages/react-core/src/providers/WalletProvider.tsx @@ -7,6 +7,7 @@ import { signMessage, signTransaction, Transaction, + WalletAdapterNetwork, WalletError, WalletName, WalletNotReadyError, @@ -23,6 +24,7 @@ import { DualConnection, DualArchitectureOptions } from '@hermis/solana-headless-core'; +import { toWalletAdapterNetwork } from '../components/HermisProvider.js'; import { HermisError, HERMIS_ERROR__STANDARD_WALLET__FEATURE_NOT_FOUND } from '@hermis/errors'; import { getIsMobile, @@ -111,10 +113,14 @@ export function WalletProvider({ storageFactory, onError, }: WalletProviderProps) { - const { connection } = useConnection(); + const { connection, network } = useConnection(); // Safely access rpcEndpoint - only exists on legacy Connection, not Kit Rpc const rpcEndpoint = connection && 'rpcEndpoint' in connection ? (connection.rpcEndpoint as string) : undefined; - const adaptersWithStandardAdapters = useStandardWalletAdapters(adapters, rpcEndpoint); + const adaptersWithStandardAdapters = useStandardWalletAdapters( + adapters, + rpcEndpoint, + toWalletAdapterNetwork(network), + ); const [walletName, setWalletName] = useLocalStorage( storageKey, @@ -679,37 +685,69 @@ export function WalletProvider({ } }, [adapterState.adapter]); + // Resolve the public key once per render from the most reliable source. + // Reading refs during render is fine for derivation — they're stable for the + // life of this render and the memo below only re-runs when the actual fields + // changed. + const publicKey = + wallet?.adapter.publicKey || + adapterState.adapter?.publicKey || + adapterState.publicKey || + null; + + const contextValue = useMemo( + () => ({ + autoConnect: !!autoConnect, + wallets, + wallet, + publicKey, + connecting: adapterState.connecting, + connected: adapterState.connected, + disconnecting: adapterState.disconnecting, + // Kit-friendly properties + address: kitSigners.address, + addressString: kitSigners.addressString, + chain: currentChain, + messageSigner: kitSigners.messageSigner, + transactionSigner: kitSigners.transactionSigner, + getChainId: handleGetChainId, + select: updateWalletNameAsync, + connect: handleConnect, + disconnect: handleDisconnect, + signTransaction: handleSignTransactionExtended, + sendTransaction: handleSendTransactionExtended, + signAndSendTransaction: handleSignAndSendTransactionExtended, + signAllTransactions: handleSignAllTransactionsExtended, + signMessage: handleSignMessage, + signIn: handleSignIn, + hasFeature, + }), + [ + autoConnect, + wallets, + wallet, + publicKey, + adapterState.connecting, + adapterState.connected, + adapterState.disconnecting, + kitSigners, + currentChain, + handleGetChainId, + updateWalletNameAsync, + handleConnect, + handleDisconnect, + handleSignTransactionExtended, + handleSendTransactionExtended, + handleSignAndSendTransactionExtended, + handleSignAllTransactionsExtended, + handleSignMessage, + handleSignIn, + hasFeature, + ], + ); + return ( - + {children} ); diff --git a/packages/wallet-standard/src/chain.ts b/packages/wallet-standard/src/chain.ts new file mode 100644 index 0000000..7b491f6 --- /dev/null +++ b/packages/wallet-standard/src/chain.ts @@ -0,0 +1,26 @@ +import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; + +/** + * Wallet Standard chain identifiers for Solana networks. + * + * Format: `solana:`. + * @see https://github.com/wallet-standard/wallet-standard + */ +export const SOLANA_MAINNET_CHAIN = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp' as const; +export const SOLANA_DEVNET_CHAIN = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1' as const; +export const SOLANA_TESTNET_CHAIN = 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z' as const; + +/** + * Convert a {@link WalletAdapterNetwork} into the canonical Wallet Standard + * chain identifier expected by wallet features like `signAndSendTransaction`. + */ +export function networkToChainId(network: WalletAdapterNetwork): `solana:${string}` { + switch (network) { + case WalletAdapterNetwork.Mainnet: + return SOLANA_MAINNET_CHAIN; + case WalletAdapterNetwork.Devnet: + return SOLANA_DEVNET_CHAIN; + case WalletAdapterNetwork.Testnet: + return SOLANA_TESTNET_CHAIN; + } +} diff --git a/packages/wallet-standard/src/environment.ts b/packages/wallet-standard/src/environment.ts index 01589e3..8dbb013 100644 --- a/packages/wallet-standard/src/environment.ts +++ b/packages/wallet-standard/src/environment.ts @@ -129,20 +129,3 @@ export function isIosAndRedirectable(): boolean { return isIos && isSafari; } -/** - * Infer Solana cluster from endpoint - * @param endpoint RPC endpoint URL - * @returns The inferred cluster/network - */ -export function getInferredNetworkFromEndpoint(endpoint?: string): WalletAdapterNetwork { - if (!endpoint) { - return WalletAdapterNetwork.Mainnet; - } - if (/devnet/i.test(endpoint)) { - return WalletAdapterNetwork.Devnet; - } else if (/testnet/i.test(endpoint)) { - return WalletAdapterNetwork.Testnet; - } else { - return WalletAdapterNetwork.Mainnet; - } -} \ No newline at end of file diff --git a/packages/wallet-standard/src/utils.ts b/packages/wallet-standard/src/utils.ts index 1bbfb45..d097786 100644 --- a/packages/wallet-standard/src/utils.ts +++ b/packages/wallet-standard/src/utils.ts @@ -16,7 +16,7 @@ import { toUiWallet, toUiWalletAccount, } from './types.js'; -import { getEnvironment, getUriForAppIdentity, getUserAgent, getInferredNetworkFromEndpoint } from './environment.js'; +import { getEnvironment, getUriForAppIdentity, getUserAgent } from './environment.js'; import { SolanaMobileWalletAdapterWalletName } from './constants.js'; import { @@ -28,52 +28,37 @@ import { // isWalletAdapterCompatibleStandardWallet is imported from types.js /** - * Create a Mobile Wallet Adapter - * @param endpoint Optional RPC endpoint + * Create a Mobile Wallet Adapter for the given network. + * + * Network must be supplied explicitly by the caller — inferring a cluster from + * the RPC endpoint URL is unsafe (URLs whose path happens to contain "mainnet" + * would be misclassified), so this function deliberately does not accept that fallback. + * + * @param endpoint Optional RPC endpoint (unused for cluster selection; kept for future use) + * @param network Solana network the mobile adapter should authorize against * @returns Mobile wallet adapter or null if not available */ -export async function createMobileWalletAdapter(endpoint?: string): Promise { +export async function createMobileWalletAdapter( + endpoint: string | undefined, + network: WalletAdapterNetwork, +): Promise { // Skip if not in browser environment if (typeof window === 'undefined' || !window.navigator) { - console.log('not in a browser'); return null; } - - // Try to import the mobile wallet adapter - // let SolanaMobileWalletAdapterState; - const { + + const { SolanaMobileWalletAdapter, - } = await import( '@solana-mobile/wallet-adapter-mobile') - try { - console.log(SolanaMobileWalletAdapter); - // SolanaMobileWalletAdapterState = SolanaMobileWalletAdapter; - } catch (error) { - console.warn('Mobile wallet adapter not available:', error); - console.log('null returned'); - return null; - } - - // Check if adapter class was found + } = await import('@solana-mobile/wallet-adapter-mobile'); + if (!SolanaMobileWalletAdapter) { - console.log('null returned'); return null; } - - // Try to create the adapter + try { - console.log('Mobile wallet'); - const network = getInferredNetworkFromEndpoint(endpoint); - - // const { - // SolanaMobileWalletAdapter, - // createDefaultAuthorizationResultCache, - // createDefaultWalletNotFoundHandler - // } = await import( '@solana-mobile/wallet-adapter-mobile') - const adapter = new SolanaMobileWalletAdapter({ addressSelector: { select: (addresses: string[]) => Promise.resolve(addresses[0]), - // resolveImmediately: true, }, appIdentity: { name: document.title || 'Solana dApp', @@ -83,11 +68,10 @@ export async function createMobileWalletAdapter(endpoint?: string): Promise void>(); // Store for existing adapters from different consumers -const existingAdaptersStore = new Map(); +const existingAdaptersStore = new Map(); /** * Generate a unique key for a consumer based on their adapters */ -function generateConsumerKey(adapters: (Adapter | SolanaMobileWalletAdapter)[], endpoint?: string): string { +function generateConsumerKey(adapters: (Adapter | SolanaMobileWalletAdapter)[], endpoint: string | undefined, network: WalletAdapterNetwork): string { const adapterNames = adapters.map(a => a.name).sort().join(','); - return `${adapterNames}:${endpoint || 'default'}`; + return `${adapterNames}:${endpoint || 'default'}:${network}`; } /** @@ -169,66 +153,77 @@ async function updateAdapters(consumerKey?: string) { try { // Get all existing adapters from all consumers const allExistingAdapters = getAllExistingAdapters(); - - // Determine endpoint - use the first available endpoint + + // Determine endpoint and network from the first stored consumer entry. + // Most setups have a single consumer; for multi-consumer cases the first + // win is good enough — adapters created here will have their network + // overridden by `setNetwork` below before the call returns. let endpoint: string | undefined; - for (const { endpoint: consumerEndpoint } of existingAdaptersStore.values()) { - if (consumerEndpoint) { - endpoint = consumerEndpoint; - break; - } + let network: WalletAdapterNetwork | undefined; + for (const entry of existingAdaptersStore.values()) { + if (!endpoint && entry.endpoint) endpoint = entry.endpoint; + if (!network) network = entry.network; + if (endpoint && network) break; } - + const userAgentString = getUserAgent(); let adaptersToUse = [...allExistingAdapters]; - + // Check if we're in a mobile environment const isMobileEnv = getEnvironment({ adapters: allExistingAdapters, userAgentString }) === Environment.MOBILE_WEB; - + // Check if mobile wallet adapter is already included const hasMobileWalletAdapter = allExistingAdapters.some( adapter => adapter.name === SolanaMobileWalletAdapterWalletName ); - + // Separate mobile wallet adapter from regular adapters - const regularAdapters = allExistingAdapters.filter(adapter => + const regularAdapters = allExistingAdapters.filter(adapter => adapter.name !== SolanaMobileWalletAdapterWalletName ) as Adapter[]; - + let mobileAdapter: SolanaMobileWalletAdapter | null = null; - + // Add mobile wallet adapter if in mobile environment and not already included - if (isMobileEnv && !hasMobileWalletAdapter) { - console.log("Created Mobile Adapter"); - - const createdMobileAdapter = await createMobileWalletAdapter(endpoint); - + if (isMobileEnv && !hasMobileWalletAdapter && network) { + const createdMobileAdapter = await createMobileWalletAdapter(endpoint, network); + if (createdMobileAdapter && isMobileWalletAdapter(createdMobileAdapter)) { mobileAdapter = createdMobileAdapter; } } else if (hasMobileWalletAdapter) { // Find existing mobile adapter - const existingMobileAdapter = allExistingAdapters.find(adapter => + const existingMobileAdapter = allExistingAdapters.find(adapter => adapter.name === SolanaMobileWalletAdapterWalletName ); if (existingMobileAdapter && isMobileWalletAdapter(existingMobileAdapter)) { mobileAdapter = existingMobileAdapter; } } - + // Get detected wallets merged with regular adapters only const mergedRegularAdapters = getDetectedWalletAdapters(regularAdapters); // Combine all adapters: mobile adapter first (if exists), then regular adapters adaptersToUse = mobileAdapter ? [mobileAdapter, ...mergedRegularAdapters] : mergedRegularAdapters; + // Tag every standard wallet adapter with the active network so its chain + // hint is correct on wallet feature calls. + if (network) { + for (const adapter of adaptersToUse) { + if (hasSetNetwork(adapter)) { + adapter.setNetwork(network); + } + } + } + // Update current adapters if they've changed - const hasChanged = currentAdapters.length !== adaptersToUse.length || + const hasChanged = currentAdapters.length !== adaptersToUse.length || !currentAdapters.every((adapter, index) => adapter.name === adaptersToUse[index]?.name); - + if (hasChanged) { currentAdapters = adaptersToUse; notifyAdapterChanges(); @@ -241,16 +236,33 @@ async function updateAdapters(consumerKey?: string) { } } +function hasSetNetwork(adapter: unknown): adapter is { setNetwork(network: WalletAdapterNetwork): void } { + return ( + typeof adapter === 'object' && + adapter !== null && + 'setNetwork' in adapter && + typeof (adapter as { setNetwork: unknown }).setNetwork === 'function' + ); +} + /** * Get wallet adapters for all available standard wallets - * This function now handles initialization and change detection automatically + * + * This function handles initialization and change detection automatically. + * The `network` parameter is required — every adapter returned will have its + * cluster set explicitly. We do **not** infer the cluster from `endpoint` by + * URL substring matching, which is unsafe (untrusted URLs containing the + * substring "mainnet" would be misclassified as mainnet). + * * @param existingAdapters Existing (non-standard) adapters to include in result - * @param endpoint Optional endpoint for mobile wallet adapter + * @param endpoint Optional endpoint (used by mobile wallet adapter) + * @param network Active Solana network for the cluster hint * @returns Array of all adapters including standard wallet adapters */ export async function getStandardWalletAdapters( - existingAdapters: (Adapter | SolanaMobileWalletAdapter)[] = [], - endpoint?: string + existingAdapters: (Adapter | SolanaMobileWalletAdapter)[] = [], + endpoint: string | undefined, + network: WalletAdapterNetwork, ): Promise<(Adapter | SolanaMobileWalletAdapter)[]> { // Skip if not in browser environment if (typeof window === 'undefined' || !window.navigator) { @@ -258,10 +270,10 @@ export async function getStandardWalletAdapters( } // Generate a unique key for this consumer - const consumerKey = generateConsumerKey(existingAdapters, endpoint); - + const consumerKey = generateConsumerKey(existingAdapters, endpoint, network); + // Store the existing adapters for this consumer - existingAdaptersStore.set(consumerKey, { adapters: existingAdapters, endpoint }); + existingAdaptersStore.set(consumerKey, { adapters: existingAdapters, endpoint, network }); // Initialize wallet detection system only once if (!isWalletDetectionInitialized) { @@ -473,26 +485,6 @@ export function isSameWallet(wallet1: TypedStandardWallet | Adapter, wallet2: Ty return getWalletIdentifier(wallet1) === getWalletIdentifier(wallet2); } -/** - * Detect cluster/network from RPC endpoint - */ -export function detectClusterFromEndpoint(endpoint: string): string { - const url = endpoint.toLowerCase(); - - if (url.includes('mainnet') || url.includes('api.solana.com')) { - return 'solana:mainnet-beta'; - } else if (url.includes('devnet')) { - return 'solana:devnet'; - } else if (url.includes('testnet')) { - return 'solana:testnet'; - } else if (url.includes('localhost') || url.includes('127.0.0.1')) { - return 'solana:localnet'; - } - - return 'solana:mainnet-beta'; - // return 'unknown'; -} - /** * Get cluster identifier for wallet standard */ diff --git a/packages/wallet-standard/src/wallet-adapter.ts b/packages/wallet-standard/src/wallet-adapter.ts index 4e038a8..bb58ab8 100644 --- a/packages/wallet-standard/src/wallet-adapter.ts +++ b/packages/wallet-standard/src/wallet-adapter.ts @@ -1,4 +1,5 @@ import { + WalletAdapterNetwork, WalletReadyState, WalletName, WalletAdapterEvents, @@ -58,10 +59,8 @@ import { StandardEventsChangeEvent } from './types.js'; import { WalletAccount } from '@wallet-standard/base'; -import { - detectClusterFromEndpoint, - ClusterMismatchError -} from './utils.js'; +import { ClusterMismatchError } from './utils.js'; +import { networkToChainId } from './chain.js'; import bs58 from 'bs58'; @@ -111,10 +110,10 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { private _eventEmitter = new EventEmitter(); private _removeAccountChangeListener: (() => void) | null = null; private _connectedAccount: StandardWalletAccount[] | null = null; - private _currentCluster: string | null = null; + private _currentCluster: `solana:${string}` | null = null; private _rpcEndpoint: string | null = null; - constructor(wallet: TypedStandardWallet) { + constructor(wallet: TypedStandardWallet, network?: WalletAdapterNetwork) { if (!isWalletAdapterCompatibleStandardWallet(wallet)) { throw new Error('Wallet is not adapter compatible'); } @@ -124,6 +123,10 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { this.url = wallet.website || ''; this.icon = wallet.icon || ''; + if (network) { + this._currentCluster = networkToChainId(network); + } + // Setup account change listener - following official wallet-standard pattern if (StandardEventsMethod in wallet.features) { try { @@ -331,8 +334,6 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { async connect(): Promise { try { if (this.connected || this.connecting) { - console.log("DEBUG connected:", this.connected); - console.log("DEBUG connecting:", this.connecting); return; } this._connecting = true; @@ -349,8 +350,6 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { const connectResult = await connectFeature.connect(); - console.log("DEBUG connectResult", connectResult); - if (!connectResult || !connectResult.accounts || !Array.isArray(connectResult.accounts)) { throw new Error('Invalid connect result from wallet'); } @@ -427,10 +426,6 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { try { // Path 1: If connection is provided, delegate to core for sign + send if (connection) { - console.log('🔍 [StandardWalletAdapter.sendTransaction] Received transaction:', transaction); - console.log('🔍 [StandardWalletAdapter.sendTransaction] Transaction type:', transaction.constructor.name); - // console.log('🔍 [StandardWalletAdapter.sendTransaction] Transaction.signatures:', transaction.signatures); - // Set recent blockhash if not already set (for legacy Transaction) if (transaction instanceof Transaction && !transaction.recentBlockhash) { const { blockhash } = await getLatestBlockhash(connection, options.preflightCommitment); @@ -439,14 +434,11 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { // Check if transaction is already signed // Note: Don't use instanceof - it fails with bundling/prototype issues - console.log('🔍 [StandardWalletAdapter.sendTransaction] About to check if signed...'); const isSigned = isTransactionSigned(transaction as any); - console.log(`🔍 [StandardWalletAdapter.sendTransaction] isSigned result: ${isSigned}`); let rawTransaction: Uint8Array; if (isSigned) { - console.log('✅ [StandardWalletAdapter.sendTransaction] Transaction is SIGNED - sending directly without wallet prompt'); // Transaction is already signed - serialize and send directly (no wallet prompt) if (transaction instanceof Transaction) { rawTransaction = transaction.serialize(); @@ -458,7 +450,6 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { throw new Error('Signed transaction does not have a serialize method'); } } else { - console.log('⚠️ [StandardWalletAdapter.sendTransaction] Transaction is UNSIGNED - signing first (will prompt wallet)'); // Transaction is unsigned - sign it first using core const dualOptions = { chain: this._currentCluster || 'solana:mainnet' @@ -659,7 +650,6 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { } async signMessage(message: Uint8Array): Promise { - console.log("DEBUG wallet", this._wallet); if (!this.connected) { throw new HermisError( HERMIS_ERROR__WALLET_CONNECTION__NOT_CONNECTED, @@ -731,11 +721,24 @@ export class StandardWalletAdapter implements IStandardWalletAdapter { } /** - * Set the current RPC endpoint for cluster detection + * Record the RPC endpoint this adapter is currently connected to. + * + * Does not touch the active cluster — call {@link setNetwork} for that. + * Inferring the cluster from a URL by substring match is unsafe because + * untrusted URLs ("example.com/mainnet-rpc") would be classified as mainnet + * and produce mismatched chain hints to wallets. Networks must be supplied + * explicitly by the consumer. */ setRpcEndpoint(endpoint: string): void { this._rpcEndpoint = endpoint; - this._currentCluster = detectClusterFromEndpoint(endpoint); + } + + /** + * Set the active Solana network so chain-aware features (signAndSendTransaction + * etc.) tag their requests with the right chain identifier. + */ + setNetwork(network: WalletAdapterNetwork): void { + this._currentCluster = networkToChainId(network); } /** diff --git a/packages/wallet-standard/src/wallet-detection.ts b/packages/wallet-standard/src/wallet-detection.ts index 8ad700e..c14669d 100644 --- a/packages/wallet-standard/src/wallet-detection.ts +++ b/packages/wallet-standard/src/wallet-detection.ts @@ -317,29 +317,34 @@ export function initializeWalletDetection(): () => void { console.warn("Failed to dispatch app ready event:", error); } - // Set up periodic check for delayed wallet registration - const periodicCheck = setInterval(() => { - try { - // Re-dispatch app ready event to catch any wallets that registered late - const appReadyEvent = new CustomEvent("wallet-standard:app-ready", { - detail: api, - bubbles: false, - cancelable: false, - composed: false, - }); - window.dispatchEvent(appReadyEvent); - } catch (error) { - console.warn("Failed to dispatch periodic app ready event:", error); - } - }, 1000); // Check every second for the first 10 seconds - - // Clear periodic check after 10 seconds - setTimeout(() => { - clearInterval(periodicCheck); - }, 10000); + // Re-dispatch `wallet-standard:app-ready` on a short backoff schedule to + // catch wallets that register slightly after the page loads. Previously + // this fired every second for 10 seconds — 10 dispatches, each of which + // re-registered every wallet and re-rendered every subscriber even after + // the wallet set had stabilized. Backed-off 4-shot schedule covers the + // same window with materially less churn. + const RETRY_DELAYS = [100, 300, 1000, 3000]; + const retryTimeouts: ReturnType[] = []; + + RETRY_DELAYS.forEach((delay) => { + const id = setTimeout(() => { + try { + const appReadyEvent = new CustomEvent("wallet-standard:app-ready", { + detail: api, + bubbles: false, + cancelable: false, + composed: false, + }); + window.dispatchEvent(appReadyEvent); + } catch (error) { + console.warn("Failed to dispatch periodic app ready event:", error); + } + }, delay); + retryTimeouts.push(id); + }); cleanupFunctions.push(() => { - clearInterval(periodicCheck); + retryTimeouts.forEach((id) => clearTimeout(id)); }); } catch (error) { From 7f147bcb36ccf12bdf5c4e8f54b76c626a46b1a8 Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 19 May 2026 11:23:11 +0100 Subject: [PATCH 3/8] docs: update examples for v2.1 explicit-network + off-chain signMessage Updates all getStandardWalletAdapters / createWalletConnectionManager examples for the new explicit-network signature, and adds two migration sections to v1-to-v2.mdx: - Off-chain message domain separator (with verifier-side sample code) - Explicit network requirement (with before/after diffs) --- .../adapter-base/kit-integration.mdx | 19 +++-- docs/api-reference/adapter-base/overview.mdx | 9 ++- .../adapter-base/wallet-connect.mdx | 19 +++-- docs/cookbook/connect-wallet.mdx | 6 +- docs/examples/vanilla-js.mdx | 16 +++-- docs/getting-started.mdx | 18 +++-- docs/installation.mdx | 6 +- docs/migration/v1-to-v2.mdx | 71 +++++++++++++++++++ docs/quickstart/adapter-base.mdx | 24 ++++--- 9 files changed, 147 insertions(+), 41 deletions(-) diff --git a/docs/api-reference/adapter-base/kit-integration.mdx b/docs/api-reference/adapter-base/kit-integration.mdx index 3735640..a39b172 100644 --- a/docs/api-reference/adapter-base/kit-integration.mdx +++ b/docs/api-reference/adapter-base/kit-integration.mdx @@ -14,11 +14,13 @@ The Kit integration utilities allow you to convert legacy wallet adapters into K Convert a wallet adapter to Kit-compatible signers. ```typescript -import { createKitSignersFromAdapter, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { createKitSignersFromAdapter } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { Connection } from '@solana/web3.js'; // Auto-detect wallet-standard compatible wallets -const adapters = getStandardWalletAdapters([]); +const adapters = await getStandardWalletAdapters([], undefined, WalletAdapterNetwork.Mainnet); const adapter = adapters[0]; // Use first detected wallet await adapter.connect(); @@ -69,7 +71,9 @@ const { address, addressString, messageSigner, transactionSigner } = ### Usage with Kit ```typescript -import { createKitSignersFromAdapter, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { createKitSignersFromAdapter } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { createSolanaRpc, devnet } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; import { @@ -81,7 +85,7 @@ import { } from '@solana/kit'; // Auto-detect wallet-standard compatible wallets -const adapters = getStandardWalletAdapters([]); +const adapters = await getStandardWalletAdapters([], undefined, WalletAdapterNetwork.Mainnet); const adapter = adapters[0]; // Use first detected wallet await adapter.connect(); @@ -314,15 +318,16 @@ import { createKitSignersFromAdapter, createKitTransaction, adapterSupportsMessageSigning, - adapterSupportsTransactionSending, - getStandardWalletAdapters + adapterSupportsTransactionSending } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { createSolanaRpc, devnet } from '@solana/kit'; import { getTransferSolInstruction } from '@solana-program/system'; import { lamports } from '@solana/kit'; // Auto-detect wallet-standard compatible wallets -const adapters = getStandardWalletAdapters([]); +const adapters = await getStandardWalletAdapters([], undefined, WalletAdapterNetwork.Mainnet); const adapter = adapters[0]; // Use first detected wallet await adapter.connect(); diff --git a/docs/api-reference/adapter-base/overview.mdx b/docs/api-reference/adapter-base/overview.mdx index f5f5468..a8f73a7 100644 --- a/docs/api-reference/adapter-base/overview.mdx +++ b/docs/api-reference/adapter-base/overview.mdx @@ -39,10 +39,15 @@ await manager.disconnect(); Get all standard wallet adapters including custom ones. +> **Breaking change in v2:** `network` is now a required argument. Inferring the +> cluster from the RPC endpoint URL has been removed — substring matching is +> unsafe because untrusted URLs containing "mainnet" would be misclassified. + ```typescript -import { getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; -const adapters = getStandardWalletAdapters(customAdapters); +const adapters = await getStandardWalletAdapters(customAdapters, undefined, WalletAdapterNetwork.Mainnet); ``` ### sortWalletAdapters() diff --git a/docs/api-reference/adapter-base/wallet-connect.mdx b/docs/api-reference/adapter-base/wallet-connect.mdx index 68496be..952a46b 100644 --- a/docs/api-reference/adapter-base/wallet-connect.mdx +++ b/docs/api-reference/adapter-base/wallet-connect.mdx @@ -8,14 +8,17 @@ description: 'Manage wallet connections in vanilla TypeScript' Factory function that creates a wallet connection manager for vanilla TypeScript applications. ```typescript -import { createWalletConnectionManager, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { createWalletConnectionManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed -const adapters = getStandardWalletAdapters(wallets); +const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Mainnet); const manager = createWalletConnectionManager( adapters, + WalletAdapterNetwork.Mainnet, 'wallet-name-storage-key', 'https://api.devnet.solana.com' // Optional: for wallet detection ); @@ -27,6 +30,12 @@ const manager = createWalletConnectionManager( Array of wallet adapters to manage + + Active Solana network. Required so wallet adapters can set their chain hint + explicitly — previous versions inferred this from the RPC URL which was + unsafe (untrusted URLs containing "mainnet" would be misclassified). + + Key to store the selected wallet name in localStorage @@ -214,12 +223,14 @@ unsubscribe(); ## Complete Example ```typescript -import { createWalletConnectionManager, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { createWalletConnectionManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { Connection, Transaction, SystemProgram, LAMPORTS_PER_SOL } from '@solana/web3.js'; // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed -const adapters = getStandardWalletAdapters(wallets); +const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Mainnet); // Create manager const manager = createWalletConnectionManager( diff --git a/docs/cookbook/connect-wallet.mdx b/docs/cookbook/connect-wallet.mdx index 0be3daa..8e64856 100644 --- a/docs/cookbook/connect-wallet.mdx +++ b/docs/cookbook/connect-wallet.mdx @@ -60,11 +60,13 @@ function WalletSelector() { ## Vanilla JavaScript ```javascript -import { WalletAdapterManager, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { WalletAdapterManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed -const adapters = getStandardWalletAdapters(wallets); +const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Mainnet); const manager = new WalletAdapterManager(adapters); manager.on('connect', (publicKey) => { diff --git a/docs/examples/vanilla-js.mdx b/docs/examples/vanilla-js.mdx index 68253d5..44ea276 100644 --- a/docs/examples/vanilla-js.mdx +++ b/docs/examples/vanilla-js.mdx @@ -175,7 +175,9 @@ h1 { ```javascript app.js (Web3.js) -import { WalletAdapterManager, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { WalletAdapterManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { Connection, clusterApiUrl, LAMPORTS_PER_SOL } from '@solana/web3.js'; // Global state @@ -183,12 +185,12 @@ let manager; let connection; // Initialize wallet manager and connection -function initWalletManager() { +async function initWalletManager() { connection = new Connection(clusterApiUrl('devnet')); // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed - const adapters = getStandardWalletAdapters(wallets); + const adapters = await getStandardWalletAdapters(wallets, connection.rpcEndpoint, WalletAdapterNetwork.Devnet); manager = new WalletAdapterManager(adapters); setupEventListeners(); @@ -281,7 +283,9 @@ initWalletManager(); ``` ```javascript app.js (Kit) -import { WalletAdapterManager, createKitSignersFromAdapter, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { WalletAdapterManager, createKitSignersFromAdapter } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { Connection, clusterApiUrl } from '@solana/web3.js'; // Global state @@ -290,12 +294,12 @@ let connection; let kitSigners; // Initialize wallet manager with Kit support -function initWalletManager() { +async function initWalletManager() { connection = new Connection(clusterApiUrl('devnet')); // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed - const adapters = getStandardWalletAdapters(wallets); + const adapters = await getStandardWalletAdapters(wallets, connection.rpcEndpoint, WalletAdapterNetwork.Devnet); manager = new WalletAdapterManager(adapters); setupEventListeners(); diff --git a/docs/getting-started.mdx b/docs/getting-started.mdx index 515ad56..8777e7e 100644 --- a/docs/getting-started.mdx +++ b/docs/getting-started.mdx @@ -113,10 +113,9 @@ function WalletButton() { ``` ```typescript Adapter Base (web3.js) -import { - WalletAdapterManager, - getStandardWalletAdapters -} from '@hermis/solana-headless-adapter-base'; +import { WalletAdapterManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { Connection, clusterApiUrl } from '@solana/web3.js'; // Create connection @@ -124,7 +123,7 @@ const connection = new Connection(clusterApiUrl('devnet')); // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed -const adapters = getStandardWalletAdapters(wallets); +const adapters = await getStandardWalletAdapters(wallets, connection.rpcEndpoint, WalletAdapterNetwork.Devnet); // Create wallet manager const manager = new WalletAdapterManager(adapters); @@ -144,10 +143,9 @@ await manager.connect(); ``` ```typescript Adapter Base (@solana/kit) -import { - WalletAdapterManager, - getStandardWalletAdapters -} from '@hermis/solana-headless-adapter-base'; +import { WalletAdapterManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; import { createSolanaRpc, devnet } from '@solana/kit'; // Create connection @@ -155,7 +153,7 @@ const rpc = createSolanaRpc(devnet('https://api.devnet.solana.com')); // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed -const adapters = getStandardWalletAdapters(wallets); +const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Devnet); // Create wallet manager const manager = new WalletAdapterManager(adapters); diff --git a/docs/installation.mdx b/docs/installation.mdx index 108f919..6b56c03 100644 --- a/docs/installation.mdx +++ b/docs/installation.mdx @@ -150,11 +150,13 @@ After installation, verify everything works: **For Adapter Base:** ```typescript -import { WalletAdapterManager, getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { WalletAdapterManager } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed -const adapters = getStandardWalletAdapters(wallets); +const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Mainnet); const manager = new WalletAdapterManager(adapters); console.log('Hermis Adapter Base initialized successfully!'); ``` diff --git a/docs/migration/v1-to-v2.mdx b/docs/migration/v1-to-v2.mdx index cf36c53..2237ac9 100644 --- a/docs/migration/v1-to-v2.mdx +++ b/docs/migration/v1-to-v2.mdx @@ -129,6 +129,77 @@ Improved wallet detection with CAIP-2 compliant chain identifiers: If you were using HermisProvider in early/pre-release versions, the following props were renamed for consistency: +### `signMessage` now applies the Solana off-chain message domain separator on local-key paths + +When the SDK signs a message with an in-SDK key (`Keypair`, `CryptoKeyPair`, +`KeyPairSigner`), it now prepends the Solana off-chain message header +(`0xff` + `"solana offchain"` + version + format + length) before signing. +Without this domain separator, signature bytes are structurally +indistinguishable from a Solana transaction signature, and a caller can be +tricked into producing a valid transaction signature by handing the signer +bytes that happen to encode a Solana transaction message. + +This is a behaviour change for **local-key signers only**. External wallets +(Phantom / Backpack / Solflare / any Standard Wallet or Adapter) already +apply this header themselves — those paths are unchanged. + +**If you verify these signatures server-side**, apply the same domain header +to the message before verification: + +```typescript +const SIGNING_DOMAIN = Buffer.concat([ + Buffer.from([0xff]), + Buffer.from('solana offchain', 'utf-8'), +]); + +function withOffchainPrefix(message: Uint8Array): Uint8Array { + const header = Buffer.alloc(4); + header.writeUInt8(0, 0); // header version + header.writeUInt8(1, 1); // format: UTF-8 + header.writeUInt16LE(message.length, 2); // length, LE + return Buffer.concat([SIGNING_DOMAIN, header, Buffer.from(message)]); +} + +// Then verify against the wrapped bytes: +nacl.sign.detached.verify(withOffchainPrefix(originalMessage), signature, publicKey); +``` + +### Explicit network required (no more URL inference) + +v2 removes substring-based cluster inference from RPC endpoint URLs. URLs whose +path happens to contain `mainnet` or `devnet` were silently classified as that +cluster — unsafe whenever the URL came from an untrusted source. + +`getStandardWalletAdapters` and `createWalletConnectionManager` now require +an explicit `network` argument: + + +```typescript Before (v1) +import { getStandardWalletAdapters } from '@hermis/solana-headless-adapter-base'; + +const adapters = await getStandardWalletAdapters(wallets, endpoint); +``` + +```typescript After (v2) +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; + +const adapters = await getStandardWalletAdapters( + wallets, + endpoint, + WalletAdapterNetwork.Mainnet, // required +); +``` + + +`detectClusterFromEndpoint` and `getInferredNetworkFromEndpoint` have been +removed from the public API. `StandardWalletAdapter` no longer derives its +cluster from `setRpcEndpoint`; call the new `setNetwork(network)` method +instead — `HermisProvider` does this automatically when you pass `network`. + +`HermisProvider`'s `network` prop was already required after v2.0; no change +is needed there. + ### HermisProvider Props Standardized To match `WalletProvider` interface and follow web3.js conventions: diff --git a/docs/quickstart/adapter-base.mdx b/docs/quickstart/adapter-base.mdx index 2a37f60..f66c513 100644 --- a/docs/quickstart/adapter-base.mdx +++ b/docs/quickstart/adapter-base.mdx @@ -30,17 +30,19 @@ Set up wallet adapters for your application: ```javascript import { WalletAdapterManager, - getStandardWalletAdapters, sortWalletAdapters } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; // Create wallet adapter instances const wallets = [ /** Other wallet that implement the wallet standard */ ]; -// Get all standard wallet adapters (includes Wallet Standard wallets) -const adapters = getStandardWalletAdapters(wallets); +// Get all standard wallet adapters (includes Wallet Standard wallets). +// `network` is required — we no longer infer it from the RPC URL. +const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Mainnet); console.log(`Found ${adapters.length} wallet adapters`); ``` @@ -247,9 +249,10 @@ Here's a complete vanilla JavaScript example: ```javascript app.js import { WalletAdapterManager, - getStandardWalletAdapters, sortWalletAdapters, } from '@hermis/solana-headless-adapter-base'; +import { getStandardWalletAdapters } from '@hermis/wallet-standard-base'; +import { WalletAdapterNetwork } from '@hermis/solana-headless-core'; class WalletApp { constructor() { @@ -257,11 +260,11 @@ class WalletApp { this.init(); } - init() { + async init() { // Auto-detect wallet-standard compatible wallets const wallets = []; // Add custom wallet adapters here if needed - const adapters = getStandardWalletAdapters(wallets); + const adapters = await getStandardWalletAdapters(wallets, undefined, WalletAdapterNetwork.Mainnet); const sorted = sortWalletAdapters(adapters); // Create wallet manager @@ -391,9 +394,14 @@ const app = new WalletApp(); ### Utility Functions - Gets all standard wallet adapters including custom ones + Gets all standard wallet adapters including custom ones. **Network is required** + — substring-based inference from the RPC URL was removed for security. ```javascript - getStandardWalletAdapters(customAdapters?: Adapter[]): Adapter[] + getStandardWalletAdapters( + customAdapters: Adapter[] | undefined, + endpoint: string | undefined, + network: WalletAdapterNetwork, + ): Promise ``` From b0d2b247748f58c790851411a57c71d02d32c585 Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 19 May 2026 12:10:43 +0100 Subject: [PATCH 4/8] chore: changeset for v2.1.0 P1 security & perf release --- .changeset/p1-security-perf-hardening.md | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .changeset/p1-security-perf-hardening.md diff --git a/.changeset/p1-security-perf-hardening.md b/.changeset/p1-security-perf-hardening.md new file mode 100644 index 0000000..60264a4 --- /dev/null +++ b/.changeset/p1-security-perf-hardening.md @@ -0,0 +1,44 @@ +--- +"@hermis/solana-headless-core": minor +"@hermis/solana-headless-adapter-base": minor +"@hermis/wallet-standard-base": minor +"@hermis/solana-headless-react": minor +--- + +P1 security & performance hardening. + +⚠️ **Behaviour changes worth checking before upgrading from 2.0.x:** +- `getStandardWalletAdapters(adapters, endpoint, network)` and + `createWalletConnectionManager(adapters, network, ...)` now require an + explicit `network` argument — RPC-URL substring inference was removed. +- Local-key `signMessage` now wraps the message with the Solana off-chain + domain separator (`0xff` + `"solana offchain"` + header). Existing + verifiers must apply the same wrapper. External wallet adapters + (Phantom/Backpack/Solflare/any Standard Wallet) are unaffected. +- The alternative 3-arg `sendTransaction(tx, wallet, options)` form is + removed; use the 4-arg form with an explicit connection. + +See `docs/migration/v1-to-v2.mdx` for migration steps and verifier-side +sample code. + +**Security / correctness** +- Fix Kit transaction-sending signer to `bs58.decode` signature bytes + (previously returned UTF-8 of the base58 string). +- Apply the Solana off-chain message domain separator on local-key + `signMessage` paths. +- Require explicit `network` for cluster identification; remove URL + substring inference. +- Remove `sendTransaction` / `signAndSendTransaction` alt overload that + silently routed to a hard-coded devnet connection. +- Fix `useStandardWalletAdapters` / `HermisProvider` memoisation that + missed adapter swaps with the same array length. +- Strip debug `console.log` calls leaking unsigned transaction bytes + from `StandardWalletAdapter`. + +**Performance** +- Memoise `WalletContext.Provider` value to eliminate full-tree + re-renders on `WalletProvider` re-renders. +- Bound the `wallet-standard:app-ready` dispatch loop: 10×1s → 4-shot + backoff (100ms, 300ms, 1s, 3s). +- `useSolanaTransaction` polling: stop on `finalized`/`failed`, + exponential backoff (1s → 30s). From b68051bb715b3827555689ea9bed80aaa3a981bb Mon Sep 17 00:00:00 2001 From: mctursh Date: Tue, 19 May 2026 12:34:25 +0100 Subject: [PATCH 5/8] chore: release v2.1.0 (1.1.0 for wallet-standard) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `pnpm version-packages` output for the P1 security & perf changeset. - @hermis/solana-headless-core: 2.0.0 → 2.1.0 - @hermis/solana-headless-adapter-base: 2.0.0 → 2.1.0 - @hermis/solana-headless-react: 2.0.0 → 2.1.0 - @hermis/wallet-standard-base: 1.0.0 → 1.1.0 react-core's bump was manually corrected from the 3.0.0 changesets produced (stale pre-release accumulator state); intended minor bump is 2.1.0 to stay aligned with the core/adapter-base release cycle. Exits the "next" pre-release mode in .changeset/pre.json. --- .changeset/p1-security-perf-hardening.md | 44 -------------------- .changeset/pre.json | 19 --------- apps/sample-dapp/CHANGELOG.md | 9 ++++ apps/sample-dapp/package.json | 2 +- apps/sample-react-dapp/CHANGELOG.md | 9 ++++ apps/sample-react-dapp/package.json | 2 +- packages/adapter-base/CHANGELOG.md | 51 +++++++++++++++++++++++ packages/adapter-base/package.json | 6 +-- packages/core/CHANGELOG.md | 45 ++++++++++++++++++++ packages/core/package.json | 2 +- packages/react-core/CHANGELOG.md | 52 ++++++++++++++++++++++++ packages/react-core/package.json | 10 ++--- packages/wallet-standard/CHANGELOG.md | 50 +++++++++++++++++++++++ packages/wallet-standard/package.json | 4 +- 14 files changed, 229 insertions(+), 76 deletions(-) delete mode 100644 .changeset/p1-security-perf-hardening.md delete mode 100644 .changeset/pre.json diff --git a/.changeset/p1-security-perf-hardening.md b/.changeset/p1-security-perf-hardening.md deleted file mode 100644 index 60264a4..0000000 --- a/.changeset/p1-security-perf-hardening.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -"@hermis/solana-headless-core": minor -"@hermis/solana-headless-adapter-base": minor -"@hermis/wallet-standard-base": minor -"@hermis/solana-headless-react": minor ---- - -P1 security & performance hardening. - -⚠️ **Behaviour changes worth checking before upgrading from 2.0.x:** -- `getStandardWalletAdapters(adapters, endpoint, network)` and - `createWalletConnectionManager(adapters, network, ...)` now require an - explicit `network` argument — RPC-URL substring inference was removed. -- Local-key `signMessage` now wraps the message with the Solana off-chain - domain separator (`0xff` + `"solana offchain"` + header). Existing - verifiers must apply the same wrapper. External wallet adapters - (Phantom/Backpack/Solflare/any Standard Wallet) are unaffected. -- The alternative 3-arg `sendTransaction(tx, wallet, options)` form is - removed; use the 4-arg form with an explicit connection. - -See `docs/migration/v1-to-v2.mdx` for migration steps and verifier-side -sample code. - -**Security / correctness** -- Fix Kit transaction-sending signer to `bs58.decode` signature bytes - (previously returned UTF-8 of the base58 string). -- Apply the Solana off-chain message domain separator on local-key - `signMessage` paths. -- Require explicit `network` for cluster identification; remove URL - substring inference. -- Remove `sendTransaction` / `signAndSendTransaction` alt overload that - silently routed to a hard-coded devnet connection. -- Fix `useStandardWalletAdapters` / `HermisProvider` memoisation that - missed adapter swaps with the same array length. -- Strip debug `console.log` calls leaking unsigned transaction bytes - from `StandardWalletAdapter`. - -**Performance** -- Memoise `WalletContext.Provider` value to eliminate full-tree - re-renders on `WalletProvider` re-renders. -- Bound the `wallet-standard:app-ready` dispatch loop: 10×1s → 4-shot - backoff (100ms, 300ms, 1s, 3s). -- `useSolanaTransaction` polling: stop on `finalized`/`failed`, - exponential backoff (1s → 30s). diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index c91a458..0000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "mode": "pre", - "tag": "beta", - "initialVersions": { - "sample-dapp": "1.0.8", - "sample-react-dapp": "0.0.13", - "@hermis/solana-headless-adapter-base": "1.1.2", - "@hermis/solana-headless-core": "1.1.2", - "@hermis/solana-headless-enterprise-core": "0.0.4", - "@hermis/errors": "1.0.0", - "@hermis/solana-headless-format-config": "1.0.5", - "@hermis/solana-headless-react": "1.1.2", - "@hermis/wallet-standard-base": "1.0.0" - }, - "changesets": [ - "hot-drinks-hear", - "wise-knives-press" - ] -} diff --git a/apps/sample-dapp/CHANGELOG.md b/apps/sample-dapp/CHANGELOG.md index b8714e1..e1fde7b 100644 --- a/apps/sample-dapp/CHANGELOG.md +++ b/apps/sample-dapp/CHANGELOG.md @@ -1,5 +1,14 @@ # sample-dapp +## 1.0.10 + +### Patch Changes + +- Updated dependencies [b0d2b24] + - @hermis/solana-headless-core@2.1.0 + - @hermis/solana-headless-adapter-base@2.1.0 + - @hermis/wallet-standard-base@1.1.0 + ## 1.0.9 ### Patch Changes diff --git a/apps/sample-dapp/package.json b/apps/sample-dapp/package.json index c7fd094..98bd501 100644 --- a/apps/sample-dapp/package.json +++ b/apps/sample-dapp/package.json @@ -1,6 +1,6 @@ { "name": "sample-dapp", - "version": "1.0.9", + "version": "1.0.10", "description": "", "main": "index.js", "type": "module", diff --git a/apps/sample-react-dapp/CHANGELOG.md b/apps/sample-react-dapp/CHANGELOG.md index bb5488d..a87ed73 100644 --- a/apps/sample-react-dapp/CHANGELOG.md +++ b/apps/sample-react-dapp/CHANGELOG.md @@ -1,5 +1,14 @@ # sample-react-dapp +## 0.0.15 + +### Patch Changes + +- Updated dependencies [b0d2b24] + - @hermis/solana-headless-core@2.1.0 + - @hermis/solana-headless-adapter-base@2.1.0 + - @hermis/solana-headless-react@2.1.0 + ## 0.0.14 ### Patch Changes diff --git a/apps/sample-react-dapp/package.json b/apps/sample-react-dapp/package.json index 54c5cad..62c1c81 100644 --- a/apps/sample-react-dapp/package.json +++ b/apps/sample-react-dapp/package.json @@ -1,7 +1,7 @@ { "name": "sample-react-dapp", "private": true, - "version": "0.0.14", + "version": "0.0.15", "type": "module", "scripts": { "dev": "vite --host", diff --git a/packages/adapter-base/CHANGELOG.md b/packages/adapter-base/CHANGELOG.md index fd1f37f..1ddea5b 100644 --- a/packages/adapter-base/CHANGELOG.md +++ b/packages/adapter-base/CHANGELOG.md @@ -1,5 +1,56 @@ # @hermis/solana-headless-adapter-base +## 2.1.0 + +### Minor Changes + +- b0d2b24: P1 security & performance hardening. + + ⚠️ **Behaviour changes worth checking before upgrading from 2.0.x:** + + - `getStandardWalletAdapters(adapters, endpoint, network)` and + `createWalletConnectionManager(adapters, network, ...)` now require an + explicit `network` argument — RPC-URL substring inference was removed. + - Local-key `signMessage` now wraps the message with the Solana off-chain + domain separator (`0xff` + `"solana offchain"` + header). Existing + verifiers must apply the same wrapper. External wallet adapters + (Phantom/Backpack/Solflare/any Standard Wallet) are unaffected. + - The alternative 3-arg `sendTransaction(tx, wallet, options)` form is + removed; use the 4-arg form with an explicit connection. + + See `docs/migration/v1-to-v2.mdx` for migration steps and verifier-side + sample code. + + **Security / correctness** + + - Fix Kit transaction-sending signer to `bs58.decode` signature bytes + (previously returned UTF-8 of the base58 string). + - Apply the Solana off-chain message domain separator on local-key + `signMessage` paths. + - Require explicit `network` for cluster identification; remove URL + substring inference. + - Remove `sendTransaction` / `signAndSendTransaction` alt overload that + silently routed to a hard-coded devnet connection. + - Fix `useStandardWalletAdapters` / `HermisProvider` memoisation that + missed adapter swaps with the same array length. + - Strip debug `console.log` calls leaking unsigned transaction bytes + from `StandardWalletAdapter`. + + **Performance** + + - Memoise `WalletContext.Provider` value to eliminate full-tree + re-renders on `WalletProvider` re-renders. + - Bound the `wallet-standard:app-ready` dispatch loop: 10×1s → 4-shot + backoff (100ms, 300ms, 1s, 3s). + - `useSolanaTransaction` polling: stop on `finalized`/`failed`, + exponential backoff (1s → 30s). + +### Patch Changes + +- Updated dependencies [b0d2b24] + - @hermis/solana-headless-core@2.1.0 + - @hermis/wallet-standard-base@1.1.0 + ## 2.0.0 ### Major Changes diff --git a/packages/adapter-base/package.json b/packages/adapter-base/package.json index 6114a0f..9d45468 100644 --- a/packages/adapter-base/package.json +++ b/packages/adapter-base/package.json @@ -1,6 +1,6 @@ { "name": "@hermis/solana-headless-adapter-base", - "version": "2.0.0", + "version": "2.1.0", "description": "The framework agnostic implementation of the Hermis headless solana wallet adapetr", "main": "dist/index.js", "module": "dist/index.mjs", @@ -24,8 +24,8 @@ }, "dependencies": { "@hermis/errors": "1.0.0", - "@hermis/solana-headless-core": "2.0.0", - "@hermis/wallet-standard-base": "1.0.0", + "@hermis/solana-headless-core": "2.1.0", + "@hermis/wallet-standard-base": "1.1.0", "@solana/kit": "^3.0.0", "@solana/wallet-adapter-base": "^0.9.23", "@solana/web3.js": "^1.98.0", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index ae7d8a5..0bf9380 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,50 @@ # @solana/headless-core +## 2.1.0 + +### Minor Changes + +- b0d2b24: P1 security & performance hardening. + + ⚠️ **Behaviour changes worth checking before upgrading from 2.0.x:** + + - `getStandardWalletAdapters(adapters, endpoint, network)` and + `createWalletConnectionManager(adapters, network, ...)` now require an + explicit `network` argument — RPC-URL substring inference was removed. + - Local-key `signMessage` now wraps the message with the Solana off-chain + domain separator (`0xff` + `"solana offchain"` + header). Existing + verifiers must apply the same wrapper. External wallet adapters + (Phantom/Backpack/Solflare/any Standard Wallet) are unaffected. + - The alternative 3-arg `sendTransaction(tx, wallet, options)` form is + removed; use the 4-arg form with an explicit connection. + + See `docs/migration/v1-to-v2.mdx` for migration steps and verifier-side + sample code. + + **Security / correctness** + + - Fix Kit transaction-sending signer to `bs58.decode` signature bytes + (previously returned UTF-8 of the base58 string). + - Apply the Solana off-chain message domain separator on local-key + `signMessage` paths. + - Require explicit `network` for cluster identification; remove URL + substring inference. + - Remove `sendTransaction` / `signAndSendTransaction` alt overload that + silently routed to a hard-coded devnet connection. + - Fix `useStandardWalletAdapters` / `HermisProvider` memoisation that + missed adapter swaps with the same array length. + - Strip debug `console.log` calls leaking unsigned transaction bytes + from `StandardWalletAdapter`. + + **Performance** + + - Memoise `WalletContext.Provider` value to eliminate full-tree + re-renders on `WalletProvider` re-renders. + - Bound the `wallet-standard:app-ready` dispatch loop: 10×1s → 4-shot + backoff (100ms, 300ms, 1s, 3s). + - `useSolanaTransaction` polling: stop on `finalized`/`failed`, + exponential backoff (1s → 30s). + ## 2.0.0 ### Major Changes diff --git a/packages/core/package.json b/packages/core/package.json index 853e56b..5e4ca97 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@hermis/solana-headless-core", - "version": "2.0.0", + "version": "2.1.0", "description": "Core wallet logic for the Hermis Solana Headless SDK", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/react-core/CHANGELOG.md b/packages/react-core/CHANGELOG.md index 1276e34..bb81909 100644 --- a/packages/react-core/CHANGELOG.md +++ b/packages/react-core/CHANGELOG.md @@ -1,5 +1,57 @@ # @solana/headless-session +## 2.1.0 + +### Minor Changes + +- b0d2b24: P1 security & performance hardening. + + ⚠️ **Behaviour changes worth checking before upgrading from 2.0.x:** + + - `getStandardWalletAdapters(adapters, endpoint, network)` and + `createWalletConnectionManager(adapters, network, ...)` now require an + explicit `network` argument — RPC-URL substring inference was removed. + - Local-key `signMessage` now wraps the message with the Solana off-chain + domain separator (`0xff` + `"solana offchain"` + header). Existing + verifiers must apply the same wrapper. External wallet adapters + (Phantom/Backpack/Solflare/any Standard Wallet) are unaffected. + - The alternative 3-arg `sendTransaction(tx, wallet, options)` form is + removed; use the 4-arg form with an explicit connection. + + See `docs/migration/v1-to-v2.mdx` for migration steps and verifier-side + sample code. + + **Security / correctness** + + - Fix Kit transaction-sending signer to `bs58.decode` signature bytes + (previously returned UTF-8 of the base58 string). + - Apply the Solana off-chain message domain separator on local-key + `signMessage` paths. + - Require explicit `network` for cluster identification; remove URL + substring inference. + - Remove `sendTransaction` / `signAndSendTransaction` alt overload that + silently routed to a hard-coded devnet connection. + - Fix `useStandardWalletAdapters` / `HermisProvider` memoisation that + missed adapter swaps with the same array length. + - Strip debug `console.log` calls leaking unsigned transaction bytes + from `StandardWalletAdapter`. + + **Performance** + + - Memoise `WalletContext.Provider` value to eliminate full-tree + re-renders on `WalletProvider` re-renders. + - Bound the `wallet-standard:app-ready` dispatch loop: 10×1s → 4-shot + backoff (100ms, 300ms, 1s, 3s). + - `useSolanaTransaction` polling: stop on `finalized`/`failed`, + exponential backoff (1s → 30s). + +### Patch Changes + +- Updated dependencies [b0d2b24] + - @hermis/solana-headless-core@2.1.0 + - @hermis/solana-headless-adapter-base@2.1.0 + - @hermis/wallet-standard-base@1.1.0 + ## 2.0.0 ### Major Changes diff --git a/packages/react-core/package.json b/packages/react-core/package.json index d02c7b3..e721535 100644 --- a/packages/react-core/package.json +++ b/packages/react-core/package.json @@ -1,6 +1,6 @@ { "name": "@hermis/solana-headless-react", - "version": "2.0.0", + "version": "2.1.0", "description": "React hooks and components for Hermis Solana Headless SDK", "main": "dist/index.cjs", "module": "dist/index.js", @@ -27,15 +27,15 @@ ], "dependencies": { "@hermis/errors": "1.0.0", - "@hermis/solana-headless-adapter-base": "2.0.0", - "@hermis/solana-headless-core": "2.0.0", - "@hermis/wallet-standard-base": "1.0.0", + "@hermis/solana-headless-adapter-base": "2.1.0", + "@hermis/solana-headless-core": "2.1.0", + "@hermis/wallet-standard-base": "1.1.0", "@solana/kit": "^3.0.0", "react": "^19.0.1" }, "peerDependencies": { "react": ">=16.8.0", - "@hermis/solana-headless-core": ">=2.0.0" + "@hermis/solana-headless-core": ">=2.1.0" }, "keywords": [ "solana", diff --git a/packages/wallet-standard/CHANGELOG.md b/packages/wallet-standard/CHANGELOG.md index f696e26..d9046d5 100644 --- a/packages/wallet-standard/CHANGELOG.md +++ b/packages/wallet-standard/CHANGELOG.md @@ -1,5 +1,55 @@ # @hermis/wallet-standard-base +## 1.1.0 + +### Minor Changes + +- b0d2b24: P1 security & performance hardening. + + ⚠️ **Behaviour changes worth checking before upgrading from 2.0.x:** + + - `getStandardWalletAdapters(adapters, endpoint, network)` and + `createWalletConnectionManager(adapters, network, ...)` now require an + explicit `network` argument — RPC-URL substring inference was removed. + - Local-key `signMessage` now wraps the message with the Solana off-chain + domain separator (`0xff` + `"solana offchain"` + header). Existing + verifiers must apply the same wrapper. External wallet adapters + (Phantom/Backpack/Solflare/any Standard Wallet) are unaffected. + - The alternative 3-arg `sendTransaction(tx, wallet, options)` form is + removed; use the 4-arg form with an explicit connection. + + See `docs/migration/v1-to-v2.mdx` for migration steps and verifier-side + sample code. + + **Security / correctness** + + - Fix Kit transaction-sending signer to `bs58.decode` signature bytes + (previously returned UTF-8 of the base58 string). + - Apply the Solana off-chain message domain separator on local-key + `signMessage` paths. + - Require explicit `network` for cluster identification; remove URL + substring inference. + - Remove `sendTransaction` / `signAndSendTransaction` alt overload that + silently routed to a hard-coded devnet connection. + - Fix `useStandardWalletAdapters` / `HermisProvider` memoisation that + missed adapter swaps with the same array length. + - Strip debug `console.log` calls leaking unsigned transaction bytes + from `StandardWalletAdapter`. + + **Performance** + + - Memoise `WalletContext.Provider` value to eliminate full-tree + re-renders on `WalletProvider` re-renders. + - Bound the `wallet-standard:app-ready` dispatch loop: 10×1s → 4-shot + backoff (100ms, 300ms, 1s, 3s). + - `useSolanaTransaction` polling: stop on `finalized`/`failed`, + exponential backoff (1s → 30s). + +### Patch Changes + +- Updated dependencies [b0d2b24] + - @hermis/solana-headless-core@2.1.0 + ## 1.0.0 ### Major Changes diff --git a/packages/wallet-standard/package.json b/packages/wallet-standard/package.json index 33a32fa..b99053c 100644 --- a/packages/wallet-standard/package.json +++ b/packages/wallet-standard/package.json @@ -1,6 +1,6 @@ { "name": "@hermis/wallet-standard-base", - "version": "1.0.0", + "version": "1.1.0", "description": "The framework agnostic wallet standard implementation. This package is completely framework-agnostic, allowing you to integrate Solana wallets with any UI framework or vanilla JavaScript.", "main": "dist/index.js", "module": "dist/index.mjs", @@ -33,7 +33,7 @@ "packageManager": "pnpm@10.5.2", "dependencies": { "@hermis/errors": "1.0.0", - "@hermis/solana-headless-core": "2.0.0", + "@hermis/solana-headless-core": "2.1.0", "@solana/wallet-adapter-base": "^0.9.23", "@solana/web3.js": "^1.98.0", "@solana-mobile/wallet-adapter-mobile": "^2.0.0", From fdfd8259bdc3ae8f1be766c339c5a7c545855a4e Mon Sep 17 00:00:00 2001 From: mctursh Date: Wed, 20 May 2026 14:00:05 +0100 Subject: [PATCH 6/8] chore(release): harden publish config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - changeset config access: restricted → public (these scopes are public) - ignore @hermis/solana-headless-enterprise-core (not ready; has known auth bugs and was never published) - mark sample-dapp private so the demo never publishes to npm - rename `prerelease` script → `enter-prerelease`; `prerelease` is an npm lifecycle pre-hook for `release`, so it was silently re-entering changeset pre-release mode on every `pnpm release` and pushing the `next` dist-tag instead of `latest` --- .changeset/config.json | 4 ++-- apps/sample-dapp/package.json | 1 + package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.changeset/config.json b/.changeset/config.json index d88011f..cb78d07 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -4,8 +4,8 @@ "commit": false, "fixed": [], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": ["@hermis/solana-headless-enterprise-core"] } diff --git a/apps/sample-dapp/package.json b/apps/sample-dapp/package.json index 98bd501..28c58f0 100644 --- a/apps/sample-dapp/package.json +++ b/apps/sample-dapp/package.json @@ -1,6 +1,7 @@ { "name": "sample-dapp", "version": "1.0.10", + "private": true, "description": "", "main": "index.js", "type": "module", diff --git a/package.json b/package.json index a65df24..1b91a1b 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "minor": "changeset add --minor", "major": "changeset add --major", "patch": "changeset add --patch", - "prerelease": "changeset pre enter next", + "enter-prerelease": "changeset pre enter next", "graduate": "changeset pre exit" }, "publishConfig": { From 6be266bb91e0797a103bf15a9fedb31ab30f5cb0 Mon Sep 17 00:00:00 2001 From: mctursh Date: Wed, 20 May 2026 14:02:11 +0100 Subject: [PATCH 7/8] docs: point documentation links at hermis.dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Domain moved from hermis.io to hermis.dev. Update all docs.hermis.io references across the root README, CONTRIBUTING, package READMEs (adapter-base, react-core — these ship to npm), and mint.json metadata. Also point the Live Demo anchor at https://hermis.dev/. --- CONTRIBUTING.md | 4 ++-- README.MD | 10 +++++----- docs/mint.json | 6 +++--- packages/adapter-base/README.md | 6 +++--- packages/react-core/README.md | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3c69f9..90281ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -254,7 +254,7 @@ Add useTokenBalance hook for querying SPL token balances ## Documentation -Our documentation is built with **Mintlify** and hosted at [docs.hermis.io](https://docs.hermis.io). +Our documentation is built with **Mintlify** and hosted at [docs.hermis.dev](https://docs.hermis.dev). ### Contributing to Docs @@ -367,7 +367,7 @@ Use workspace protocol for internal dependencies: If you have questions or need help: -1. Check the [documentation](https://docs.hermis.io) +1. Check the [documentation](https://docs.hermis.dev) 2. Search existing [GitHub Issues](https://github.com/Assylum-Labs/hermis/issues) 3. Open a new issue with your question 4. Be specific and provide context diff --git a/README.MD b/README.MD index 62ea4ea..29c6855 100644 --- a/README.MD +++ b/README.MD @@ -7,12 +7,12 @@ A modular, UI-agnostic wallet adapter for Solana applications. Build wallet inte ## Documentation -Comprehensive documentation is available at **[docs.hermis.io](https://docs.hermis.io)** +Comprehensive documentation is available at **[docs.hermis.dev](https://docs.hermis.dev)** -- [Getting Started](https://docs.hermis.io/getting-started) -- [Quick Start Guides](https://docs.hermis.io/quickstart) -- [API Reference](https://docs.hermis.io/api-reference) -- [Examples](https://docs.hermis.io/examples) +- [Getting Started](https://docs.hermis.dev/getting-started) +- [Quick Start Guides](https://docs.hermis.dev/quickstart) +- [API Reference](https://docs.hermis.dev/api-reference) +- [Examples](https://docs.hermis.dev/examples) ## Packages diff --git a/docs/mint.json b/docs/mint.json index e393b30..ed5d89d 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -12,8 +12,8 @@ "metadata": { "og:title": "Hermis - Headless Solana Wallet Adapter", "og:description": "Framework-agnostic, headless wallet adapter library for Solana. Build wallet integrations with complete UI freedom.", - "og:image": "https://docs.hermis.io/favicon.png", - "og:url": "https://docs.hermis.io", + "og:image": "https://docs.hermis.dev/favicon.png", + "og:url": "https://docs.hermis.dev", "twitter:card": "summary_large_image", "twitter:site": "@Hermis_xyz" }, @@ -55,7 +55,7 @@ { "name": "Live Demo", "icon": "play", - "url": "https://assylum-labs.github.io/hermis/" + "url": "https://hermis.dev/" }, { "name": "Community", diff --git a/packages/adapter-base/README.md b/packages/adapter-base/README.md index 4a165a5..23613d4 100644 --- a/packages/adapter-base/README.md +++ b/packages/adapter-base/README.md @@ -51,12 +51,12 @@ if (phantomAdapter) { ## Documentation -For complete documentation, visit [docs.hermis.io/quickstart/adapter-base](https://docs.hermis.io/quickstart/adapter-base) +For complete documentation, visit [docs.hermis.dev/quickstart/adapter-base](https://docs.hermis.dev/quickstart/adapter-base) ## API Reference -- [WalletAdapterManager](https://docs.hermis.io/api-reference/adapter-base/wallet-manager) -- [Utility Functions](https://docs.hermis.io/api-reference/adapter-base/utilities) +- [WalletAdapterManager](https://docs.hermis.dev/api-reference/adapter-base/wallet-manager) +- [Utility Functions](https://docs.hermis.dev/api-reference/adapter-base/utilities) ## License diff --git a/packages/react-core/README.md b/packages/react-core/README.md index d1a7f8d..b3fa701 100644 --- a/packages/react-core/README.md +++ b/packages/react-core/README.md @@ -117,7 +117,7 @@ function NetworkInfo() { ## Documentation -For complete documentation, examples, and API reference, visit [docs.hermis.io/quickstart/react](https://docs.hermis.io/quickstart/react) +For complete documentation, examples, and API reference, visit [docs.hermis.dev/quickstart/react](https://docs.hermis.dev/quickstart/react) ## License From 9107ce6491b8c5d9f6a0e799de47edbfffe9749b Mon Sep 17 00:00:00 2001 From: mctursh Date: Wed, 20 May 2026 14:07:15 +0100 Subject: [PATCH 8/8] chore(sample-react-dapp): align vite base with custom domain + dev flag Match the root-path base used for the hermis.dev custom domain (also landed on main) and keep the __DEV__ define so @hermis/errors emits full error messages in dev. --- apps/sample-react-dapp/vite.config.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/sample-react-dapp/vite.config.ts b/apps/sample-react-dapp/vite.config.ts index 3f0073b..4184fb2 100644 --- a/apps/sample-react-dapp/vite.config.ts +++ b/apps/sample-react-dapp/vite.config.ts @@ -4,16 +4,18 @@ import react from '@vitejs/plugin-react-swc' // https://vite.dev/config/ export default defineConfig(({ command }) => { const isProduction = command === 'build' - const isGithubPages = process.env.GITHUB_PAGES === 'true' - + return { - base: isGithubPages ? '/hermis/' : '/', + // Custom domain (hermis.dev) serves at root, not a GH Pages subpath. + base: '/', plugins: [ react(), ], define: { global: 'globalThis', 'process.env': {}, + // Enable development mode for better error messages from @hermis/errors + __DEV__: !isProduction, }, resolve: { alias: {