If you are familiar with the older EVM (Ethereum) version of Bitsave, you know it used a "Factory" pattern where the main contract deployed unique "Child Contracts" for every single user to hold their funds.
Solana works fundamentally differently. On Solana, the logic (the Program) is completely separated from the data (the State). We don't deploy new contracts for users. Instead, a single, highly optimized Anchor program securely partitions and manages data using Program Derived Addresses (PDAs).
There are three core state accounts you need to know about:
- GlobalState: This is the master configuration account. It exists once per protocol deployment. It holds global variables like the
total_value_locked, supported token mints, and protocol fees. - UserVault: When a user registers with Bitsave, the protocol derives a unique
UserVaultPDA mathematically linked to their wallet address. This account acts as their personal profile on the protocol. - Saving: Instead of stuffing all of a user's savings plans into a massive, expensive list, each specific savings goal (e.g., "Car Fund", "Vacation") gets its very own PDA. This account stores the exact amount saved, the maturity date, early withdrawal penalties, and validity state.
When a user interacts with the protocol, they follow a standard lifecycle:
Before a user can save, they must join. They call this instruction to pay a small registration fee (in SOL) to the protocol admin. In return, the program initializes their personalized UserVault PDA.
The user specifies a name for their goal, a maturity time (when they are allowed to withdraw without penalties), and an amount.
- They can save native SOL or any supported SPL Token.
- The funds are securely moved from their personal wallet into a Protocol Vault Account that only the Bitsave program can control (specifically, a PDA derived for their vault).
- A new
SavingPDA is created to track this specific goal.
If a user wants to add more funds to a goal before it matures, they call this instruction. It pulls more funds from their wallet into the protocol vault and updates the total balance in the Saving account.
When the user is ready, they call withdraw.
- If they waited until maturity: The program returns their full saved funds back to their wallet.
- If they withdrew early: The program applies the penalty percentage they agreed to upon creation, deducts it from their savings, and sends the remaining balance back to their wallet.
- In both cases, the
SavingPDA is closed, and its rent is returned to the user.
- No Middlemen: The Bitsave program does not hold user funds directly in its main execution account. Instead, it creates accounts that are mathematically owned by the user's
UserVaultPDA. - Explicit Validation: To prevent sophisticated attacks, the program explicitly verifies that any provided SPL Token accounts match the exact mint and ownership expected by the user's vault before interacting with them.
- Cross-Program Invocations (CPIs): When funds move, Bitsave doesn't do the accounting itself. It securely asks the native Solana System Program (for SOL) or the SPL Token Program (for tokens) to perform the transfer.
- Mathematical Proofs: You cannot access someone else's savings. Because a
SavingPDA is derived using[User Wallet Address + Vault Address + Saving Name], the Solana runtime guarantees that only the user holding the private keys to that wallet can interact with or withdraw those specific funds.
Integrating the Bitsave protocol into a modern frontend is straightforward using Anchor's TypeScript client.
Install the required Solana and Anchor libraries in your Next.js/React project:
npm install @solana/web3.js @coral-xyz/anchor @solana/wallet-adapter-react @solana/spl-tokenYou will also need the IDL (Interface Description Language) file generated by Anchor (found in target/idl/bitsave.json after running anchor build). This file tells the frontend exactly how to talk to the smart contract.
In your React component, use the wallet adapter to get the user's connection and initialize the Anchor Program.
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
import { AnchorProvider, Program, setProvider } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import idl from "./path/to/bitsave.json"; // Your IDL file
import { Bitsave } from "./path/to/bitsave_types"; // Optional: TypeScript types
const PROGRAM_ID = new PublicKey(
"8p4LcCZUsg53vjBP6F2cuWUuZNtHqX8v2EF72oQFkoLn",
);
export function useBitsaveProgram() {
const { connection } = useConnection();
const wallet = useAnchorWallet();
if (!wallet) return null;
const provider = new AnchorProvider(connection, wallet, {
preflightCommitment: "processed",
});
setProvider(provider);
const program = new Program(
idl as any,
PROGRAM_ID,
provider,
) as Program<Bitsave>;
return program;
}Before you can send a transaction, your frontend needs to calculate the specific PDA addresses for the user.
import { PublicKey } from "@solana/web3.js";
// Get the Global State Address
const [globalStatePDA] = PublicKey.findProgramAddressSync(
[Buffer.from("global_state")],
PROGRAM_ID,
);
// Get the current User's Vault Address
const [userVaultPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("user_vault"), wallet.publicKey.toBuffer()],
PROGRAM_ID,
);
// Get a specific Savings Plan Address by its name
const savingName = "New Car Fund";
const [savingPDA] = PublicKey.findProgramAddressSync(
[Buffer.from("saving"), userVaultPDA.toBuffer(), Buffer.from(savingName)],
PROGRAM_ID,
);Use the program.methods API to construct and send transactions.
const joinProtocol = async () => {
const program = useBitsaveProgram();
try {
const tx = await program.methods
.joinBitsave()
.accounts({
globalState: globalStatePDA,
userVault: userVaultPDA,
user: wallet.publicKey,
adminAccount: new PublicKey("ADMIN_WALLET_ADDRESS_HERE"),
})
.rpc();
console.log("Successfully joined! Tx:", tx);
} catch (error) {
console.error("Failed to join:", error);
}
};import { BN } from "@coral-xyz/anchor";
import { SystemProgram, LAMPORTS_PER_SOL } from "@solana/web3.js";
import {
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
const createSolSaving = async (
name: string,
amountInSol: number,
daysToMaturity: number,
) => {
const program = useBitsaveProgram();
const amount = new BN(amountInSol * LAMPORTS_PER_SOL);
const maturityTime = new BN(
Math.floor(Date.now() / 1000) + daysToMaturity * 24 * 60 * 60,
);
const penaltyPercentage = 10;
const safeMode = false;
try {
const tx = await program.methods
.createSaving(name, maturityTime, penaltyPercentage, safeMode, amount)
.accounts({
globalState: globalStatePDA,
userVault: userVaultPDA,
saving: savingPDA,
user: wallet.publicKey,
adminAccount: new PublicKey("ADMIN_WALLET_ADDRESS_HERE"),
tokenMint: PublicKey.default,
userTokenAccount: wallet.publicKey,
vaultTokenAccount: userVaultPDA,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
})
.rpc();
console.log("Saving plan created! Tx:", tx);
} catch (error) {
console.error("Failed to create saving:", error);
}
};const fetchUserData = async () => {
const program = useBitsaveProgram();
try {
// Fetch the user's main profile
const vaultData = await program.account.userVault.fetch(userVaultPDA);
console.log("User Vault Owner:", vaultData.owner.toBase58());
// Fetch a specific savings plan details
const savingData = await program.account.saving.fetch(savingPDA);
console.log("Amount Saved:", savingData.amount.toString());
console.log(
"Maturity Time:",
new Date(savingData.maturityTime.toNumber() * 1000).toLocaleString(),
);
} catch (error) {
console.error("Error fetching data. User might not be registered.", error);
}
};