import { Transaction, VersionedTransaction } from '@solana/web3.js' const SOLANA_SIGN_FEATURE = 'solana:signTransaction' // Wallet Standard `solana:signTransaction` feature: raw bytes in, raw bytes out. interface SolanaSignFeature { signTransaction: (input: { account: { address: string } transaction: Uint8Array }) => Promise> } // Minimal Solana adapter shape. `wallet` exists only on Wallet Standard adapters. export interface SignableSolanaAdapter { publicKey?: { toBase58(): string } | null signTransaction?: | (( tx: Transaction | VersionedTransaction ) => Promise) | undefined wallet?: | { accounts: ReadonlyArray<{ address: string }> features: Record } | undefined } /** * Sign raw Solana tx bytes → raw signed bytes. Sign-only (the relay broadcasts). * Prefers the Wallet Standard feature (no Transaction object → avoids the * serialize() crash on some mobile WebViews); falls back to the adapter signer * for legacy wallets. `connectedAddress` (from the iframe child) wins over the * adapter publicKey, which can drift. The caller verifies the message wasn't * altered (see InjectedConnector Path 1). */ export async function signSolanaBytes( adapter: SignableSolanaAdapter, serializedTx: Uint8Array, connectedAddress?: string ): Promise { const wallet = adapter.wallet if ( wallet && SOLANA_SIGN_FEATURE in wallet.features && wallet.accounts.length > 0 ) { const account = resolveSigningAccount( wallet.accounts, connectedAddress ?? adapter.publicKey?.toBase58() ) const feature = wallet.features[SOLANA_SIGN_FEATURE] as SolanaSignFeature const [output] = await feature.signTransaction({ account, transaction: serializedTx }) if (!output?.signedTransaction) { throw new Error('Wallet returned no signed transaction') } // Caller (InjectedConnector Path 1) verifies the message — keeps this // package @meshconnect-free. return output.signedTransaction } // Legacy fallback: build a Transaction and use the adapter's signer. if (adapter.signTransaction === undefined) { throw new Error('Adapter does not support signTransaction') } const tx = deserialize(serializedTx) const signed = await adapter.signTransaction(tx) // Serialize without the fee-payer's signature — the relay fills that slot. return signed instanceof VersionedTransaction ? signed.serialize() : new Uint8Array( signed.serialize({ requireAllSignatures: false, verifySignatures: false }) ) } /** * Pick which account signs. We must sign with the account the user connected — * never guess. A wrong signer produces a useless signature for a payment, so we * throw instead of silently falling back to the first account. */ function resolveSigningAccount( accounts: ReadonlyArray, connectedAddress: string | undefined ): T { if (connectedAddress) { const match = accounts.find(a => a.address === connectedAddress) if (!match) { throw new Error( 'Connected Solana account not found in the wallet — refusing to sign with a different account' ) } return match } // No explicit selection is only safe when the wallet has exactly one account. if (accounts.length === 1) return accounts[0]! throw new Error( 'Cannot determine which Solana account to sign with — multiple accounts and none selected' ) } function deserialize(bytes: Uint8Array): Transaction | VersionedTransaction { try { return VersionedTransaction.deserialize(bytes) } catch { try { return Transaction.from(bytes) } catch { throw new Error( 'Failed to deserialize transaction as either versioned or legacy format' ) } } }