@spree-finance/spree-evm-sdk
v0.2.0
Published
TypeScript SDK for building unsigned transactions for Spree Finance contracts
Readme
SPREE FINANCE EVM SDK
TypeScript SDK for building unsigned transactions for Spree Protocol smart contracts on Base and MOCA Devnet. Supports brand hierarchies (Spree parent, Airsp branded child) and multi-network deployments.
Features
- Transaction Building: Build unsigned legacy transactions for offline/hardware wallet signing
- Read Operations: Execute
eth_callfor view functions without building transactions - Type Safety: Full TypeScript support with branded
Hextypes - Gas Overrides: Customize gas parameters per transaction
- Nonce Management: Automatic nonce handling with optional caching for batch operations
- Error Handling: Typed errors for RPC and network failures
What's Included
The npm package includes:
- Built Code: ESM (
dist/index.js) and CommonJS (dist/index.cjs) builds - Type Definitions: Full TypeScript declarations (
dist/index.d.ts) - Contract ABIs: JSON ABI files for all supported contracts (
contracts/)
Package size: ~89 KB compressed, ~821 KB unpacked
Installation
npm install @spree-finance/spree-evm-sdkOr with Yarn:
yarn add @spree-finance/spree-evm-sdkOr with pnpm:
pnpm add @spree-finance/spree-evm-sdkPeer Dependencies
This SDK requires ethers v6 as a peer dependency:
npm install ethers@^6.0.0Requirements
- Node.js >= 18.0.0
- TypeScript >= 5.3 (for TypeScript projects)
- ESM support (NodeNext module resolution)
Integrations
For end-to-end examples (backend-built unsigned tx → wallet signing → broadcasting), see:
- Examples - Runnable examples with code snippets below
examples/- Full examples directory with test suiteINTEGRATION.md- Integration patterns
Documentation
AGENTS.md- Agent/AI development guideCLAUDE.md- Claude Code development guideexamples/README.md- Examples documentationMIGRATION.md- Migration guide for breaking changes
Breaking Changes (v0.2.0)
⚠️ Important: Version 0.2.0 includes breaking changes to PendingSPVault:
createCampaign()signature changed - Now requires 3 additional parameters for supply caps:// New signature (v0.2.0) await vault.createCampaign( campaignId, spBudget, destinations, whitelist, metadata, maxTotalSupply, // NEW: 0 = unlimited maxMintPerUser, // NEW: 0 = unlimited dailyMintLimit // NEW: 0 = unlimited );Campaigninterface expanded - 10 new fields added (supply caps, fee tracking, metrics)Backward compatibility - Use
createCampaignLegacy()during migration (deprecated, removed in v1.0.0)
See MIGRATION.md for detailed migration instructions.
Quick Start
Base Sepolia (Primary Testnet)
import { SpreeVault4626 } from '@spree-finance/spree-evm-sdk';
// Initialize the vault wrapper
const vault = new SpreeVault4626({
rpcUrl: 'https://sepolia.base.org',
chainId: 84532n,
contractAddress: '0x...',
from: '0x...', // Your wallet address
defaultGasPriceWei: 1_000_000_000n, // 1 gwei
defaultGasLimit: 500_000n,
});
// Read contract state (uses eth_call)
const balance = await vault.readBalanceOf('0x...');
const totalAssets = await vault.readTotalAssets();
const vaultHealth = await vault.readVaultHealth();
// Build unsigned transaction for signing
const unsignedTx = await vault.deposit(1_000_000n, '0x...');
// Sign with your wallet/hardware device and broadcast
const signedTx = await wallet.signTransaction(unsignedTx);
const txHash = await provider.sendTransaction(signedTx);MOCA Devnet (Brand Deployments)
import {
PendingSPVault,
getContractsForBrand,
MOCA_DEVNET_CONFIG,
} from '@spree-finance/spree-evm-sdk';
// Get brand-specific contract addresses
const spreeContracts = getContractsForBrand('spree', 5151n);
// Returns: { FACTORY, POINTS, COLLATERAL_VAULT, PENDING_SP_VAULT }
const airspContracts = getContractsForBrand('airsp', 5151n);
// Returns: { FACTORY, POINTS, COLLATERAL_VAULT, PENDING_SP_VAULT }
// Initialize with brand-specific address
const pendingSP = new PendingSPVault({
rpcUrl: MOCA_DEVNET_CONFIG.rpcUrl,
chainId: MOCA_DEVNET_CONFIG.chainId,
contractAddress: spreeContracts!.PENDING_SP_VAULT,
from: userAddress,
defaultGasPriceWei: 1_000_000_000n,
defaultGasLimit: 500_000n,
});
// Create campaign on MOCA Devnet
const campaignTx = await pendingSP.createCampaign(
campaignId,
spBudget,
[settlementDestination],
[transferWhitelist],
metadata,
0n, // maxTotalSupply (0 = unlimited)
0n, // maxMintPerUser (0 = unlimited)
0n // dailyMintLimit (0 = unlimited)
);MOCA Testnet (Spree Deployments)
import {
PendingSPVault,
getContractsForBrand,
MOCA_TESTNET_CONFIG,
} from '@spree-finance/spree-evm-sdk';
// Get Spree contract addresses on MOCA Testnet
const spreeContracts = getContractsForBrand('spree', 222888n);
// Returns: { FACTORY, POINTS, COLLATERAL_VAULT, PENDING_SP_VAULT }
// Initialize with testnet configuration
const pendingSP = new PendingSPVault({
rpcUrl: MOCA_TESTNET_CONFIG.rpcUrl,
chainId: MOCA_TESTNET_CONFIG.chainId,
contractAddress: spreeContracts!.PENDING_SP_VAULT,
from: userAddress,
defaultGasPriceWei: 1_000_000_000n,
defaultGasLimit: 500_000n,
});
// Create campaign on MOCA Testnet
const campaignTx = await pendingSP.createCampaign(
campaignId,
spBudget,
[settlementDestination],
[transferWhitelist],
metadata,
0n, // maxTotalSupply (0 = unlimited)
0n, // maxMintPerUser (0 = unlimited)
0n // dailyMintLimit (0 = unlimited)
);Examples
The examples/ directory contains 20+ runnable examples demonstrating best practices for using the SDK. These examples show how to build unsigned transactions and perform read-only operations.
Setup
cd examples
npm install
# Configure environment
cp .env.example .env
# Edit .env with your values (RPC URL, private key, contract addresses)Getting Started
1. Read Contract State (Query without transactions)
Query vault metrics using eth_call - no transaction needed:
import { SpreeVault4626 } from '@spree-finance/spree-evm-sdk';
const vault = new SpreeVault4626({
rpcUrl: 'https://sepolia.base.org',
chainId: 84532n,
contractAddress: '0x...',
from: '0x...',
defaultGasPriceWei: 1_000_000_000n,
defaultGasLimit: 500_000n,
});
// Query vault health metrics
const health = await vault.readVaultHealth();
console.log(`Total Assets: ${health.totalAssets}`);
console.log(`Utilization: ${health.utilization}%`);
console.log(`Is Balanced: ${health.isBalanced}`);
// Run: npm run read:vault-health2. Build Unsigned Transaction (SDK's core purpose)
Build an unsigned deposit transaction for later signing:
import { SpreeVault4626 } from '@spree-finance/spree-evm-sdk';
const vault = new SpreeVault4626(config);
// Build unsigned transaction (no signing, no broadcasting)
const unsignedTx = await vault.deposit(1_000_000n, receiverAddress);
// unsignedTx is a hex string ready for:
// - Hardware wallet signing
// - Software wallet signing
// - Storage for later signing
// - Audit before execution
// Run: npm run tx:build-deposit3. Complete Transaction Flow (Build → Sign → Broadcast)
Using ethers v6 to sign and broadcast SDK-built transactions:
import { SpreeVault4626 } from '@spree-finance/spree-evm-sdk';
import { Wallet, JsonRpcProvider } from 'ethers';
const vault = new SpreeVault4626(config);
const provider = new JsonRpcProvider(process.env.RPC_URL);
const wallet = new Wallet(process.env.PRIVATE_KEY, provider);
// Step 1: SDK builds unsigned transaction
const unsignedTx = await vault.deposit(1_000_000n, receiverAddress);
// Step 2: Sign with ethers
const signedTx = await wallet.signTransaction(unsignedTx);
// Step 3: Broadcast to network
const txResponse = await provider.broadcastTransaction(signedTx);
console.log(`Transaction hash: ${txResponse.hash}`);
// Run: npm run tx:sign-broadcast4. Advanced Multi-Step Workflow (Campaign creation)
Create a Pending SP campaign with token approvals and boost configuration:
import { PendingSPVault, BoostEngine, ERC20 } from '@spree-finance/spree-evm-sdk';
const spToken = new ERC20(config);
const pendingSP = new PendingSPVault(config);
const boostEngine = new BoostEngine(config);
// Step 1: Approve SP tokens for campaign
const approveTx = await spToken.approve(vaultAddress, budget);
const signedApprove = await signTransaction(approveTx);
await broadcastTransaction(signedApprove);
// Step 2: Create campaign
const createTx = await pendingSP.createCampaign(
campaignId,
spBudget,
[settlementDestination],
[transferWhitelist],
metadata
);
// Step 3: Configure boost multipliers
const boostTx = await boostEngine.setNamespaceMultipliers(
namespace,
[100n, 125n, 150n, 200n, 250n] // Tier 0-4 multipliers
);
// Step 4: Activate campaign
const activateTx = await pendingSP.setCampaignActive(campaignId, true);
// Run: npm run pending-sp:create-campaignAll Examples
| Category | Command | Description |
|----------|---------|-------------|
| Read Operations | | |
| | npm run read:vault-health | Query vault metrics (total assets, utilization, health) |
| | npm run read:balances | Check USDC, SP token, and vault share balances |
| | npm run read:campaign-info | View Pending SP campaign details and budget |
| | npm run read:user-status | Query user tier, status, and boost multiplier |
| | npm run read:boost-multiplier | Check boost multipliers for reward calculation |
| Transactions | | |
| | npm run tx:build-deposit | Build unsigned deposit transaction |
| | npm run tx:sign-broadcast | Complete flow: build, sign, and broadcast |
| | npm run tx:batch-nonce | Batch multiple transactions with nonce caching |
| Pending SP Vault | | |
| | npm run pending-sp:create-campaign | Create campaign with boost integration |
| | npm run pending-sp:create-basic | Simple campaign creation |
| | npm run pending-sp:create-and-mint | Create campaign and mint tokens |
| | npm run pending-sp:mint | Mint pSP tokens to users |
| | npm run pending-sp:mint-boosted | Mint with automatic boost calculation |
| | npm run pending-sp:expire | Expire campaign tokens |
| | npm run pending-sp:fund-campaign | Fund campaign with SP tokens |
| | npm run pending-sp:user-story | Complete user workflow demonstration |
| Factory | | |
| | npm run factory:mint | Mint SP tokens through factory |
| Rewards | | |
| | npm run rewards:claim | Claim all available rewards in batch |
See examples/README.md for detailed documentation on each example.
Contract Wrappers
SpreeVault4626
ERC-4626 compliant yield vault with harvest/rebalance functionality.
import { SpreeVault4626 } from '@spree-finance/spree-evm-sdk';
const vault = new SpreeVault4626(config);
// ERC-4626 Core Operations
await vault.deposit(assets, receiver);
await vault.mint(shares, receiver);
await vault.withdraw(assets, receiver, owner);
await vault.redeem(shares, receiver, owner);
// Read Functions
const shares = await vault.readConvertToShares(assets);
const assets = await vault.readConvertToAssets(shares);
const health = await vault.readVaultHealth();
// Strategy Operations
await vault.harvest();
await vault.rebalance();
await vault.emergencyWithdrawAll();
// Admin Functions
await vault.setFees(mintFeeBps, redeemFeeBps, feeReceiver);
await vault.setAdapter(adapterAddress);
await vault.updateParameters(minIdleBufferBps, maxUtilizationBps);BonusRewardsVault
Epoch-based bonus reward distribution vault.
import { BonusRewardsVault } from '@spree-finance/spree-evm-sdk';
const rewards = new BonusRewardsVault(config);
// Claim Rewards
await rewards.claimBonus(epochId);
await rewards.claimBonusBatch([1n, 2n, 3n]);
// Read Functions
const currentEpoch = await rewards.readGetCurrentEpochId();
const claimable = await rewards.readGetClaimableEpochs(user, 10n);
const estimate = await rewards.readBonusEstimateForEpoch(user, epochId);
// TVL Snapshots
await rewards.takeTVLSnapshot();
const history = await rewards.readGetTVLHistory();
// Partner Management
await rewards.registerPartner(partnerAddress);
const revenue = await rewards.readGetPartnerRevenue(partner);Factory
Vault creation and mint/redeem operations.
import { Factory } from '@spree-finance/spree-evm-sdk';
const factory = new Factory(config);
// Vault Management
await factory.createVault(asset, assetToSharesRate);
await factory.pauseVault(asset);
await factory.unpauseVault(asset);
// Mint/Redeem
await factory.mint(asset, amount, receiver, expectBasketMode);
await factory.requestToRedeem(asset, pointsAmount, receiver, expectBasketMode);
await factory.finalizeRedeem(account);
// Configuration
await factory.setMintRate(asset, rate);
await factory.setRedeemRate(asset, rate);
await factory.setGlobalCap(limit);Points
ERC-20 points token with TWAB tracking.
import { Points } from '@spree-finance/spree-evm-sdk';
const points = new Points(config);
// ERC-20 Operations
await points.transfer(to, amount);
await points.approve(spender, amount);
const balance = await points.readBalanceOf(owner);
// Admin Functions
await points.mint(to, amount);
await points.burn(from, amount);
await points.addToTransferWhitelist(account);PythPriceOracle
Pyth Network price oracle integration.
import { PythPriceOracle } from '@spree-finance/spree-evm-sdk';
const oracle = new PythPriceOracle(config);
// Read Prices
const price = await oracle.readGetPrice(asset);
const priceWithAge = await oracle.readGetPriceNoOlderThan(asset, maxAge);
// Configuration
await oracle.setPriceFeed(asset, pythFeedId);
await oracle.setPythSource(pythOracleAddress);PendingSPVault
Conditional reward token system with campaign-scoped budgets and programmable settlement.
import { PendingSPVault } from '@spree-finance/spree-evm-sdk';
const pendingSP = new PendingSPVault(config);
// Campaign Management
await pendingSP.createCampaign(
campaignId,
spBudget,
settlementDestinations,
transferWhitelist,
metadata
);
await pendingSP.fundCampaign(campaignId, amount);
await pendingSP.setCampaignActive(campaignId, true);
// Token Operations
await pendingSP.mint(campaignId, to, amount, metadata);
await pendingSP.settle(from, to, amount, campaignId, metadata);
await pendingSP.expire(from, amount, campaignId, metadata);
await pendingSP.transferWithCampaign(to, amount, campaignId);
// Read Functions
const campaign = await pendingSP.readGetCampaign(campaignId);
const balance = await pendingSP.readBalanceOfCampaign(user, campaignId);
const availableBudget = await pendingSP.readGetCampaignAvailableBudget(campaignId);
const userCampaigns = await pendingSP.readGetUserCampaigns(user);Mint with Boost
Use mintBoosted() for automatic tier-based boost calculation and application:
import { PendingSPVault } from '@spree-finance/spree-evm-sdk';
const pendingSP = new PendingSPVault(config);
// Standard mint (no boost)
await pendingSP.mint(campaignId, to, amount, metadata);
// Mint with automatic boost calculation
// Contract calculates boost based on user's tier and mints base + boost amount
await pendingSP.mintBoosted(campaignId, to, amount, metadata);The mintBoosted() function:
- Calculates boost amount using the configured BoostEngine
- Mints
baseAmount + boostAmountin a single transaction - Emits both
PspMintedandBoostedMintevents - Respects campaign budget with the total (base + boost) amount
Complete Campaign Workflow
Full campaign lifecycle with nonce management for batch operations:
import { PendingSPVault, ERC20 } from '@spree-finance/spree-evm-sdk';
const spToken = new ERC20(erc20Config);
const pendingSP = new PendingSPVault(config);
// Enable nonce caching for sequential transactions
pendingSP.enableNonceCache();
spToken.enableNonceCache();
// Step 1: Approve SP tokens
const approveTx = await spToken.approve(vaultAddress, budget);
const signedApprove = await signTransaction(approveTx);
await broadcastTransaction(signedApprove);
// Step 2: Create campaign
const createTx = await pendingSP.createCampaign(
campaignId,
spBudget,
[settlementDestination],
[transferWhitelist],
metadata
);
// Step 3: Fund campaign
const fundTx = await pendingSP.fundCampaign(campaignId, spBudget);
// Step 4: Set campaign admin and minter
await pendingSP.setCampaignAdmin(campaignId, adminAddress, true);
await pendingSP.setMinter(campaignId, minterAddress, mintAllowance, true);
// Step 5: Activate campaign
const activateTx = await pendingSP.setCampaignActive(campaignId, true);
// Disable nonce caching when done
pendingSP.disableNonceCache();
spToken.disableNonceCache();See the examples/pending-sp/ directory for complete implementations including:
create-campaign.ts- Full campaign creation with boost integrationcreate-and-mint.ts- Create, fund, mint, and settle workflowmint-boosted.ts- Mint with automatic boost calculationuser-story.ts- Complete user journey demonstrationexpire.ts- Token expiration handling
Brand Configuration
The SDK supports multiple brands with parent-child relationships:
Available Brands
- spree: Root/parent brand
- airsp: Branded child of Spree
Supported Networks
- Base Sepolia (chainId: 84532) - Ethereum testnet
- MOCA Devnet (chainId: 5151) - Spree/Airsp deployments (active)
- MOCA Testnet (chainId: 222888) - Spree deployments (active)
Using Brand Configuration
import {
getBrandConfig,
getContractsForBrand,
getChildBrands,
ContractRegistry,
} from '@spree-finance/spree-evm-sdk';
// Get brand configuration
const spreeConfig = getBrandConfig('spree');
const airspConfig = getBrandConfig('airsp');
// Get contracts for brand on specific chain
const spreeContracts = getContractsForBrand('spree', 5151n);
// Returns: { FACTORY, POINTS, COLLATERAL_VAULT, PENDING_SP_VAULT }
// Get child brands
const spreeChildren = getChildBrands('spree'); // [airspConfig]
// Use ContractRegistry for brand hierarchy
const registry = new ContractRegistry({ apiBaseUrl: '...' });
const hierarchy = registry.getContractsForBrandHierarchy('spree', 5151);
// Returns: { parent: {...}, children: [{...}] }
// Get specific vault addresses
const pendingSPAddress = registry.getPendingSPVaultAddress('airsp', 5151);
const collateralVaultAddress = registry.getCollateralVaultAddress('spree', 5151);SpreeStatusRegistry
User tier and status management system with TWAB integration.
import { SpreeStatusRegistry } from '@spree-finance/spree-evm-sdk';
const statusRegistry = new SpreeStatusRegistry(config);
// Read User Status
const tier = await statusRegistry.readGetTier(user, namespace);
const status = await statusRegistry.readGetStatus(user, namespace);
const multiplier = await statusRegistry.readGetBoostMultiplier(user, namespace);
// Tier Configuration (admin)
await statusRegistry.createNamespace(namespace, admin);
await statusRegistry.setTierConfig(namespace, tierLevel, {
spThreshold: 1000_000_000_000n,
holdingPeriod: 2592000n,
boostMultiplier: 125n,
});
await statusRegistry.setMaxTier(namespace, 5);
// Tier Updates
await statusRegistry.updateTier(user, namespace);
await statusRegistry.batchUpdateTiers([user1, user2], namespace);
// Oracle/Operator Functions
await statusRegistry.setUserTier(user, namespace, 3, 100n);
await statusRegistry.setTierByOracle(user, namespace, 2, 50n);BoostEngine
Tier-based reward boosting system with campaign-specific configurations.
import { BoostEngine } from '@spree-finance/spree-evm-sdk';
const boostEngine = new BoostEngine(config);
// Read Boost Information
const multiplier = await boostEngine.readGetMultiplier(user, namespace);
const tierMultiplier = await boostEngine.readGetTierMultiplier(namespace, tier);
const boostedAmount = await boostEngine.readCalculateBoostedReward(
user,
namespace,
baseAmount
);
// Configure Namespace Multipliers (admin)
await boostEngine.setNamespaceMultipliers(namespace, [
100n, // Tier 0
125n, // Tier 1
150n, // Tier 2
200n, // Tier 3
250n, // Tier 4
]);
// Configure Campaign Boosts (configurator)
await boostEngine.setCampaignBoost(campaignId, {
baseMultiplier: 100n,
earlyBirdBonus: 10n,
earlyBirdCount: 100n,
loyaltyBonus: 5n,
referralBonus: 5n,
maxPerUser: 10_000_000_000n,
cooldownSeconds: 86400n,
});
// Record Boost Application (recorder)
await boostEngine.recordBoostApplication(user, namespace, campaignId);Advanced Usage
Gas Overrides
Customize gas parameters per transaction:
const tx = await vault.deposit(1_000_000n, receiver, {
gasLimit: 300_000n,
gasPriceWei: 2_000_000_000n,
valueWei: 0n, // For payable functions
});Nonce Caching
For batch transaction building:
// Enable caching to avoid RPC calls per transaction
vault.enableNonceCache();
const tx1 = await vault.deposit(1_000_000n, receiver);
const tx2 = await vault.withdraw(500_000n, receiver, owner);
const tx3 = await vault.harvest();
// All transactions have sequential nonces without extra RPC calls
vault.disableNonceCache();Manual Nonce Control
vault.setNonce(42n); // Start from specific nonce
const tx = await vault.deposit(1_000_000n, receiver);
// Transaction will have nonce 42
vault.resetNonceCache(); // Clear and fetch fresh on next callCalldata Encoding
Encode function calls without building full transactions:
const calldata = vault.encodeCall('deposit', [1_000_000n, receiver]);
// Use for multicall or direct contract interactionError Handling
import { RpcError, NetworkError } from '@spree-finance/spree-evm-sdk';
try {
const tx = await vault.deposit(1_000_000n, receiver);
} catch (error) {
if (error instanceof NetworkError) {
console.error('Network issue:', error.message);
} else if (error instanceof RpcError) {
console.error('RPC error:', error.code, error.message);
}
}API Reference
Configuration
All contract wrappers accept a BaseContractConfig:
interface BaseContractConfig {
rpcUrl: string; // JSON-RPC endpoint URL
chainId: bigint; // Chain ID (e.g., 84532n for Base Sepolia)
contractAddress: Hex; // Contract address
from: Hex; // Sender address for nonce lookup
defaultGasPriceWei: bigint; // Default gas price
defaultGasLimit: bigint; // Default gas limit
}Types
type Hex = `0x${string}`; // Branded hex string
interface TxOverrides {
gasPriceWei?: bigint;
gasLimit?: bigint;
valueWei?: bigint;
}Development
# Install dependencies
npm install
# Build
npm run build
# Run SDK tests
npm test
npm run test:coverage
# Run example tests
cd examples
npm install
npm test
npm run test:coveragePre-publish Checklist
Before publishing to npm, run the full validation:
npm run prepublishOnlyThis runs: lint → test → build
See AGENTS.md for detailed development guidelines and CLAUDE.md for Claude Code specific instructions.
Changelog
See CHANGELOG.md for version history and release notes.
License
MIT
