From c67c0bdf2d570b318ba0ab3e5e1de6116080071a Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Thu, 26 Mar 2026 22:42:07 -0700 Subject: [PATCH 1/8] Add Solana wallet auth to SDK and simplify coin-gated example MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a WalletAuth module to @audius/sdk so apps can hand off wallet credentials (pubkey, message, signature) and have the SDK inject X-Solana-* headers automatically via middleware. Apps connect wallets however they want (Phantom, wallet-adapter, etc.) — the SDK owns the message format and request plumbing but bundles zero wallet dependencies. The coin-gated example now uses sdk.walletAuth.setCredential() and a single sdk.tracks.streamTrack() call for both OAuth and wallet auth paths, removing the manual fetch + header construction. Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/sdk/createSdk.ts | 9 +- packages/sdk/src/sdk/index.ts | 1 + .../sdk/middleware/addWalletAuthMiddleware.ts | 42 +++++++ packages/sdk/src/sdk/middleware/index.ts | 1 + packages/sdk/src/sdk/walletAuth/WalletAuth.ts | 33 ++++++ packages/sdk/src/sdk/walletAuth/index.ts | 3 + packages/sdk/src/sdk/walletAuth/message.ts | 13 +++ packages/sdk/src/sdk/walletAuth/types.ts | 9 ++ packages/web/examples/coin-gated/src/App.tsx | 110 ++++++------------ 9 files changed, 147 insertions(+), 74 deletions(-) create mode 100644 packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts create mode 100644 packages/sdk/src/sdk/walletAuth/WalletAuth.ts create mode 100644 packages/sdk/src/sdk/walletAuth/index.ts create mode 100644 packages/sdk/src/sdk/walletAuth/message.ts create mode 100644 packages/sdk/src/sdk/walletAuth/types.ts diff --git a/packages/sdk/src/sdk/createSdk.ts b/packages/sdk/src/sdk/createSdk.ts index 3e0253af007..2d97ae606a6 100644 --- a/packages/sdk/src/sdk/createSdk.ts +++ b/packages/sdk/src/sdk/createSdk.ts @@ -27,9 +27,11 @@ import { productionConfig } from './config/production' import { addAppInfoMiddleware, addRequestSignatureMiddleware, - addTokenRefreshMiddleware + addTokenRefreshMiddleware, + addWalletAuthMiddleware } from './middleware' import { OAuth } from './oauth' +import { WalletAuth } from './walletAuth' import { TokenStoreLocalStorage } from './oauth/TokenStoreLocalStorage' import { Logger, Storage, StorageNodeSelector } from './services' import { SdkConfigSchema, type SdkConfig } from './types' @@ -72,6 +74,8 @@ export const createSdk = (config: SdkConfig) => { openUrl: services?.openUrl }) + const walletAuth = new WalletAuth() + if (apiSecret || services?.audiusWalletClient) { middleware.push( addRequestSignatureMiddleware({ @@ -99,6 +103,8 @@ export const createSdk = (config: SdkConfig) => { ) } + middleware.push(addWalletAuthMiddleware({ walletAuth })) + // Auto-refresh middleware — intercepts 401s and retries with a fresh token. if (apiKey && oauth) { middleware.push( @@ -137,6 +143,7 @@ export const createSdk = (config: SdkConfig) => { return { oauth, + walletAuth, tokenStore, tracks: new TracksApi(apiConfig), users: usersApi, diff --git a/packages/sdk/src/sdk/index.ts b/packages/sdk/src/sdk/index.ts index 0f57ef476e8..a7a26333f83 100644 --- a/packages/sdk/src/sdk/index.ts +++ b/packages/sdk/src/sdk/index.ts @@ -59,6 +59,7 @@ export * from './services' export { productionConfig } from './config/production' export { developmentConfig } from './config/development' export * from './oauth/types' +export * from './walletAuth' export { ParseRequestError } from './utils/parseParams' export * from './utils/rendezvous' export * as Errors from './utils/errors' diff --git a/packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts b/packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts new file mode 100644 index 00000000000..e462f0d7999 --- /dev/null +++ b/packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts @@ -0,0 +1,42 @@ +import type { + FetchParams, + Middleware, + RequestContext +} from '../api/generated/default' +import type { WalletAuth } from '../walletAuth' + +const WALLET_HEADER = 'X-Solana-Wallet' +const MESSAGE_HEADER = 'X-Solana-Message' +const SIGNATURE_HEADER = 'X-Solana-Signature' + +/** + * Injects Solana wallet auth headers into every request when a credential + * is present. Skips if the request already has an Authorization header + * (i.e. OAuth takes precedence). + */ +export const addWalletAuthMiddleware = ({ + walletAuth +}: { + walletAuth: WalletAuth +}): Middleware => ({ + pre: async (context: RequestContext): Promise => { + const credential = walletAuth.getCredential() + if (!credential) return context + + const headers = context.init.headers as Record + if (headers['Authorization']) return context + + return { + ...context, + init: { + ...context.init, + headers: { + ...headers, + [WALLET_HEADER]: credential.publicKey, + [MESSAGE_HEADER]: credential.message, + [SIGNATURE_HEADER]: credential.signature + } + } + } + } +}) diff --git a/packages/sdk/src/sdk/middleware/index.ts b/packages/sdk/src/sdk/middleware/index.ts index ccd0647d27d..e9f23cb1fce 100644 --- a/packages/sdk/src/sdk/middleware/index.ts +++ b/packages/sdk/src/sdk/middleware/index.ts @@ -1,3 +1,4 @@ export { addAppInfoMiddleware } from './addAppInfoMiddleware' export { addRequestSignatureMiddleware } from './addRequestSignatureMiddleware' export { addTokenRefreshMiddleware } from './addTokenRefreshMiddleware' +export { addWalletAuthMiddleware } from './addWalletAuthMiddleware' diff --git a/packages/sdk/src/sdk/walletAuth/WalletAuth.ts b/packages/sdk/src/sdk/walletAuth/WalletAuth.ts new file mode 100644 index 00000000000..722b6b64b19 --- /dev/null +++ b/packages/sdk/src/sdk/walletAuth/WalletAuth.ts @@ -0,0 +1,33 @@ +import type { WalletAuthCredential } from './types' + +/** + * Manages Solana wallet authentication credentials. + * + * The SDK does not connect wallets directly — apps use whatever wallet + * adapter they prefer, sign the message from `createAuthMessage()`, and + * hand the credential here. The SDK then injects the appropriate headers + * into all API requests via middleware. + */ +export class WalletAuth { + private credential: WalletAuthCredential | null = null + + /** Store a signed wallet credential. */ + setCredential(credential: WalletAuthCredential) { + this.credential = credential + } + + /** Clear the stored credential (disconnect). */ + clearCredential() { + this.credential = null + } + + /** Returns the current credential, or null if not authenticated. */ + getCredential(): WalletAuthCredential | null { + return this.credential + } + + /** Whether a wallet credential is currently set. */ + isAuthenticated(): boolean { + return this.credential !== null + } +} diff --git a/packages/sdk/src/sdk/walletAuth/index.ts b/packages/sdk/src/sdk/walletAuth/index.ts new file mode 100644 index 00000000000..1f85de2f696 --- /dev/null +++ b/packages/sdk/src/sdk/walletAuth/index.ts @@ -0,0 +1,3 @@ +export { WalletAuth } from './WalletAuth' +export { createAuthMessage } from './message' +export type { WalletAuthCredential } from './types' diff --git a/packages/sdk/src/sdk/walletAuth/message.ts b/packages/sdk/src/sdk/walletAuth/message.ts new file mode 100644 index 00000000000..63861f15de1 --- /dev/null +++ b/packages/sdk/src/sdk/walletAuth/message.ts @@ -0,0 +1,13 @@ +/** + * Creates the canonical message for Solana wallet authentication. + * + * The message embeds a timestamp so the API can enforce expiry in the future. + * Apps sign this with the user's wallet and pass the result to + * `WalletAuth.setCredential()`. + */ +export function createAuthMessage() { + const timestamp = Date.now() + const message = `audius:wallet-auth:${timestamp}` + const messageBytes = new TextEncoder().encode(message) + return { message, messageBytes, timestamp } +} diff --git a/packages/sdk/src/sdk/walletAuth/types.ts b/packages/sdk/src/sdk/walletAuth/types.ts new file mode 100644 index 00000000000..8261df896fb --- /dev/null +++ b/packages/sdk/src/sdk/walletAuth/types.ts @@ -0,0 +1,9 @@ +/** Credential tuple produced by signing an SDK-generated message with a Solana wallet. */ +export type WalletAuthCredential = { + /** Base58-encoded Solana public key */ + publicKey: string + /** The message that was signed (from `createAuthMessage`) */ + message: string + /** Base58-encoded ed25519 signature */ + signature: string +} diff --git a/packages/web/examples/coin-gated/src/App.tsx b/packages/web/examples/coin-gated/src/App.tsx index 1e207df185e..7b9f5d74c29 100644 --- a/packages/web/examples/coin-gated/src/App.tsx +++ b/packages/web/examples/coin-gated/src/App.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' +import { createAuthMessage } from '@audius/sdk' import bs58 from 'bs58' import { config } from './config' @@ -9,12 +10,6 @@ import { getSDK } from './sdk' // Types // --------------------------------------------------------------------------- -type SolanaWalletAuth = { - pubkey: string - message: string - signature: string // base58-encoded ed25519 signature -} - type UserProfile = { id?: string handle?: string @@ -82,7 +77,6 @@ function useCoinBalance( queryKey: ['coin-balance', userId ? 'user' : 'wallet', userId ?? walletAddress, coinMint], queryFn: async () => { if (userId) { - // Single-coin lookup by user ID + mint — no need to fetch all coins const res = await sdk.users.getUserCoin({ id: userId, mint: coinMint! }) const coin = res.data as { decimals?: number; balance?: number } | undefined if (!coin) return null @@ -90,7 +84,6 @@ function useCoinBalance( const rawBalance = coin.balance ?? 0 return rawBalance / Math.pow(10, decimals) } - // Wallet path: no single-coin endpoint, fetch all and filter const res = await sdk.wallets.getWalletCoins({ walletId: walletAddress! }) const match = (res.data ?? []).find( (c: { mint?: string }) => c.mint === coinMint @@ -124,44 +117,6 @@ function useGatedTracks(artistId: string | undefined, userId: string | undefined }) } -// --------------------------------------------------------------------------- -// Stream helpers -// --------------------------------------------------------------------------- - -async function streamWithOAuth(trackId: string, userId: string): Promise { - // Use the generated streamTrack method which goes through the SDK middleware - // pipeline (adds Bearer token automatically). With noRedirect, the API - // returns a JSON object { data: "https://content-node/..." } instead of 302. - const sdk = getSDK() - const res = await sdk.tracks.streamTrack({ trackId, userId, noRedirect: true }) - // res.data is the direct content node URL — use it as the audio src - return res.data -} - -async function streamWithWallet( - trackId: string, - wallet: SolanaWalletAuth -): Promise { - // Build the stream URL manually and add Solana wallet headers. - // The middleware on the API verifies the ed25519 signature and checks - // on-chain token balances in real time. - const sdk = getSDK() - // Get the base URL from the SDK's configuration - const base = (sdk.tracks as unknown as { configuration: { basePath: string } }) - .configuration.basePath - const url = `${base}/tracks/${encodeURIComponent(trackId)}/stream?no_redirect=true` - const res = await fetch(url, { - headers: { - 'X-Solana-Wallet': wallet.pubkey, - 'X-Solana-Message': wallet.message, - 'X-Solana-Signature': wallet.signature - } - }) - if (!res.ok) throw new Error(`Stream failed: ${res.status}`) - const json = await res.json() - return json.data -} - // --------------------------------------------------------------------------- // App // --------------------------------------------------------------------------- @@ -173,7 +128,8 @@ export default function App() { // Auth state const [profile, setProfile] = useState(null) - const [solWallet, setSolWallet] = useState(null) + const [walletConnected, setWalletConnected] = useState(false) + const [walletPubkey, setWalletPubkey] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -181,6 +137,7 @@ export default function App() { const [playingId, setPlayingId] = useState(null) const [streamLoading, setStreamLoading] = useState(false) const audioRef = useRef(null) + // Data const { data: coin, isPending: coinPending, error: coinError } = useCoin(activeTicker) const userId = profile?.id ? String(profile.id) : undefined @@ -191,9 +148,8 @@ export default function App() { error: tracksError } = useGatedTracks(artistId, userId) - // Balance for the active coin — works for both auth paths const coinMint = coin?.mint - const { data: coinBalance } = useCoinBalance(userId, solWallet?.pubkey, coinMint) + const { data: coinBalance } = useCoinBalance(userId, walletPubkey ?? undefined, coinMint) // ------------------------------------------------------------------------- // OAuth session restore @@ -258,22 +214,31 @@ export default function App() { return } try { + const sdk = getSDK() const { publicKey } = await phantom.connect() const pubkey = publicKey.toString() - const message = `Audius coin-gated access: ${Date.now()}` - const msgBytes = new TextEncoder().encode(message) - const { signature: sigBytes } = await phantom.signMessage(msgBytes, 'utf8') + + // SDK owns the message format; app just signs it + const { message, messageBytes } = createAuthMessage() + const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') const signature = bs58.encode(sigBytes) - setSolWallet({ pubkey, message, signature }) + + // Hand credential to SDK — middleware injects headers automatically + sdk.walletAuth.setCredential({ publicKey: pubkey, message, signature }) + setWalletConnected(true) + setWalletPubkey(pubkey) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Wallet connection failed') } }, []) const handleDisconnectWallet = useCallback(async () => { + const sdk = getSDK() const phantom = getPhantom() if (phantom) await phantom.disconnect().catch(() => {}) - setSolWallet(null) + sdk.walletAuth.clearCredential() + setWalletConnected(false) + setWalletPubkey(null) }, []) // ------------------------------------------------------------------------- @@ -288,7 +253,6 @@ export default function App() { const handlePlay = useCallback( async (trackId: string) => { - // Toggle off if (playingId === trackId) { cleanupAudio() setPlayingId(null) @@ -300,20 +264,24 @@ export default function App() { setError(null) try { - let streamUrl: string - if (profile?.id) { - streamUrl = await streamWithOAuth(trackId, String(profile.id)) - } else if (solWallet) { - streamUrl = await streamWithWallet(trackId, solWallet) - } else { + if (!profile?.id && !walletConnected) { setError('Sign in with Audius or connect a Solana wallet to stream.') setStreamLoading(false) return } + // streamTrack works for both auth paths — OAuth token or wallet + // headers are injected automatically by SDK middleware + const sdk = getSDK() + const res = await sdk.tracks.streamTrack({ + trackId, + userId, + noRedirect: true + }) + if (!audioRef.current) audioRef.current = new Audio() const audio = audioRef.current - audio.src = streamUrl + audio.src = res.data audio.onended = () => { setPlayingId(null) cleanupAudio() @@ -331,7 +299,7 @@ export default function App() { setStreamLoading(false) } }, - [playingId, profile, solWallet, cleanupAudio] + [playingId, profile, walletConnected, userId, cleanupAudio] ) // ------------------------------------------------------------------------- @@ -359,7 +327,7 @@ export default function App() { // ------------------------------------------------------------------------- // Render: main // ------------------------------------------------------------------------- - const isAuthed = !!profile || !!solWallet + const isAuthed = !!profile || walletConnected const coinTicker = coin?.ticker ?? activeTicker return ( @@ -443,7 +411,7 @@ export default function App() { )} - {!solWallet ? ( + {!walletConnected ? ( From c841382b8a185ad0292e36a40ef693027d33ca25 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Thu, 26 Mar 2026 23:11:34 -0700 Subject: [PATCH 2/8] =?UTF-8?q?Rename=20walletAuth=20=E2=86=92=20solWallet?= =?UTF-8?q?,=20consolidate,=20simplify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - walletAuth/ (4 files) → solWallet/ (2 files) - createAuthMessage → createSolWalletSignatureMessage - addWalletAuthMiddleware → addSolWalletSignatureMiddleware - sdk.walletAuth → sdk.solWallet - Strip superfluous comments, add usage example to middleware docstring Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/sdk/createSdk.ts | 12 ++--- packages/sdk/src/sdk/index.ts | 2 +- .../addSolWalletSignatureMiddleware.ts | 52 +++++++++++++++++++ .../sdk/middleware/addWalletAuthMiddleware.ts | 42 --------------- packages/sdk/src/sdk/middleware/index.ts | 2 +- packages/sdk/src/sdk/solWallet/SolWallet.ts | 32 ++++++++++++ packages/sdk/src/sdk/solWallet/index.ts | 2 + packages/sdk/src/sdk/walletAuth/WalletAuth.ts | 33 ------------ packages/sdk/src/sdk/walletAuth/index.ts | 3 -- packages/sdk/src/sdk/walletAuth/message.ts | 13 ----- packages/sdk/src/sdk/walletAuth/types.ts | 9 ---- packages/web/examples/coin-gated/src/App.tsx | 11 ++-- 12 files changed, 98 insertions(+), 115 deletions(-) create mode 100644 packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts delete mode 100644 packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts create mode 100644 packages/sdk/src/sdk/solWallet/SolWallet.ts create mode 100644 packages/sdk/src/sdk/solWallet/index.ts delete mode 100644 packages/sdk/src/sdk/walletAuth/WalletAuth.ts delete mode 100644 packages/sdk/src/sdk/walletAuth/index.ts delete mode 100644 packages/sdk/src/sdk/walletAuth/message.ts delete mode 100644 packages/sdk/src/sdk/walletAuth/types.ts diff --git a/packages/sdk/src/sdk/createSdk.ts b/packages/sdk/src/sdk/createSdk.ts index 2d97ae606a6..1d5590e49fe 100644 --- a/packages/sdk/src/sdk/createSdk.ts +++ b/packages/sdk/src/sdk/createSdk.ts @@ -27,11 +27,11 @@ import { productionConfig } from './config/production' import { addAppInfoMiddleware, addRequestSignatureMiddleware, - addTokenRefreshMiddleware, - addWalletAuthMiddleware + addSolWalletSignatureMiddleware, + addTokenRefreshMiddleware } from './middleware' import { OAuth } from './oauth' -import { WalletAuth } from './walletAuth' +import { SolWallet } from './solWallet' import { TokenStoreLocalStorage } from './oauth/TokenStoreLocalStorage' import { Logger, Storage, StorageNodeSelector } from './services' import { SdkConfigSchema, type SdkConfig } from './types' @@ -74,7 +74,7 @@ export const createSdk = (config: SdkConfig) => { openUrl: services?.openUrl }) - const walletAuth = new WalletAuth() + const solWallet = new SolWallet() if (apiSecret || services?.audiusWalletClient) { middleware.push( @@ -103,7 +103,7 @@ export const createSdk = (config: SdkConfig) => { ) } - middleware.push(addWalletAuthMiddleware({ walletAuth })) + middleware.push(addSolWalletSignatureMiddleware({ solWallet })) // Auto-refresh middleware — intercepts 401s and retries with a fresh token. if (apiKey && oauth) { @@ -143,7 +143,7 @@ export const createSdk = (config: SdkConfig) => { return { oauth, - walletAuth, + solWallet, tokenStore, tracks: new TracksApi(apiConfig), users: usersApi, diff --git a/packages/sdk/src/sdk/index.ts b/packages/sdk/src/sdk/index.ts index a7a26333f83..2e2fc426ce0 100644 --- a/packages/sdk/src/sdk/index.ts +++ b/packages/sdk/src/sdk/index.ts @@ -59,7 +59,7 @@ export * from './services' export { productionConfig } from './config/production' export { developmentConfig } from './config/development' export * from './oauth/types' -export * from './walletAuth' +export * from './solWallet' export { ParseRequestError } from './utils/parseParams' export * from './utils/rendezvous' export * as Errors from './utils/errors' diff --git a/packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts b/packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts new file mode 100644 index 00000000000..94bb28b8d53 --- /dev/null +++ b/packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts @@ -0,0 +1,52 @@ +import type { + FetchParams, + Middleware, + RequestContext +} from '../api/generated/default' +import type { SolWallet } from '../solWallet' + +/** + * Injects X-Solana-* headers when a wallet credential is set. + * Skips if an Authorization header is already present (OAuth takes precedence). + * + * @example + * ```ts + * import { createSolWalletSignatureMessage } from '@audius/sdk' + * import bs58 from 'bs58' + * + * const sdk = getSDK() + * const { publicKey } = await phantom.connect() + * const { message, messageBytes } = createSolWalletSignatureMessage() + * const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') + * const signature = bs58.encode(sigBytes) + * + * sdk.solWallet.setCredential({ publicKey: publicKey.toString(), message, signature }) + * // All subsequent SDK calls now include wallet auth headers automatically. + * ``` + */ +export const addSolWalletSignatureMiddleware = ({ + solWallet +}: { + solWallet: SolWallet +}): Middleware => ({ + pre: async (context: RequestContext): Promise => { + const credential = solWallet.getCredential() + if (!credential) return context + + const headers = context.init.headers as Record + if (headers['Authorization']) return context + + return { + ...context, + init: { + ...context.init, + headers: { + ...headers, + 'X-Solana-Wallet': credential.publicKey, + 'X-Solana-Message': credential.message, + 'X-Solana-Signature': credential.signature + } + } + } + } +}) diff --git a/packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts b/packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts deleted file mode 100644 index e462f0d7999..00000000000 --- a/packages/sdk/src/sdk/middleware/addWalletAuthMiddleware.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { - FetchParams, - Middleware, - RequestContext -} from '../api/generated/default' -import type { WalletAuth } from '../walletAuth' - -const WALLET_HEADER = 'X-Solana-Wallet' -const MESSAGE_HEADER = 'X-Solana-Message' -const SIGNATURE_HEADER = 'X-Solana-Signature' - -/** - * Injects Solana wallet auth headers into every request when a credential - * is present. Skips if the request already has an Authorization header - * (i.e. OAuth takes precedence). - */ -export const addWalletAuthMiddleware = ({ - walletAuth -}: { - walletAuth: WalletAuth -}): Middleware => ({ - pre: async (context: RequestContext): Promise => { - const credential = walletAuth.getCredential() - if (!credential) return context - - const headers = context.init.headers as Record - if (headers['Authorization']) return context - - return { - ...context, - init: { - ...context.init, - headers: { - ...headers, - [WALLET_HEADER]: credential.publicKey, - [MESSAGE_HEADER]: credential.message, - [SIGNATURE_HEADER]: credential.signature - } - } - } - } -}) diff --git a/packages/sdk/src/sdk/middleware/index.ts b/packages/sdk/src/sdk/middleware/index.ts index e9f23cb1fce..f881dc05c51 100644 --- a/packages/sdk/src/sdk/middleware/index.ts +++ b/packages/sdk/src/sdk/middleware/index.ts @@ -1,4 +1,4 @@ export { addAppInfoMiddleware } from './addAppInfoMiddleware' export { addRequestSignatureMiddleware } from './addRequestSignatureMiddleware' +export { addSolWalletSignatureMiddleware } from './addSolWalletSignatureMiddleware' export { addTokenRefreshMiddleware } from './addTokenRefreshMiddleware' -export { addWalletAuthMiddleware } from './addWalletAuthMiddleware' diff --git a/packages/sdk/src/sdk/solWallet/SolWallet.ts b/packages/sdk/src/sdk/solWallet/SolWallet.ts new file mode 100644 index 00000000000..40b32686798 --- /dev/null +++ b/packages/sdk/src/sdk/solWallet/SolWallet.ts @@ -0,0 +1,32 @@ +export type SolWalletCredential = { + publicKey: string + message: string + signature: string +} + +export function createSolWalletSignatureMessage() { + const timestamp = Date.now() + const message = `audius:sol-wallet:${timestamp}` + const messageBytes = new TextEncoder().encode(message) + return { message, messageBytes, timestamp } +} + +export class SolWallet { + private credential: SolWalletCredential | null = null + + setCredential(credential: SolWalletCredential) { + this.credential = credential + } + + clearCredential() { + this.credential = null + } + + getCredential(): SolWalletCredential | null { + return this.credential + } + + isAuthenticated(): boolean { + return this.credential !== null + } +} diff --git a/packages/sdk/src/sdk/solWallet/index.ts b/packages/sdk/src/sdk/solWallet/index.ts new file mode 100644 index 00000000000..15b6a5722aa --- /dev/null +++ b/packages/sdk/src/sdk/solWallet/index.ts @@ -0,0 +1,2 @@ +export { SolWallet, createSolWalletSignatureMessage } from './SolWallet' +export type { SolWalletCredential } from './SolWallet' diff --git a/packages/sdk/src/sdk/walletAuth/WalletAuth.ts b/packages/sdk/src/sdk/walletAuth/WalletAuth.ts deleted file mode 100644 index 722b6b64b19..00000000000 --- a/packages/sdk/src/sdk/walletAuth/WalletAuth.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { WalletAuthCredential } from './types' - -/** - * Manages Solana wallet authentication credentials. - * - * The SDK does not connect wallets directly — apps use whatever wallet - * adapter they prefer, sign the message from `createAuthMessage()`, and - * hand the credential here. The SDK then injects the appropriate headers - * into all API requests via middleware. - */ -export class WalletAuth { - private credential: WalletAuthCredential | null = null - - /** Store a signed wallet credential. */ - setCredential(credential: WalletAuthCredential) { - this.credential = credential - } - - /** Clear the stored credential (disconnect). */ - clearCredential() { - this.credential = null - } - - /** Returns the current credential, or null if not authenticated. */ - getCredential(): WalletAuthCredential | null { - return this.credential - } - - /** Whether a wallet credential is currently set. */ - isAuthenticated(): boolean { - return this.credential !== null - } -} diff --git a/packages/sdk/src/sdk/walletAuth/index.ts b/packages/sdk/src/sdk/walletAuth/index.ts deleted file mode 100644 index 1f85de2f696..00000000000 --- a/packages/sdk/src/sdk/walletAuth/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { WalletAuth } from './WalletAuth' -export { createAuthMessage } from './message' -export type { WalletAuthCredential } from './types' diff --git a/packages/sdk/src/sdk/walletAuth/message.ts b/packages/sdk/src/sdk/walletAuth/message.ts deleted file mode 100644 index 63861f15de1..00000000000 --- a/packages/sdk/src/sdk/walletAuth/message.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Creates the canonical message for Solana wallet authentication. - * - * The message embeds a timestamp so the API can enforce expiry in the future. - * Apps sign this with the user's wallet and pass the result to - * `WalletAuth.setCredential()`. - */ -export function createAuthMessage() { - const timestamp = Date.now() - const message = `audius:wallet-auth:${timestamp}` - const messageBytes = new TextEncoder().encode(message) - return { message, messageBytes, timestamp } -} diff --git a/packages/sdk/src/sdk/walletAuth/types.ts b/packages/sdk/src/sdk/walletAuth/types.ts deleted file mode 100644 index 8261df896fb..00000000000 --- a/packages/sdk/src/sdk/walletAuth/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** Credential tuple produced by signing an SDK-generated message with a Solana wallet. */ -export type WalletAuthCredential = { - /** Base58-encoded Solana public key */ - publicKey: string - /** The message that was signed (from `createAuthMessage`) */ - message: string - /** Base58-encoded ed25519 signature */ - signature: string -} diff --git a/packages/web/examples/coin-gated/src/App.tsx b/packages/web/examples/coin-gated/src/App.tsx index 7b9f5d74c29..538b82eeb63 100644 --- a/packages/web/examples/coin-gated/src/App.tsx +++ b/packages/web/examples/coin-gated/src/App.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { createAuthMessage } from '@audius/sdk' +import { createSolWalletSignatureMessage } from '@audius/sdk' import bs58 from 'bs58' import { config } from './config' @@ -218,13 +218,10 @@ export default function App() { const { publicKey } = await phantom.connect() const pubkey = publicKey.toString() - // SDK owns the message format; app just signs it - const { message, messageBytes } = createAuthMessage() + const { message, messageBytes } = createSolWalletSignatureMessage() const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') const signature = bs58.encode(sigBytes) - - // Hand credential to SDK — middleware injects headers automatically - sdk.walletAuth.setCredential({ publicKey: pubkey, message, signature }) + sdk.solWallet.setCredential({ publicKey: pubkey, message, signature }) setWalletConnected(true) setWalletPubkey(pubkey) } catch (e: unknown) { @@ -236,7 +233,7 @@ export default function App() { const sdk = getSDK() const phantom = getPhantom() if (phantom) await phantom.disconnect().catch(() => {}) - sdk.walletAuth.clearCredential() + sdk.solWallet.clearCredential() setWalletConnected(false) setWalletPubkey(null) }, []) From 3875ab7dead5927a5609e021ff3c706d1f1e942f Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Thu, 26 Mar 2026 23:20:41 -0700 Subject: [PATCH 3/8] =?UTF-8?q?Rename=20solWallet=20=E2=86=92=20solanaWall?= =?UTF-8?q?et=20throughout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/sdk/createSdk.ts | 10 +++++----- packages/sdk/src/sdk/index.ts | 2 +- ...ts => addSolanaWalletSignatureMiddleware.ts} | 17 ++++++++--------- packages/sdk/src/sdk/middleware/index.ts | 2 +- packages/sdk/src/sdk/solWallet/index.ts | 2 -- .../SolanaWallet.ts} | 14 +++++++------- packages/sdk/src/sdk/solanaWallet/index.ts | 2 ++ packages/web/examples/coin-gated/src/App.tsx | 8 ++++---- 8 files changed, 28 insertions(+), 29 deletions(-) rename packages/sdk/src/sdk/middleware/{addSolWalletSignatureMiddleware.ts => addSolanaWalletSignatureMiddleware.ts} (72%) delete mode 100644 packages/sdk/src/sdk/solWallet/index.ts rename packages/sdk/src/sdk/{solWallet/SolWallet.ts => solanaWallet/SolanaWallet.ts} (53%) create mode 100644 packages/sdk/src/sdk/solanaWallet/index.ts diff --git a/packages/sdk/src/sdk/createSdk.ts b/packages/sdk/src/sdk/createSdk.ts index 1d5590e49fe..b29e9c72e5e 100644 --- a/packages/sdk/src/sdk/createSdk.ts +++ b/packages/sdk/src/sdk/createSdk.ts @@ -27,11 +27,11 @@ import { productionConfig } from './config/production' import { addAppInfoMiddleware, addRequestSignatureMiddleware, - addSolWalletSignatureMiddleware, + addSolanaWalletSignatureMiddleware, addTokenRefreshMiddleware } from './middleware' import { OAuth } from './oauth' -import { SolWallet } from './solWallet' +import { SolanaWallet } from './solanaWallet' import { TokenStoreLocalStorage } from './oauth/TokenStoreLocalStorage' import { Logger, Storage, StorageNodeSelector } from './services' import { SdkConfigSchema, type SdkConfig } from './types' @@ -74,7 +74,7 @@ export const createSdk = (config: SdkConfig) => { openUrl: services?.openUrl }) - const solWallet = new SolWallet() + const solanaWallet = new SolanaWallet() if (apiSecret || services?.audiusWalletClient) { middleware.push( @@ -103,7 +103,7 @@ export const createSdk = (config: SdkConfig) => { ) } - middleware.push(addSolWalletSignatureMiddleware({ solWallet })) + middleware.push(addSolanaWalletSignatureMiddleware({ solanaWallet })) // Auto-refresh middleware — intercepts 401s and retries with a fresh token. if (apiKey && oauth) { @@ -143,7 +143,7 @@ export const createSdk = (config: SdkConfig) => { return { oauth, - solWallet, + solanaWallet, tokenStore, tracks: new TracksApi(apiConfig), users: usersApi, diff --git a/packages/sdk/src/sdk/index.ts b/packages/sdk/src/sdk/index.ts index 2e2fc426ce0..d025b6cec76 100644 --- a/packages/sdk/src/sdk/index.ts +++ b/packages/sdk/src/sdk/index.ts @@ -59,7 +59,7 @@ export * from './services' export { productionConfig } from './config/production' export { developmentConfig } from './config/development' export * from './oauth/types' -export * from './solWallet' +export * from './solanaWallet' export { ParseRequestError } from './utils/parseParams' export * from './utils/rendezvous' export * as Errors from './utils/errors' diff --git a/packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts similarity index 72% rename from packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts rename to packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts index 94bb28b8d53..7fd72fbce0d 100644 --- a/packages/sdk/src/sdk/middleware/addSolWalletSignatureMiddleware.ts +++ b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts @@ -3,7 +3,7 @@ import type { Middleware, RequestContext } from '../api/generated/default' -import type { SolWallet } from '../solWallet' +import type { SolanaWallet } from '../solanaWallet' /** * Injects X-Solana-* headers when a wallet credential is set. @@ -11,26 +11,25 @@ import type { SolWallet } from '../solWallet' * * @example * ```ts - * import { createSolWalletSignatureMessage } from '@audius/sdk' + * import { createSolanaWalletSignatureMessage } from '@audius/sdk' * import bs58 from 'bs58' * * const sdk = getSDK() * const { publicKey } = await phantom.connect() - * const { message, messageBytes } = createSolWalletSignatureMessage() + * const { message, messageBytes } = createSolanaWalletSignatureMessage() * const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') * const signature = bs58.encode(sigBytes) * - * sdk.solWallet.setCredential({ publicKey: publicKey.toString(), message, signature }) - * // All subsequent SDK calls now include wallet auth headers automatically. + * sdk.solanaWallet.setCredential({ publicKey: publicKey.toString(), message, signature }) * ``` */ -export const addSolWalletSignatureMiddleware = ({ - solWallet +export const addSolanaWalletSignatureMiddleware = ({ + solanaWallet }: { - solWallet: SolWallet + solanaWallet: SolanaWallet }): Middleware => ({ pre: async (context: RequestContext): Promise => { - const credential = solWallet.getCredential() + const credential = solanaWallet.getCredential() if (!credential) return context const headers = context.init.headers as Record diff --git a/packages/sdk/src/sdk/middleware/index.ts b/packages/sdk/src/sdk/middleware/index.ts index f881dc05c51..fe0caaa2244 100644 --- a/packages/sdk/src/sdk/middleware/index.ts +++ b/packages/sdk/src/sdk/middleware/index.ts @@ -1,4 +1,4 @@ export { addAppInfoMiddleware } from './addAppInfoMiddleware' export { addRequestSignatureMiddleware } from './addRequestSignatureMiddleware' -export { addSolWalletSignatureMiddleware } from './addSolWalletSignatureMiddleware' +export { addSolanaWalletSignatureMiddleware } from './addSolanaWalletSignatureMiddleware' export { addTokenRefreshMiddleware } from './addTokenRefreshMiddleware' diff --git a/packages/sdk/src/sdk/solWallet/index.ts b/packages/sdk/src/sdk/solWallet/index.ts deleted file mode 100644 index 15b6a5722aa..00000000000 --- a/packages/sdk/src/sdk/solWallet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { SolWallet, createSolWalletSignatureMessage } from './SolWallet' -export type { SolWalletCredential } from './SolWallet' diff --git a/packages/sdk/src/sdk/solWallet/SolWallet.ts b/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts similarity index 53% rename from packages/sdk/src/sdk/solWallet/SolWallet.ts rename to packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts index 40b32686798..50537ca8b96 100644 --- a/packages/sdk/src/sdk/solWallet/SolWallet.ts +++ b/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts @@ -1,20 +1,20 @@ -export type SolWalletCredential = { +export type SolanaWalletCredential = { publicKey: string message: string signature: string } -export function createSolWalletSignatureMessage() { +export function createSolanaWalletSignatureMessage() { const timestamp = Date.now() - const message = `audius:sol-wallet:${timestamp}` + const message = `audius:solana-wallet:${timestamp}` const messageBytes = new TextEncoder().encode(message) return { message, messageBytes, timestamp } } -export class SolWallet { - private credential: SolWalletCredential | null = null +export class SolanaWallet { + private credential: SolanaWalletCredential | null = null - setCredential(credential: SolWalletCredential) { + setCredential(credential: SolanaWalletCredential) { this.credential = credential } @@ -22,7 +22,7 @@ export class SolWallet { this.credential = null } - getCredential(): SolWalletCredential | null { + getCredential(): SolanaWalletCredential | null { return this.credential } diff --git a/packages/sdk/src/sdk/solanaWallet/index.ts b/packages/sdk/src/sdk/solanaWallet/index.ts new file mode 100644 index 00000000000..7092f143b4a --- /dev/null +++ b/packages/sdk/src/sdk/solanaWallet/index.ts @@ -0,0 +1,2 @@ +export { SolanaWallet, createSolanaWalletSignatureMessage } from './SolanaWallet' +export type { SolanaWalletCredential } from './SolanaWallet' diff --git a/packages/web/examples/coin-gated/src/App.tsx b/packages/web/examples/coin-gated/src/App.tsx index 538b82eeb63..0e55d18e79f 100644 --- a/packages/web/examples/coin-gated/src/App.tsx +++ b/packages/web/examples/coin-gated/src/App.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { createSolWalletSignatureMessage } from '@audius/sdk' +import { createSolanaWalletSignatureMessage } from '@audius/sdk' import bs58 from 'bs58' import { config } from './config' @@ -218,10 +218,10 @@ export default function App() { const { publicKey } = await phantom.connect() const pubkey = publicKey.toString() - const { message, messageBytes } = createSolWalletSignatureMessage() + const { message, messageBytes } = createSolanaWalletSignatureMessage() const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') const signature = bs58.encode(sigBytes) - sdk.solWallet.setCredential({ publicKey: pubkey, message, signature }) + sdk.solanaWallet.setCredential({ publicKey: pubkey, message, signature }) setWalletConnected(true) setWalletPubkey(pubkey) } catch (e: unknown) { @@ -233,7 +233,7 @@ export default function App() { const sdk = getSDK() const phantom = getPhantom() if (phantom) await phantom.disconnect().catch(() => {}) - sdk.solWallet.clearCredential() + sdk.solanaWallet.clearCredential() setWalletConnected(false) setWalletPubkey(null) }, []) From f9b9f21686fcb19022f1b9041a8fb561adfa969d Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Thu, 26 Mar 2026 23:27:12 -0700 Subject: [PATCH 4/8] Clean up --- .../addSolanaWalletSignatureMiddleware.ts | 14 ++++++++------ packages/web/examples/coin-gated/src/App.tsx | 2 -- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts index 7fd72fbce0d..095b2129cbb 100644 --- a/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts +++ b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts @@ -11,16 +11,18 @@ import type { SolanaWallet } from '../solanaWallet' * * @example * ```ts - * import { createSolanaWalletSignatureMessage } from '@audius/sdk' + * import { createSolanaWalletSignatureMessage, sdk as audiusSdk } from '@audius/sdk' * import bs58 from 'bs58' * - * const sdk = getSDK() - * const { publicKey } = await phantom.connect() + * const sdk = audiusSdk({ appName: APP_NAME }) + * const wallet = window.solana + * const { publicKey } = await solanaWallet.connect() * const { message, messageBytes } = createSolanaWalletSignatureMessage() - * const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') + * const { signature: sigBytes } = await wallet.signMessage(messageBytes, 'utf8') * const signature = bs58.encode(sigBytes) - * * sdk.solanaWallet.setCredential({ publicKey: publicKey.toString(), message, signature }) + * + * sdk.tracks.getTrack({ id: '123' }) * ``` */ export const addSolanaWalletSignatureMiddleware = ({ @@ -33,7 +35,7 @@ export const addSolanaWalletSignatureMiddleware = ({ if (!credential) return context const headers = context.init.headers as Record - if (headers['Authorization']) return context + if (headers.Authorization) return context return { ...context, diff --git a/packages/web/examples/coin-gated/src/App.tsx b/packages/web/examples/coin-gated/src/App.tsx index 0e55d18e79f..da8959d1536 100644 --- a/packages/web/examples/coin-gated/src/App.tsx +++ b/packages/web/examples/coin-gated/src/App.tsx @@ -267,8 +267,6 @@ export default function App() { return } - // streamTrack works for both auth paths — OAuth token or wallet - // headers are injected automatically by SDK middleware const sdk = getSDK() const res = await sdk.tracks.streamTrack({ trackId, From 9a51a692d9aa4c51296c3c02dade008d1a37b9e7 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 27 Mar 2026 15:32:17 -0700 Subject: [PATCH 5/8] Add sdk.solanaWallet.auth() for one-line wallet connect Moves connect + message creation + signing + base58 encoding into SolanaWallet.auth(provider), so consumers just pass window.solana (or any wallet adapter). Includes a minimal inline base58 encoder to avoid adding a dependency. Co-Authored-By: Claude Opus 4.6 --- .../addSolanaWalletSignatureMiddleware.ts | 13 ++--- .../sdk/src/sdk/solanaWallet/SolanaWallet.ts | 47 +++++++++++++++++++ packages/sdk/src/sdk/solanaWallet/index.ts | 10 +++- packages/web/examples/coin-gated/src/App.tsx | 31 ++++-------- 4 files changed, 67 insertions(+), 34 deletions(-) diff --git a/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts index 095b2129cbb..5a350f07d5c 100644 --- a/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts +++ b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts @@ -11,17 +11,10 @@ import type { SolanaWallet } from '../solanaWallet' * * @example * ```ts - * import { createSolanaWalletSignatureMessage, sdk as audiusSdk } from '@audius/sdk' - * import bs58 from 'bs58' - * - * const sdk = audiusSdk({ appName: APP_NAME }) - * const wallet = window.solana - * const { publicKey } = await solanaWallet.connect() - * const { message, messageBytes } = createSolanaWalletSignatureMessage() - * const { signature: sigBytes } = await wallet.signMessage(messageBytes, 'utf8') - * const signature = bs58.encode(sigBytes) - * sdk.solanaWallet.setCredential({ publicKey: publicKey.toString(), message, signature }) + * import { sdk as audiusSdk } from '@audius/sdk' * + * const sdk = audiusSdk({ appName: 'MyApp' }) + * await sdk.solanaWallet.auth(window.solana) * sdk.tracks.getTrack({ id: '123' }) * ``` */ diff --git a/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts b/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts index 50537ca8b96..c0c0a1d7115 100644 --- a/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts +++ b/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts @@ -1,9 +1,40 @@ +const BASE58_ALPHABET = + '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + +function encodeBase58(bytes: Uint8Array): string { + let result = '' + let value = BigInt( + '0x' + Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') + ) + while (value > 0n) { + result = BASE58_ALPHABET[Number(value % 58n)] + result + value /= 58n + } + for (const b of bytes) { + if (b !== 0) break + result = '1' + result + } + return result +} + export type SolanaWalletCredential = { publicKey: string message: string signature: string } +/** + * Minimal interface for a Solana wallet provider (Phantom, Solflare, etc.). + * Apps pass their connected wallet to `SolanaWallet.auth()`. + */ +export type SolanaWalletProvider = { + connect(): Promise<{ publicKey: { toString(): string } }> + signMessage( + message: Uint8Array, + encoding: string + ): Promise<{ signature: Uint8Array }> +} + export function createSolanaWalletSignatureMessage() { const timestamp = Date.now() const message = `audius:solana-wallet:${timestamp}` @@ -14,6 +45,22 @@ export function createSolanaWalletSignatureMessage() { export class SolanaWallet { private credential: SolanaWalletCredential | null = null + /** Connect a wallet, sign the auth message, and store the credential. */ + async auth(provider: SolanaWalletProvider) { + const { publicKey } = await provider.connect() + const { message, messageBytes } = createSolanaWalletSignatureMessage() + const { signature: sigBytes } = await provider.signMessage( + messageBytes, + 'utf8' + ) + this.credential = { + publicKey: publicKey.toString(), + message, + signature: encodeBase58(sigBytes) + } + return { publicKey: this.credential.publicKey } + } + setCredential(credential: SolanaWalletCredential) { this.credential = credential } diff --git a/packages/sdk/src/sdk/solanaWallet/index.ts b/packages/sdk/src/sdk/solanaWallet/index.ts index 7092f143b4a..d2017d52143 100644 --- a/packages/sdk/src/sdk/solanaWallet/index.ts +++ b/packages/sdk/src/sdk/solanaWallet/index.ts @@ -1,2 +1,8 @@ -export { SolanaWallet, createSolanaWalletSignatureMessage } from './SolanaWallet' -export type { SolanaWalletCredential } from './SolanaWallet' +export { + SolanaWallet, + createSolanaWalletSignatureMessage +} from './SolanaWallet' +export type { + SolanaWalletCredential, + SolanaWalletProvider +} from './SolanaWallet' diff --git a/packages/web/examples/coin-gated/src/App.tsx b/packages/web/examples/coin-gated/src/App.tsx index da8959d1536..9c39382aff0 100644 --- a/packages/web/examples/coin-gated/src/App.tsx +++ b/packages/web/examples/coin-gated/src/App.tsx @@ -1,7 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' -import { createSolanaWalletSignatureMessage } from '@audius/sdk' -import bs58 from 'bs58' import { config } from './config' import { getSDK } from './sdk' @@ -29,19 +27,15 @@ type TrackItem = { } // --------------------------------------------------------------------------- -// Phantom / Solana wallet adapter +// Phantom wallet detection // --------------------------------------------------------------------------- -interface PhantomProvider { - isPhantom?: boolean - connect(): Promise<{ publicKey: { toBytes(): Uint8Array; toString(): string } }> - signMessage(message: Uint8Array, encoding: string): Promise<{ signature: Uint8Array }> - disconnect(): Promise -} - -function getPhantom(): PhantomProvider | null { +function getPhantom() { if (typeof window !== 'undefined' && 'solana' in window) { - const provider = (window as Record).solana as PhantomProvider + const provider = (window as Record).solana as { + isPhantom?: boolean + disconnect(): Promise + } if (provider?.isPhantom) return provider } return null @@ -208,22 +202,15 @@ export default function App() { const handleConnectWallet = useCallback(async () => { setError(null) - const phantom = getPhantom() - if (!phantom) { + if (!getPhantom()) { setError('Phantom wallet not found. Install phantom.app to use wallet sign-in.') return } try { const sdk = getSDK() - const { publicKey } = await phantom.connect() - const pubkey = publicKey.toString() - - const { message, messageBytes } = createSolanaWalletSignatureMessage() - const { signature: sigBytes } = await phantom.signMessage(messageBytes, 'utf8') - const signature = bs58.encode(sigBytes) - sdk.solanaWallet.setCredential({ publicKey: pubkey, message, signature }) + const { publicKey } = await sdk.solanaWallet.auth(window.solana) setWalletConnected(true) - setWalletPubkey(pubkey) + setWalletPubkey(publicKey) } catch (e: unknown) { setError(e instanceof Error ? e.message : 'Wallet connection failed') } From fe2dabf3d257a01a4c2fca71b58304b948a5fe19 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 27 Mar 2026 15:49:52 -0700 Subject: [PATCH 6/8] Use bs58 import instead of inline base58 encoder Co-Authored-By: Claude Opus 4.6 --- .../sdk/src/sdk/solanaWallet/SolanaWallet.ts | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts b/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts index c0c0a1d7115..480c4c5252d 100644 --- a/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts +++ b/packages/sdk/src/sdk/solanaWallet/SolanaWallet.ts @@ -1,21 +1,4 @@ -const BASE58_ALPHABET = - '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' - -function encodeBase58(bytes: Uint8Array): string { - let result = '' - let value = BigInt( - '0x' + Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') - ) - while (value > 0n) { - result = BASE58_ALPHABET[Number(value % 58n)] + result - value /= 58n - } - for (const b of bytes) { - if (b !== 0) break - result = '1' + result - } - return result -} +import bs58 from 'bs58' export type SolanaWalletCredential = { publicKey: string @@ -45,7 +28,6 @@ export function createSolanaWalletSignatureMessage() { export class SolanaWallet { private credential: SolanaWalletCredential | null = null - /** Connect a wallet, sign the auth message, and store the credential. */ async auth(provider: SolanaWalletProvider) { const { publicKey } = await provider.connect() const { message, messageBytes } = createSolanaWalletSignatureMessage() @@ -56,7 +38,7 @@ export class SolanaWallet { this.credential = { publicKey: publicKey.toString(), message, - signature: encodeBase58(sigBytes) + signature: bs58.encode(sigBytes) } return { publicKey: this.credential.publicKey } } From e58df77f6b8341cc6243f89f3a796d0c6087d62d Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 27 Mar 2026 19:37:59 -0700 Subject: [PATCH 7/8] Fix wallet headers not sent alongside OAuth and stale query cache - Remove Authorization guard in SDK middleware so wallet headers are sent alongside OAuth tokens (API needs both to merge balances) - Add walletConnected to useGatedTracks query key so React Query refetches after wallet connect instead of serving stale cache Co-Authored-By: Claude Opus 4.6 --- .../sdk/middleware/addSolanaWalletSignatureMiddleware.ts | 4 +--- packages/web/examples/coin-gated/src/App.tsx | 6 +++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts index 5a350f07d5c..e14881fa14e 100644 --- a/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts +++ b/packages/sdk/src/sdk/middleware/addSolanaWalletSignatureMiddleware.ts @@ -7,7 +7,7 @@ import type { SolanaWallet } from '../solanaWallet' /** * Injects X-Solana-* headers when a wallet credential is set. - * Skips if an Authorization header is already present (OAuth takes precedence). + * Works alongside OAuth — both can be present so the API merges balances. * * @example * ```ts @@ -28,8 +28,6 @@ export const addSolanaWalletSignatureMiddleware = ({ if (!credential) return context const headers = context.init.headers as Record - if (headers.Authorization) return context - return { ...context, init: { diff --git a/packages/web/examples/coin-gated/src/App.tsx b/packages/web/examples/coin-gated/src/App.tsx index 9c39382aff0..94a308b542e 100644 --- a/packages/web/examples/coin-gated/src/App.tsx +++ b/packages/web/examples/coin-gated/src/App.tsx @@ -95,10 +95,10 @@ function useCoinBalance( // Hooks: useGatedTracks // --------------------------------------------------------------------------- -function useGatedTracks(artistId: string | undefined, userId: string | undefined) { +function useGatedTracks(artistId: string | undefined, userId: string | undefined, walletConnected: boolean) { const sdk = getSDK() return useQuery({ - queryKey: ['gated-tracks', artistId, userId], + queryKey: ['gated-tracks', artistId, userId, walletConnected], queryFn: async () => { const res = await sdk.users.getTracksByUser({ id: artistId!, @@ -140,7 +140,7 @@ export default function App() { data: tracks, isPending: tracksPending, error: tracksError - } = useGatedTracks(artistId, userId) + } = useGatedTracks(artistId, userId, walletConnected) const coinMint = coin?.mint const { data: coinBalance } = useCoinBalance(userId, walletPubkey ?? undefined, coinMint) From 66f0ffb304816821e1d283c81e176fb93cd29170 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Fri, 27 Mar 2026 20:00:04 -0700 Subject: [PATCH 8/8] Add solanaWallet to createSdkWithServices Co-Authored-By: Claude Opus 4.6 --- packages/sdk/src/sdk/createSdkWithServices.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/sdk/src/sdk/createSdkWithServices.ts b/packages/sdk/src/sdk/createSdkWithServices.ts index 161ca92e34e..0a6bfcb6c99 100644 --- a/packages/sdk/src/sdk/createSdkWithServices.ts +++ b/packages/sdk/src/sdk/createSdkWithServices.ts @@ -31,6 +31,7 @@ import { productionConfig } from './config/production' import { addAppInfoMiddleware, addRequestSignatureMiddleware, + addSolanaWalletSignatureMiddleware, addTokenRefreshMiddleware } from './middleware' import { OAuth } from './oauth' @@ -70,6 +71,7 @@ import { StorageNodeSelector, getDefaultStorageNodeSelectorConfig } from './services/StorageNodeSelector' +import { SolanaWallet } from './solanaWallet' import { SdkConfig, SdkConfigSchema, ServicesContainer } from './types' import fetch from './utils/fetch' @@ -351,6 +353,8 @@ const initializeApis = ({ : productionConfig.network.apiEndpoint const basePath = `${apiEndpoint}/v1` + const solanaWallet = new SolanaWallet() + const middleware = [ addAppInfoMiddleware({ apiKey, @@ -362,7 +366,8 @@ const initializeApis = ({ services, apiKey, apiSecret - }) + }), + addSolanaWalletSignatureMiddleware({ solanaWallet }) ] // Token store for PKCE flow — provides dynamic accessToken to Configuration @@ -453,6 +458,7 @@ const initializeApis = ({ return { oauth, + solanaWallet, tokenStore, tracks, users,