Very new at rust and Solana development, hoping to get some advice.
I am creating some blockchain functionality for a betting system. Players will bet on the outcome of a certain event by sending the details of their bet to my Solana program and their bet in SOL to a "fund account" PDA. When the outcome is determined, I determine who the winners are and collect their public keys and how much SOL should be sent to them. The program will use the PDA to send the SOL transactions. I am hoping this can be done in a single transaction to save on transaction fees.
I've created some simple scripts to test this functionality.
This is my rust program:
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
program_error::ProgramError,
system_instruction,
program::invoke_signed,
msg,
log::sol_log,
};
entrypoint!(process_instruction);
// Assuming `TransferRequest` and `process_transfer_requests` from the previous example.
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct TransferRequest {
pub recipient: Pubkey,
pub amount: u64,
}
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct TransferRequests {
pub requests: Vec<TransferRequest>,
}
// Function to create a PDA for the fund account
pub fn create_fund_pda(program_id: &Pubkey) -> (Pubkey, u8) {
// Replace "fundaccountseed" with your chosen seed
Pubkey::find_program_address(&[b"fundAccount"], program_id)
}
// Example function to process payouts, using the PDA for transfers
fn process_payouts(
program_id: &Pubkey,
accounts: &[AccountInfo],
transfer_requests: Vec<TransferRequest>,
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
// Assuming the first account is the PDA for the fund and the second is the system program.
let fund_pda_account = next_account_info(accounts_iter)?;
let system_program_account = next_account_info(accounts_iter)?;
let (fund_pda, bump_seed) = create_fund_pda(program_id);
assert_eq!(&fund_pda, fund_pda_account.key);
for request in transfer_requests {
// Logging is optional and used here for demonstration.
msg!(
"Processing transfer request for recipient: {}, amount: {}",
request.recipient,
request.amount
);
let transfer_instruction = system_instruction::transfer(
&fund_pda_account.key,
&request.recipient,
request.amount,
);
sol_log(&format!("Transfer instruction: {:?}", transfer_instruction));
// Since PDAs cannot sign for themselves, you use `invoke_signed` with the appropriate seeds.
invoke_signed(
&transfer_instruction,
&[fund_pda_account.clone(), system_program_account.clone()],
&[&[&b"fundAccount"[..], &[bump_seed]]],
)?;
}
Ok(())
}
/// The main entry point for the Solana program.
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
sol_log("Received instruction data");
// Log the received instruction data length
sol_log(&format!(
"Instruction data length: {}",
instruction_data.len()
));
match TransferRequests::try_from_slice(instruction_data) {
Ok(transfer_requests) => {
sol_log("Deserialized TransferRequests successfully");
sol_log(&format!("Number of requests: {}", transfer_requests.requests.len()));
// Continue processing as normal...
process_payouts(program_id, accounts, transfer_requests.requests)
}
Err(e) => {
sol_log("Failed to deserialize TransferRequests");
sol_log(&format!("Deserialization error: {:?}", e));
Err(ProgramError::InvalidInstructionData)
}
}
}
And this is my javascript program that interfaces with that:
const solanaWeb3 = require('@solana/web3.js');
const { Keypair, Transaction, TransactionInstruction, PublicKey, SystemProgram } = solanaWeb3;
const borsh = require('borsh');
const fs = require('fs');
const keypairPath = process.argv[2];
if (!keypairPath) {
console.error('Usage: node <script.js> <keypairPath>');
process.exit(1);
}
const keypairData = JSON.parse(fs.readFileSync(keypairPath, 'utf8'));
async function airdropToPDA(connection, fundPDA) {
const LAMPORTS_PER_SOL = solanaWeb3.LAMPORTS_PER_SOL;
let airdropSignature = await connection.requestAirdrop(
fundPDA,
1 * LAMPORTS_PER_SOL
);
await connection.confirmTransaction(airdropSignature);
console.log(`Airdropped 1 SOL to ${fundPDA.toString()}`);
}
class TransferRequest {
constructor({recipient, amount}) {
// Ensure the recipient public key is stored as a raw 32-byte array
this.recipient = new PublicKey(recipient).toBuffer();
this.amount = amount;
}
}
class TransferRequests {
constructor({requests}) {
this.requests = requests;
}
}
const schema = new Map([
[TransferRequest, {kind: 'struct', fields: [['recipient', [32]], ['amount', 'u64']]}],
[TransferRequests, {kind: 'struct', fields: [['requests', [TransferRequest]]]}]
]);
async function sendBulkTransfer(connection, programId, seeds, transferRequests, payer) {
// Derive the PDA
const [fundPDA, bumpSeed] = PublicKey.findProgramAddressSync(seeds, programId);
console.log("fundPDA: ", fundPDA.toBase58());
// Prepare transfer requests
const transfers = transferRequests.map(request => new TransferRequest({recipient: request.recipient, amount: request.amount}));
const bulkTransferData = new TransferRequests({requests: transfers});
const serializedData = borsh.serialize(schema, bulkTransferData);
const keys = [
{ pubkey: fundPDA, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
];
const instruction = new TransactionInstruction({
keys,
programId,
data: serializedData,
});
const transaction = new Transaction().add(instruction);
const signedTransaction = await solanaWeb3.sendAndConfirmTransaction(connection, transaction, [payer]);
console.log("Signed transaction: ", signedTransaction);
}
async function main() {
const connection = new solanaWeb3.Connection(solanaWeb3.clusterApiUrl('devnet'), 'confirmed');
const programId = new solanaWeb3.PublicKey("HFaVb5gk49QzxuNNjSbJKGZ7rC1qR8LqYDNaX539Gntx");
const seeds = [Buffer.from("fundAccount", 'utf8')];
const payer = solanaWeb3.Keypair.fromSecretKey(new Uint8Array(keypairData));
// Derive the PDA
const [fundPDA, _bumpSeed] = PublicKey.findProgramAddressSync(seeds, programId);
// Airdrop to PDA
await airdropToPDA(connection, fundPDA);
// in a loop, create a new keypair, extract the public key, and add it to the transferRequests array
const transferRequests = [];
for (let i = 0; i < 10; i++) {
const keypair = Keypair.generate();
const publicKey = keypair.publicKey;
transferRequests.push(new TransferRequest({recipient: publicKey, amount: 0.001 * solanaWeb3.LAMPORTS_PER_SOL}));
}
//console.log("transfer requests: ", transferRequests);
sendBulkTransfer(connection, programId, seeds, transferRequests, payer)
.then(() => console.log("Bulk transfer submitted."))
.catch((e) => console.error(e));
}
main().then(() => console.log("Completed")).catch(console.error);
Currently, the instructions are correctly being read by the Solana program, but it is encountering an error when trying to invoke_signed
:
'Program 9h64X77j5Anpe3gyBeMx1WpkLVBnyG7nP3YHPcNyLAfK invoke [1]',
'Program log: Received instruction data',
'Program log: Instruction data length: 404',
'Program log: Deserialized TransferRequests successfully',
'Program log: Number of requests: 10',
'Program log: Processing transfer request for recipient: 39QQCWN5n4K45B5NFL2RcuUbGrpk4a2ud7QgKaK2e5WU, amount: 1000000',
'Program log: Transfer instruction: Instruction { program_id: 11111111111111111111111111111111, accounts: [AccountMeta { pubkey: GgfViAgvtuWuW8mHXJwYkaMWHT8t1jVvDoV3jATmfY2A, is_signer: true, is_writable: true }, AccountMeta { pubkey: 39QQCWN5n4K45B5NFL2RcuUbGrpk4a2ud7QgKaK2e5WU, is_signer: false, is_writable: true }], data: [2, 0, 0, 0, 64, 66, 15, 0, 0, 0, 0, 0] }',
'Program 9h64X77j5Anpe3gyBeMx1WpkLVBnyG7nP3YHPcNyLAfK consumed 53253 of 200000 compute units',
'Program 9h64X77j5Anpe3gyBeMx1WpkLVBnyG7nP3YHPcNyLAfK failed: Instruction passed to inner instruction is too large (12884933690 > 1280)'
i.e. it's saying the instruction is too large. I've tried to google this problem and mostly people say to use an earlier version of the Solana CLI but when I do that I run into all kinds of dependency problems. Any help with this would be so appreciated!