@sweefi/solana
v0.1.0
Published
Solana-native s402 protocol implementation for SweeFi
Maintainers
Readme
@sweefi/solana
Solana-native s402 payment adapter — exact SPL token and native SOL transfers with browser-safe signing.
@sweefi/solana is the Solana-specific implementation of the SweeFi payment protocol. It implements the PaymentAdapter interface from @sweefi/ui-core, exposes two-sided signer abstractions (ClientSolanaSigner for payers, FacilitatorSolanaSigner for verifiers), and provides the full s402 scheme trio (client, server, facilitator) for exact payments.
Apache 2.0 open source. Part of the SweeFi ecosystem.
Installation
npm install @sweefi/solana@solana/web3.js and @solana/spl-token are peer dependencies and must be installed alongside this package:
npm install @sweefi/solana @solana/web3.js @solana/spl-tokenPeer Dependencies
| Package | Version |
|---------|---------|
| @solana/web3.js | ^1.95.0 |
| @solana/spl-token | ^0.4.0 |
Quick Start
Node.js agent (keypair)
import { Connection, Keypair, clusterApiUrl } from '@solana/web3.js';
import {
SolanaPaymentAdapter,
SolanaKeypairSigner,
SOLANA_DEVNET_CAIP2,
USDC_DEVNET_MINT,
} from '@sweefi/solana';
// 1. Set up the connection and signer
const connection = new Connection(clusterApiUrl('devnet'), 'confirmed');
const keypair = Keypair.fromSecretKey(secretKeyBytes);
const wallet = new SolanaKeypairSigner(keypair);
// 2. Create the adapter
const adapter = new SolanaPaymentAdapter({
wallet,
connection,
network: SOLANA_DEVNET_CAIP2,
});
// 3. Simulate before committing
const sim = await adapter.simulate({
s402Version: '1',
accepts: ['exact'],
network: SOLANA_DEVNET_CAIP2,
asset: USDC_DEVNET_MINT,
amount: '1000000', // 1 USDC (6 decimals)
payTo: '0xRecipient...',
});
if (sim.success) {
console.log('Estimated fee:', sim.estimatedFee); // { amount: 5000n, currency: 'SOL' }
}
// 4. Sign and broadcast
const { txId } = await adapter.signAndBroadcast({
s402Version: '1',
accepts: ['exact'],
network: SOLANA_DEVNET_CAIP2,
asset: USDC_DEVNET_MINT,
amount: '1000000',
payTo: '0xRecipient...',
});
console.log('Confirmed:', txId); // base58 transaction signatureBrowser wallet (Phantom, Backpack, etc.)
import { useConnection, useWallet } from '@solana/wallet-adapter-react';
import { SolanaPaymentAdapter, SolanaWalletSigner } from '@sweefi/solana';
function usePaymentAdapter() {
const { connection } = useConnection();
const wallet = useWallet();
// SolanaWalletSigner wraps any @solana/wallet-adapter-base compatible wallet
const signer = new SolanaWalletSigner(wallet);
return new SolanaPaymentAdapter({
wallet: signer,
connection,
network: 'solana:mainnet-beta',
});
}Native SOL transfer
Pass NATIVE_SOL_MINT (or the string 'native') as the asset field:
import { NATIVE_SOL_MINT, SOLANA_DEVNET_CAIP2 } from '@sweefi/solana';
const { txId } = await adapter.signAndBroadcast({
s402Version: '1',
accepts: ['exact'],
network: SOLANA_DEVNET_CAIP2,
asset: NATIVE_SOL_MINT,
amount: '1000000000', // 1 SOL in lamports
payTo: 'Recipient...',
});PaymentAdapter Interface
SolanaPaymentAdapter implements the PaymentAdapter interface from @sweefi/ui-core:
| Method | Description |
|--------|-------------|
| network | CAIP-2 identifier ('solana:mainnet-beta', 'solana:devnet', or 'solana:testnet') |
| getAddress() | Returns the payer's base58 public key, or null if the wallet is disconnected |
| simulate(reqs) | Dry-run the transaction. Returns { success, estimatedFee? } or { success: false, error } |
| signAndBroadcast(reqs) | Sign, broadcast, and confirm the transaction. Returns { txId } |
simulate() builds the exact same instruction set as signAndBroadcast() — including protocol fee splits — using a legacy Transaction so simulation results match on-chain behavior.
Signer Utilities
SolanaKeypairSigner — Node.js agents and CLI tools
import { Keypair } from '@solana/web3.js';
import { SolanaKeypairSigner } from '@sweefi/solana';
const keypair = Keypair.fromSecretKey(secretKeyBytes);
const signer = new SolanaKeypairSigner(keypair);
// signer.address — base58 public key
// signer.signTransaction(tx, connection) — signs, returns { serialized, signature, blockhash, lastValidBlockHeight }Use this when: running an autonomous agent, a CLI tool, or any server-side process with direct access to a keypair.
SolanaWalletSigner — Browser wallets
import { SolanaWalletSigner } from '@sweefi/solana';
import type { SolanaWalletAdapter } from '@sweefi/solana';
// Any wallet implementing { publicKey, signTransaction } works — no hard dep on wallet-adapter-base
const signer = new SolanaWalletSigner(wallet);Use this when: building a frontend where the private key lives in a browser extension (Phantom, Backpack, Solflare, etc.). SolanaWalletSigner delegates signing to the wallet's signTransaction method and never touches the private key.
Blockhash threading — why signTransaction returns blockhash
Every Solana transaction embeds a recent blockhash at sign time. That same blockhash's lastValidBlockHeight is the deadline for confirmation.
A naive implementation fetches a fresh blockhash twice: once when building the transaction and again when calling confirmTransaction. If the second fetch returns a different lastValidBlockHeight (which happens whenever a new blockhash is produced between the two calls), confirmTransaction may report expiry even though the transaction succeeds on-chain.
@sweefi/solana avoids this by threading the blockhash through the signing stack:
// ClientSolanaSigner.signTransaction returns all four values:
const { serialized, signature, blockhash, lastValidBlockHeight } =
await signer.signTransaction(tx, connection);
// ExactSolanaClientScheme.createPaymentWithMeta surfaces them:
const { s402Payload, blockhash, lastValidBlockHeight } =
await scheme.createPaymentWithMeta(requirements);
// SolanaPaymentAdapter.signAndBroadcast uses the sign-time blockhash — no second fetch:
await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, 'confirmed');This guarantees that the confirmation window matches the transaction exactly.
toFacilitatorSolanaSigner(config?) — server-side verification
import { toFacilitatorSolanaSigner } from '@sweefi/solana';
// Uses public cluster endpoints by default
const facilitator = toFacilitatorSolanaSigner();
// With a custom RPC URL
const facilitator = toFacilitatorSolanaSigner({
rpcUrl: 'https://my-rpc.example.com',
});
// With per-network overrides
const facilitator = toFacilitatorSolanaSigner({
rpcUrls: {
'solana:mainnet-beta': 'https://mainnet-rpc.example.com',
'solana:devnet': 'https://devnet-rpc.example.com',
},
});FacilitatorSolanaSigner methods:
| Method | Description |
|--------|-------------|
| verifyAndGetPayer(serializedTx, network) | Ed25519-verify the payer's signature via Web Crypto API. Returns the payer's base58 address or throws |
| simulateTransaction(serializedTx, network) | Dry-run; returns pre/post SOL and SPL token balances keyed by account index |
| executeTransaction(serializedTx, network) | Broadcast raw signed bytes; returns the transaction signature (txid) |
| confirmTransaction(signature, network) | Wait for 'confirmed' finality; throws on failure |
verifyAndGetPayer uses crypto.subtle.verify('Ed25519', ...) — zero extra dependencies, works in Node.js 18+ and all modern browsers.
s402 Scheme Implementations
| Scheme | Client | Facilitator | Server |
|--------|--------|-------------|--------|
| Exact | ExactSolanaClientScheme | ExactSolanaFacilitatorScheme | ExactSolanaServerScheme |
| Prepaid | — | — | — |
| Stream | — | — | — |
| Escrow | — | — | — |
Only the Exact scheme is implemented. Prepaid, Stream, and Escrow require Anchor programs that have not yet been deployed on Solana. Those will be added once the on-chain programs are ready.
- Exact — One-shot transfer. For SPL tokens, the client signs a
createTransferInstructionfrom their ATA to the recipient's ATA. For native SOL, it usesSystemProgram.transfer. Protocol fee splits are handled atomically by adding a second instruction to the same transaction.
Server scheme — building payment requirements
import { ExactSolanaServerScheme, SOLANA_DEVNET_CAIP2, USDC_DEVNET_MINT } from '@sweefi/solana';
const server = new ExactSolanaServerScheme();
const requirements = server.buildRequirements({
network: SOLANA_DEVNET_CAIP2,
payTo: 'YourWallet...',
amount: '1000000', // 1 USDC
asset: USDC_DEVNET_MINT, // optional — defaults to USDC for the network
protocolFeeBps: 50, // optional — 0.5%
protocolFeeAddress: 'FeeWallet...',
});
// Returns a fully-formed s402PaymentRequirements objectClient scheme — building a payment payload
import { ExactSolanaClientScheme, SolanaKeypairSigner } from '@sweefi/solana';
import { Connection } from '@solana/web3.js';
const scheme = new ExactSolanaClientScheme(
new SolanaKeypairSigner(keypair),
new Connection(clusterApiUrl('devnet'), 'confirmed'),
);
// Standard interface method (returns payload only)
const payload = await scheme.createPayment(requirements);
// Extended method (also returns blockhash metadata for confirmTransaction)
const { s402Payload, blockhash, lastValidBlockHeight } =
await scheme.createPaymentWithMeta(requirements);Facilitator scheme — verifying and settling
import { ExactSolanaFacilitatorScheme, toFacilitatorSolanaSigner } from '@sweefi/solana';
const scheme = new ExactSolanaFacilitatorScheme(toFacilitatorSolanaSigner());
// Cryptographically verify + simulate (does not submit on-chain)
const verification = await scheme.verify(payload, requirements);
// { valid: true, payerAddress: '...' } or { valid: false, invalidReason: '...' }
// Verify + submit on-chain
const settlement = await scheme.settle(payload, requirements);
// { success: true, txDigest: '...', finalityMs: 450 }verify() runs a 4-step check:
- Scheme validation (is the payload type
'exact'?) - Network validation (is this actually a Solana network?)
- Signature recovery — cryptographic proof the payer signed the transaction (Web Crypto Ed25519)
- Simulation — would this transaction succeed if submitted?
- Balance verification — did the recipient's account increase by the required amount?
settle() re-runs verification before submitting, mirroring the defense-in-depth pattern from @sweefi/sui.
Constants
import {
SOLANA_MAINNET_CAIP2, // 'solana:mainnet-beta'
SOLANA_DEVNET_CAIP2, // 'solana:devnet'
SOLANA_TESTNET_CAIP2, // 'solana:testnet'
NATIVE_SOL_MINT, // 'So11111111111111111111111111111111111111112'
USDC_MAINNET_MINT, // 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
USDC_DEVNET_MINT, // '4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU'
USDC_DECIMALS, // 6
SOL_DECIMALS, // 9
LAMPORTS_PER_SOL, // 1_000_000_000
BASE_FEE_LAMPORTS, // 5_000n
} from '@sweefi/solana';Utilities
import {
createSolanaConnection, // Create a Connection from a CAIP-2 network identifier
networkToCluster, // 'solana:devnet' → 'devnet'
getDefaultUsdcMint, // Returns the correct USDC mint for mainnet-beta or devnet
uint8ArrayToBase64, // Browser-safe Uint8Array → base64 (btoa, not Buffer)
base64ToUint8Array, // Browser-safe base64 → Uint8Array (atob, not Buffer)
} from '@sweefi/solana';
// createSolanaConnection accepts an optional RPC URL override
const connection = createSolanaConnection('solana:devnet');
const connection = createSolanaConnection('solana:mainnet-beta', 'https://my-rpc.example.com');Browser note: All binary/base64 conversions in @sweefi/solana use btoa/atob (Web Platform globals). Buffer.from() is intentionally avoided — it is a Node.js global absent in browsers without a polyfill (e.g., Vite does not include one by default).
License
Apache 2.0 — https://github.com/sweeinc/sweefi
