@rakurai/rakurai-stake-sdk
v1.3.0
Published
TypeScript SDK for Rakurai liquid SOL staking on Solana. Ships a ready-made React modal and a framework-agnostic core for custom integrations.
Maintainers
Readme
@rakurai/rakurai-stake-sdk
A TypeScript SDK for Rakurai staking on Solana. It provides two things:
- Liquid staking (raiSOL). Helper functions and a ready-made React modal for staking and unstaking SOL through Rakurai's liquid staking pool.
- Auto-compound. Tools for validators running the Rakurai client to let their stakers turn on auto-compounding, which restakes rewards automatically every epoch.
You keep full control of signing and sending transactions. The SDK never holds private keys. It calls Rakurai's HTTP API where needed, hands you standard @solana/web3.js transactions, and the auto-compound instruction builder runs fully offline.
Contents
- Requirements
- Installation
- Package entry points
- Liquid staking modal (React)
- Auto-compound
- API reference
- Wallet integration
Requirements
@solana/web3.js^1.98.0(peer dependency).- For the React modal: React
>=18, plus@solana/wallet-adapter-react,@solana/wallet-adapter-react-ui, and@solana/wallet-adapter-wallets. - Auto-compound works on Solana mainnet only, and only for stake delegated to a validator that runs the Rakurai client.
Installation
Core SDK (framework-agnostic, works in the browser or in Node):
npm install @rakurai/rakurai-stake-sdk @solana/web3.jsReact modal (adds the wallet-adapter packages):
npm install @rakurai/rakurai-stake-sdk @solana/web3.js \
@solana/wallet-adapter-react @solana/wallet-adapter-react-ui \
@solana/wallet-adapter-walletsPackage entry points
The SDK ships two independent entry points. Import from the one you need.
| Import path | Contents | Environment |
|---|---|---|
| @rakurai/rakurai-stake-sdk | API functions, types, and the auto-compound instruction builder | Browser or Node |
| @rakurai/rakurai-stake-sdk/react | The React provider, hooks, and UI components | Browser (React) |
| @rakurai/rakurai-stake-sdk/react/styles.css | Stylesheet for the React components | Import once in your app |
The core entry has no React or wallet-adapter dependency, so it is safe to use in a backend. Brand images are inlined into the React bundle, so there are no asset files to copy into your project.
Liquid staking modal (React)
The modal handles wallet connection, balances, APY and price display, and the stake and unstake transactions. You wire up the provider and open the modal; the SDK does the rest.
What you can customize
You can change the title, description, and button labels; the header logo and token icons; all colors (background, text, the call-to-action gradient, borders, tabs, and overlay); the Learn More link; the default unstake mode; and your referral code.
The Powered by Rakurai footer is always shown and cannot be changed or hidden. This is the only fixed element.
Step 1: Set up wallet providers
Set up the Solana wallet adapters once, near the root of your app, and import both stylesheets. Add any wallets you want to support to the wallets array; they appear automatically in the modal's wallet picker.
// main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
BackpackWalletAdapter,
} from '@solana/wallet-adapter-wallets';
import '@solana/wallet-adapter-react-ui/styles.css';
import '@rakurai/rakurai-stake-sdk/react/styles.css';
import { App } from './App';
const wallets = [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
new BackpackWalletAdapter(),
];
ReactDOM.createRoot(document.getElementById('root')!).render(
<ConnectionProvider endpoint="https://api.mainnet-beta.solana.com">
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<App />
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);The SDK works with any wallet that supports the Solana Wallet Adapter standard (Phantom, Solflare, Backpack, Glow, Ledger, Coinbase Wallet, and others). To offer more wallets, add their adapters to the wallets array. No other change is required.
Use a dedicated RPC endpoint (for example Helius, Triton, QuickNode, or Alchemy) for production. Solana's shared public endpoint (https://api.mainnet-beta.solana.com) throttles browser traffic and can return errors under load.
Step 2: Add the provider and open the modal
Wrap your app in RakuraiStakeProvider and open the modal from any component inside it.
// App.tsx
import {
RakuraiStakeProvider,
useWalletAdapterBridge,
useRakuraiStakeModal,
} from '@rakurai/rakurai-stake-sdk/react';
function StakeButton() {
const { open } = useRakuraiStakeModal();
return <button onClick={() => open('stake')}>Stake SOL</button>;
}
export function App() {
const walletBridge = useWalletAdapterBridge();
return (
<RakuraiStakeProvider wallet={walletBridge}>
<StakeButton />
{/* your existing page content */}
</RakuraiStakeProvider>
);
}That completes the integration. When the user opens the modal, the SDK does the following automatically:
- Opens the wallet picker on Connect Wallet, and shows Disconnect Wallet once connected.
- Fetches SOL and raiSOL balances, APY, and the raiSOL price.
- Builds, signs, sends, and confirms stake and unstake transactions.
- Refreshes balances after a successful transaction.
Call open('unstake') to open the unstake tab instead of the stake tab.
Step 3: Customize the theme (optional)
Pass a theme object to match your branding. Every field is optional; omit any you do not need.
<RakuraiStakeProvider
wallet={walletBridge}
theme={{
// Text and labels
title: 'Stake with MyApp',
description: 'Earn raiSOL yield, compounded every epoch.',
stakeBtnLabel: 'Stake SOL',
unstakeBtnLabel: 'Unstake',
connectBtnLabel: 'Connect Wallet',
disconnectBtnLabel: 'Disconnect Wallet',
learnMoreLabel: 'Learn More',
learnMoreUrl: 'https://yourapp.com/staking',
// Icons
logoUrl: 'https://yourapp.com/logo.png',
solTokenIconUrl: 'https://yourapp.com/sol.png',
raiSolTokenIconUrl: 'https://yourapp.com/raisol.png',
// Colors
colors: {
bg: 'hsl(222 47% 11%)', // modal background
text: 'hsl(210 40% 98%)', // primary text
textMuted: 'hsl(215 20% 65%)', // secondary text
ctaFrom: 'rgb(59, 130, 246)', // CTA gradient start
ctaTo: 'rgb(34, 197, 94)', // CTA gradient end
ctaText: '#ffffff', // CTA button text
inputBg: 'hsl(217 33% 17%)', // input background
borderColor: 'hsl(217 33% 20%)', // modal and input borders
tabActiveBg: 'linear-gradient', // active tab indicator
overlay: 'rgba(0,0,0,0.75)', // backdrop
zIndex: 1000, // keep at 1000 so the wallet picker (1040) stays on top
},
}}
>
<StakeButton />
</RakuraiStakeProvider>To adjust the modal size, override these CSS custom properties on .rakurai-stake-modal-root in your own stylesheet:
.rakurai-stake-modal-root {
--rakurai-modal-min-height: 600px; /* default 480px */
--rakurai-modal-max-width: 520px; /* default 480px */
}The modal is responsive: it expands when content needs more room, and on small screens it reduces padding and caps its height to fit the viewport.
Step 4: Other provider props (optional)
<RakuraiStakeProvider
wallet={walletBridge}
unstakeMode="native" // 'native' (unstakes over an epoch, no fee) or 'jupiter' (instant swap)
referralCode="MYPARTNER" // reports confirmed stakes and unstakes to the Rakurai campaign API
theme={{ /* see Step 3 */ }}
>
<YourApp />
</RakuraiStakeProvider>Next.js (App Router)
Add "use client" to any file that imports from the SDK, and place the wallet and Rakurai providers in a client component.
// components/Providers.tsx
'use client';
import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react';
import { WalletModalProvider } from '@solana/wallet-adapter-react-ui';
import {
PhantomWalletAdapter,
SolflareWalletAdapter,
BackpackWalletAdapter,
} from '@solana/wallet-adapter-wallets';
import { RakuraiStakeProvider, useWalletAdapterBridge } from '@rakurai/rakurai-stake-sdk/react';
import '@solana/wallet-adapter-react-ui/styles.css';
import '@rakurai/rakurai-stake-sdk/react/styles.css';
const wallets = [
new PhantomWalletAdapter(),
new SolflareWalletAdapter(),
new BackpackWalletAdapter(),
];
function RakuraiWrapper({ children }: { children: React.ReactNode }) {
const walletBridge = useWalletAdapterBridge();
return <RakuraiStakeProvider wallet={walletBridge}>{children}</RakuraiStakeProvider>;
}
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ConnectionProvider endpoint="https://api.mainnet-beta.solana.com">
<WalletProvider wallets={wallets} autoConnect>
<WalletModalProvider>
<RakuraiWrapper>{children}</RakuraiWrapper>
</WalletModalProvider>
</WalletProvider>
</ConnectionProvider>
);
}Then wrap the body of app/layout.tsx with <Providers>.
Integration checklist
- [ ] Installed all packages.
- [ ] Added wallet adapters to the
walletsarray. - [ ] Imported
@rakurai/rakurai-stake-sdk/react/styles.cssonce. - [ ]
RakuraiStakeProvidersits insideConnectionProvider,WalletProvider, andWalletModalProvider. - [ ]
useWalletAdapterBridge()is called inside the wallet providers. - [ ]
useRakuraiStakeModal()is called insideRakuraiStakeProvider. - [ ] Your button opens the modal, and Connect Wallet opens the wallet picker.
- [ ] After connecting, balances and APY appear, and stake and unstake work end to end.
Auto-compound
Auto-compound restakes a user's staking rewards every epoch, so their stake grows on its own. This section is written to be followed step by step. You do not need to understand Solana internals to integrate it.
Auto-compound only works for stake delegated to a validator that runs the Rakurai client. Throughout this section, replace YOUR_VOTE_ACCOUNT with your validator's vote account. This is the address that uniquely identifies your validator; you can find it in your validator dashboard or by running solana validators.
Before you start
You will need:
- Your validator's vote account address (
YOUR_VOTE_ACCOUNT). - A connected wallet in your app, through
@solana/wallet-adapter-react. - The way your app already sends Solana transactions. Signing and broadcasting stay on your side. Any RPC endpoint your app already uses works; a dedicated provider gives a smoother experience than the shared public endpoint.
Choose an integration path
| Your goal | Use | |---|---| | A ready-made screen where users turn auto-compound on and off for stake they already have | Part 1: the modal | | A checkbox on your own staking form so users opt in while they stake | Part 2: the builder | | No Rakurai UI, just the logic (for example in a backend) | Part 3: headless |
You can combine more than one.
Part 1: The Manage Auto-Compound screen
A drop-in popup that lists the user's stake on your validator, lets them switch auto-compound on or off, and submits one transaction.
Step 1. Import the stylesheet once, in your app entry or root layout:
import '@rakurai/rakurai-stake-sdk/react/styles.css';Step 2. Wrap your app with the provider. It must sit inside your wallet-adapter providers (ConnectionProvider, then WalletProvider, then WalletModalProvider). In Next.js, add 'use client' at the top of the file.
import {
RakuraiStakeProvider,
useWalletAdapterBridge,
} from '@rakurai/rakurai-stake-sdk/react';
function AutoCompoundProvider({ children }) {
const wallet = useWalletAdapterBridge();
return (
<RakuraiStakeProvider wallet={wallet} validatorVoteAccount="YOUR_VOTE_ACCOUNT">
{children}
</RakuraiStakeProvider>
);
}Step 3. Add a button that opens the screen, anywhere inside the provider:
import { useRakuraiStakeModal } from '@rakurai/rakurai-stake-sdk/react';
function ManageButton() {
const { openAutoCompound } = useRakuraiStakeModal();
return <button onClick={() => openAutoCompound()}>Manage Auto-Compound</button>;
}The popup lists the user's active stake accounts on your validator only, each with a checkbox and an Update button. The user selects the accounts they want and clicks Update, which triggers one wallet approval. The Powered by Rakurai mark is always shown.
To manage a different validator without changing the provider, pass a vote account to the call: openAutoCompound('SOME_OTHER_VOTE_ACCOUNT'). This value overrides the provider default for that open.
Part 2: An auto-compound checkbox on your own staking form
If your app already lets users stake natively, you enable auto-compound by adding one instruction to your stake transaction, right after the instruction that delegates to your validator.
Step 1 (optional). Show the branded checkbox:
import { AutoCompoundToggle } from '@rakurai/rakurai-stake-sdk/react';
<AutoCompoundToggle checked={autoCompoundOn} onChange={setAutoCompoundOn} />Step 2. When the checkbox is on, add the instruction to your stake transaction:
import { buildEnableAutoCompoundInstruction } from '@rakurai/rakurai-stake-sdk';
// Your transaction already has: createAccount, then delegate(YOUR_VOTE_ACCOUNT).
if (autoCompoundOn) {
tx.add(
buildEnableAutoCompoundInstruction({
stakeAccount, // the stake account you just created
stakeAuthority: userWallet, // the user's wallet, which must sign
})
);
}
// The user then signs the transaction and your app sends it, the same way it sends any stake transaction.The following must all be true, or the transaction will fail:
- The stake account is delegated to your validator's vote account.
- The auto-compound instruction comes after the delegate instruction in the same transaction.
- The user's wallet is the stake authority and signs the transaction. The user keeps withdraw authority.
- Signing and sending stay on your side.
Part 3: Headless (no Rakurai UI)
Import only from @rakurai/rakurai-stake-sdk (never /react). No React, no wallet adapter, and no CSS are involved; the only dependency is @solana/web3.js.
import {
buildEnableAutoCompoundInstruction, // enable while staking (see Part 2)
getNativeStakeAccounts, // list a wallet's stake and status on a validator
updateAutoCompound, // turn on or off for existing accounts
} from '@rakurai/rakurai-stake-sdk';
// List the wallet's stake accounts on your validator.
const accounts = await getNativeStakeAccounts({ pubkey, voteAccount: 'YOUR_VOTE_ACCOUNT' });
// Build an unsigned transaction to turn auto-compound on or off. You sign and broadcast it.
const tx = await updateAutoCompound({
pubkey,
accounts: [{ stakeAccountAddress, enable: true }],
});getNativeStakeAccounts and updateAutoCompound call Rakurai's API. buildEnableAutoCompoundInstruction does not touch the network. In all cases, you own signing and sending.
Customize colors, titles, and logos
You can rebrand the colors, titles, and logos of the auto-compound UI. Everything else is fixed, including the Powered by Rakurai mark, which is always shown. Pass a theme object to RakuraiStakeProvider:
<RakuraiStakeProvider
wallet={wallet}
validatorVoteAccount="YOUR_VOTE_ACCOUNT"
theme={{
// Titles
autoCompoundTitle: 'Auto-Compound',
autoCompoundDescription: 'Grow your stake automatically every epoch.',
// Logo (the modal's header icon): any URL or imported image
logoUrl: '/my-logo.png',
// Colors
colors: {
bg: '#0b1120', // modal background
text: '#ffffff', // main text
textMuted: '#94a3b8', // secondary text
ctaFrom: '#6d28d9', // gradient start (buttons and checkboxes)
ctaTo: '#22c55e', // gradient end
ctaText: '#ffffff', // button text
inputBg: '#111827', // row and input background
borderColor: '#1f2937',
overlay: 'rgba(0,0,0,0.7)', // dimmed backdrop
},
}}
>
<App />
</RakuraiStakeProvider>| What you can change | How |
|---|---|
| Colors | theme.colors: background, text, the gradient (buttons and checkboxes), borders, and overlay |
| Titles | theme.autoCompoundTitle and theme.autoCompoundDescription |
| Logo | theme.logoUrl (the modal header icon) |
| Powered by Rakurai | Fixed. It cannot be changed or hidden. |
For the standalone checkbox (AutoCompoundToggle), set its label with the label prop, and its colors with className (target .rakurai-ac-toggle in your CSS) or the style prop:
<AutoCompoundToggle checked={on} onChange={setOn} label="Auto-compound rewards" className="my-toggle" />Requirements and limits
- Stake must be delegated to a validator's vote account that runs the Rakurai client.
- The user's wallet is the stake authority and signs the transaction. It keeps withdraw authority.
- In Part 2, the enable instruction must come after the delegate instruction.
- The native staking minimum is 1 SOL.
- Mainnet only. The program ID is fixed inside the SDK (
SmArtSKbXQjHpXHDfGpzTayVtvwKnVWLSu13ha2R5uF); integrators never pass it.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| 403 Access forbidden when sending | Your app is sending through Solana's shared public endpoint, which throttles browser traffic. Point your ConnectionProvider at a dedicated RPC endpoint. This is on the app side, not the SDK. |
| Next.js error about a Client Component | Add 'use client' at the top of the file where you use the Rakurai React components. |
| Logos or icons not showing | They are inlined into the bundle, so no setup is needed. Confirm you imported @rakurai/rakurai-stake-sdk/react/styles.css. |
| Modal shows "No validator selected" | Pass validatorVoteAccount on the provider, or pass a vote account to openAutoCompound('YOUR_VOTE_ACCOUNT'). |
| Modal list is empty | That wallet has no active stake on that validator yet. The user needs to stake first. |
API reference
Import functions and types from the package entry:
import { liquidStake, RaiSOLStakeError } from '@rakurai/rakurai-stake-sdk';
import type { StakeParams } from '@rakurai/rakurai-stake-sdk';All functions throw RaiSOLStakeError on failure. Handle it with instanceof RaiSOLStakeError, and read error.message and the optional error.code (an HTTP-style status when available). Amounts passed to staking functions are integers in lamports (1 SOL is 1000000000).
Liquid staking and data
liquidStake(params: StakeParams): Promise<VersionedTransaction>
Requests a liquid-stake transaction from the API and returns it as a versioned transaction.
- Parameters:
{ pubkey: string; amount: number }.pubkeyis the wallet address (base58);amountis in lamports. - After: Sign with the user's wallet and send.
const tx = await liquidStake({ pubkey, amount: 1_000_000_000 });liquidUnstake(params: UnstakeParams): Promise<UnstakeResult>
Requests a native unstake transaction and the stake-account keypair that must co-sign it.
- Parameters:
{ pubkey: string; amount: number }. - Returns:
{ transaction, stakeAccountKeypair }. - After: Co-sign with
stakeAccountKeypairfirst (transaction.sign([stakeAccountKeypair]), or pass it asextraSignersto your wallet bridge), then sign with the user's wallet and send.
liquidUnstakeJupiter(params: UnstakeJupiterParams): Promise<VersionedTransaction>
Unstakes instantly through a Jupiter swap route.
- Parameters:
{ pubkey: string; amount: number }. - After: Sign with the user's wallet and send.
updateCampaign(params: CampaignUpdateParams): Promise<CampaignUpdateResponse>
Reports a confirmed stake or unstake to the campaign and referral backend.
- Parameters:
{ address, amount, txn_sig, is_stake, referral_code }. Setis_staketotruefor a stake andfalsefor an unstake. - Returns:
{ message?, error? }.
getReferral(referral_code: string): Promise<ReferralDetailsResponse>
Fetches referral stats for a code.
- Returns:
{ referral_code?, no_of_referrals?, referral_staked_amount?, error? }.
getRaiSOLApy(): Promise<number>
Returns the APY for Rakurai's liquid staking pool (for example 7.11).
getRaiSOLPrice(): Promise<number>
Returns the price of 1 raiSOL in SOL (for example 1.263).
getWalletSolBalance(params: WalletBalanceParams): Promise<number>
Returns a wallet's SOL balance in SOL units. The API returns lamports; the SDK divides by 1e9 before returning.
- Parameters:
{ pubkey: string }.
getWalletRaiSOLBalance(params: WalletBalanceParams): Promise<number>
Returns a wallet's raiSOL balance in raiSOL units. The API returns 9-decimal units; the SDK divides by 1e9 before returning.
- Parameters:
{ pubkey: string }.
Auto-compound
For step-by-step usage, see Auto-compound above.
buildEnableAutoCompoundInstruction(params): TransactionInstruction
Builds the instruction that enables auto-compound on a native stake account. It is pure and offline: no network calls and no keypairs, so it is safe in a browser or a Node backend. The program ID is fixed inside the SDK.
- Parameters:
{ stakeAccount: PublicKey | string; stakeAuthority: PublicKey | string }. - Returns: a
TransactionInstruction(synchronous, not a promise). - After: Append it to your stake transaction after the delegate instruction. The user's wallet (
stakeAuthority) must sign.
tx.add(buildEnableAutoCompoundInstruction({ stakeAccount, stakeAuthority: userWallet }));getNativeStakeAccounts(params): Promise<NativeStakeAccount[]>
Lists a wallet's active native stake accounts delegated to Rakurai-powered validators, with their current auto-compound status. Pass voteAccount to restrict the result to one validator.
- Parameters:
{ pubkey: string; voteAccount?: string }. - Returns: an array of
{ stakeAccountAddress, activeStake, autoCompoundEnabled, voteAccount, name, image }.activeStakeis in lamports.
updateAutoCompound(params): Promise<VersionedTransaction>
Builds a transaction that enables or disables auto-compound on existing stake accounts. The enable-or-disable decision is computed server-side, because disabling depends on on-chain transient-stake state.
- Parameters:
{ pubkey: string; accounts: AutoCompoundAccountUpdate[] }, where each account is{ stakeAccountAddress: string; enable: boolean }. - Returns: an unsigned
VersionedTransaction. - After: Sign with the owner wallet (which is the staker and withdraw authority) and broadcast.
The core entry also exports the PDA helpers deriveTurboConfigPda, deriveTurboTransientPda, and deriveTurboPositionPda, plus the constants TURBO_STAKE_PROGRAM_ID and ENABLE_AUTOCOMPOUND_DISCRIMINATOR, for advanced use.
React exports
Import from @rakurai/rakurai-stake-sdk/react.
RakuraiStakeProvider
interface RakuraiStakeProviderProps {
children: ReactNode;
wallet: WalletBridge;
unstakeMode?: 'native' | 'jupiter'; // default: 'native'
referralCode?: string;
theme?: ThemeConfig;
validatorVoteAccount?: string; // default validator for the auto-compound modal
}useRakuraiStakeModal()
const { open, close, openAutoCompound, closeAutoCompound } = useRakuraiStakeModal();
open(); // open the stake tab
open('unstake'); // open the unstake tab
openAutoCompound(); // open the auto-compound modal (uses validatorVoteAccount)
openAutoCompound('VOTE_ACCOUNT'); // open it for a specific validator
close();
closeAutoCompound();useWalletAdapterBridge(options?)
Returns a ready-made WalletBridge for @solana/wallet-adapter-react apps.
const walletBridge = useWalletAdapterBridge({
solBalance: 2.5, // optional: overrides the API fetch
raisolBalance: 1.3, // optional: overrides the API fetch
});AutoCompoundToggle
A standalone branded on/off control for your own staking form. It is presentational only; pair it with buildEnableAutoCompoundInstruction.
interface AutoCompoundToggleProps {
checked: boolean;
onChange: (next: boolean) => void;
disabled?: boolean;
label?: string; // default: 'Auto-compound rewards'
className?: string;
style?: CSSProperties;
}Endpoints called by the modal
The liquid staking modal calls these endpoints automatically.
| Trigger | Method | Endpoint |
|---|---|---|
| Modal opens | GET | /api/v1/price/raisol |
| Modal opens | GET | /api/v1/validator/apy-liquid |
| Wallet connected | POST | /api/v1/wallet/balance/sol |
| Wallet connected | POST | /api/v1/wallet/balance/raisol |
| User stakes | POST | /api/v1/staking/liquid/stake |
| User unstakes (native) | POST | /api/v1/staking/liquid/unstake |
| User unstakes (Jupiter) | POST | /api/v1/staking/liquid/jupiter/unstake |
| Transaction confirmed with referral | POST | /api/v1/wallet/campaign/update |
The base URL and endpoint paths are defined as constants at the top of src/rakurai-stake-sdk.ts. Change them if you need a different environment.
Wallet integration
The React modal talks to the host app's wallet through a single interface, WalletBridge. For @solana/wallet-adapter-react apps, useWalletAdapterBridge() implements it for you. For any other setup, implement the interface yourself:
interface WalletBridge {
readonly publicKey: string | null;
connect(): Promise<void>;
disconnect(): Promise<void>;
signAndSendTransaction(
tx: Transaction | VersionedTransaction,
extraSigners?: Keypair[]
): Promise<string>;
solBalance?: number;
raisolBalance?: number;
}For the native unstake flow, sign with the extra signers before the user's wallet signs (for a legacy transaction, tx.partialSign(...extraSigners)). The SDK's core functions accept only a wallet public key string and return standard @solana/web3.js transactions, so any wallet that can sign Solana transactions works.
