A simple example showing the exact flow from your diagram: Wallet → Middle Contract → Double Contract using Cross-Program Invocation (CPI) without Anchor framework.
Following your diagram exactly:
- Wallet sends a transaction to Middle Contract
- Middle Contract receives the request and calls Double Contract (System Program)
- Double Contract creates a new data account with 1 SOL
- The data account (2) gets created successfully
This is Cross-Program Invocation (CPI) - where the Middle Contract calls the Double Contract on behalf of the Wallet.
Based on your diagram, here's what you should rename:
Current Structure:
├── contract/ # This is your "Middle Contract"
├── client/ # This is your "Wallet"
└── basic-client/ # Rename this to "direct-wallet"
Rename to match your diagram:
# Rename the folders to match your image
mv contract/ middle-contract/
mv client/ wallet/
mv basic-client/ direct-wallet/New Structure:
├── middle-contract/ # The Middle Contract from your diagram
├── wallet/ # The Wallet from your diagram
└── direct-wallet/ # Shows direct wallet→system program (no middle contract)
This is the Middle Contract from your diagram. Here's what it does:
pub fn process_instruction(
program_id: &Pubkey, // Middle Contract's ID
accounts: &[AccountInfo], // Accounts from Wallet
_instruction_data: &[u8], // Data from Wallet (empty)
) -> ProgramResult {Step 1: Receive accounts from Wallet
let payer_account = next_account_info(iter)?; // Wallet account
let pda_account = next_account_info(iter)?; // Future data account (2)
let system_program = next_account_info(iter)?; // Double Contract (System Program)Step 2: Create PDA for data account (2)
let (pda, bump) = Pubkey::find_program_address(
&[b"client1", payer_pubkey.as_ref()], // Seeds for data account (2)
&program_id, // Middle Contract controls it
);What happens here:
- Middle Contract creates a special address (PDA) for data account (2)
- This PDA can only be controlled by Middle Contract
- The address is predictable using: "client1" + Wallet address + Middle Contract ID
Step 3: Middle Contract calls Double Contract
let ix = create_account(
&payer_account.key, // Wallet pays
&pda, // Address for data account (2)
1000000000, // Put 1 SOL in data account (2)
4, // 4 bytes of storage
&SYSTEM_PROGRAM_ID, // Double Contract (System Program) owns it
);Step 4: Execute the call to Double Contract (CPI)
let signer_seeds = &[b"client1", payer_pubkey.as_ref(), &[bump]];
invoke_signed(&ix, accounts, &[signer_seeds])?;This is the CPI! Middle Contract is calling Double Contract (System Program) to create data account (2).
This represents the Wallet from your diagram:
// Wallet sets up the blockchain
liveSvm = new LiteSVM();
programId = PublicKey.unique(); // Middle Contract ID
payer = Keypair.generate(); // Wallet keypair
// Wallet deploys Middle Contract
liveSvm.addProgramFromFile(programId, "./contract.so");
// Wallet gets some SOL
liveSvm.airdrop(payer.publicKey, BigInt(100000000000));
// Wallet calculates where data account (2) will be created
[pda, bump] = PublicKey.findProgramAddressSync(
[Buffer.from("client1"), payer.publicKey.toBuffer()],
programId // Middle Contract ID
);Wallet creates transaction to Middle Contract:
let ix = new TransactionInstruction({
keys: [
{ pubkey: payer.publicKey, isSigner: true, isWritable: true }, // Wallet
{ pubkey: pda, isSigner: false, isWritable: true }, // data account (2)
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false } // Double Contract
],
programId, // Middle Contract ID
data: Buffer.from("") // No data needed
});Wallet sends transaction:
const tx = new Transaction().add(ix);
liveSvm.sendTransaction(tx);
// Wallet checks if data account (2) was created with 1 SOL
const balance = liveSvm.getBalance(pda);
expect(Number(balance)).toBe(1000000000);This shows what happens when Wallet talks directly to Double Contract (skipping Middle Contract):
// Direct Wallet → Double Contract (no Middle Contract)
const instruction = SystemProgram.createAccount({
fromPubkey: kp.publicKey, // Wallet pays
newAccountPubkey: dataAccount.publicKey, // Regular account (not PDA)
lamports: 1000_000_000, // 1 SOL
space: 8, // Storage space
programId: SystemProgram.programId // Double Contract owns it
});
await conn.sendTransaction(tx, [kp, dataAccount]);- Wallet creates a transaction
- Wallet sends transaction to Middle Contract
- Middle Contract receives the transaction
- Middle Contract calls Double Contract (System Program) via CPI
- Double Contract creates data account (2) with 1 SOL
- Success! data account (2) now exists
With Middle Contract (your main project):
- Creates data account (2) at a PDA address
- Only Middle Contract can control this account
- Address is predictable and unique
Direct Wallet (simple version):
- Creates regular account
- Wallet controls the account directly
- Uses random keypair for address
├── middle-contract/ # Your Middle Contract (Rust program)
│ ├── src/lib.rs # Main contract logic
│ └── Cargo.toml
├── wallet/ # Your Wallet (test client)
│ ├── index.test.ts # Wallet test
│ ├── contract.so # Compiled Middle Contract
│ └── package.json
└── direct-wallet/ # Direct Wallet example
├── index.ts # Direct System Program call
└── package.json
-
Build Middle Contract:
cd middle-contract cargo build-bpf cp target/deploy/empty-contract.so ../wallet/contract.so -
Test Wallet → Middle Contract → Double Contract:
cd wallet bun install bun test
-
Try Direct Wallet → Double Contract:
cd direct-wallet bun install bun run index.ts