Access and iterate over account metadata within a Solana(Anchor) Program? - solana

To access and iterate over Solana account metadata on the client one can simply use solana/web3.js. But how can I access the same data within the solana(Anchor) program itself. How can I get the number of tweet accounts created and access the pubKeys of the account authors. I want this to be done on-chain to prevent any users from manipulating the front-end code for their own benefit, such as minting/transferring utility tokens to themselves rather than other users of the dApp. Basically how can the web3 code below be implemented on-chain?
Below is the web3.js version of what I want to do within the program.
const tweetAccounts = await program.account.tweet.all();
let totalAccounts = tweetAccounts.length;
for (let tweetAccount of tweetAccounts) {
let allKeys = [];
let theKey = tweetAccount.author.publicKey;
allKeys.push(theKey);
}
Solana Program Code I have to create tweet account.
pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String, review: i32) -> ProgramResult {
let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
let author: &Signer = &ctx.accounts.author;
let clock: Clock = Clock::get().unwrap();
if topic.chars().count() > 50 {
return Err(ErrorCode::TopicTooLong.into())
}
if content.chars().count() > 280 {
return Err(ErrorCode::ContentTooLong.into())
}
tweet.author = *author.key;
tweet.timestamp = clock.unix_timestamp;
tweet.topic = topic;
tweet.content = content;
tweet.review = review;
Ok(())
}
#[derive(Accounts)]
pub struct SendTweet<'info> {
#[account(init, payer = author, space = Tweet::LEN)]
pub tweet: Account<'info, Tweet>,
#[account(mut)]
pub author: Signer<'info>,
pub system_program: Program<'info, System>,
}

It's impossible to dynamically load new accounts on-chain, meaning every account that you want to use must all be specified up front.
For your situation, this means that there is no immediate solution! The best way to get around this is to create a global account with all of the required counters, and update it as needed on every instruction.
I wish there were a more satisfying response, but this is the best I can come up with.

Related

Solana: Error processing Instruction 0: Cross-program invocation with unauthorized signer or writable account, Transfer sol to wallet without pri key

I am trying to implement Program Flow of a bet game in solana with following features:
A Player transfers some amount to house wallet (deposit_wallet) and plays a game
System records the win loss net balance in a player specific PDA account (player_stats)
If the net balance is plus a player can withdraw the win amount.
On withdraw request the program transfers the amount from house wallet (deposit_wallet or separate withdraw_wallet) to the player
Here is the implementation code for instruction
Initialize instruction on first play (works ok):
#[derive(Accounts)]
#[instruction(withdraw_bump: u8)]
pub struct RegisterPlayer<'info> {
#[account(mut)]
pub player: Signer<'info>,
// space: 8 discriminator + 2 level + 8 balance + 1 latest_result + 1 bump
#[account(
init,
payer = player,
space = 8 + std::mem::size_of::<PlayerStats>(), seeds = [b"player-stats", player.key().as_ref()], bump
)]
pub player_stats: Account<'info, PlayerStats>,
#[account(
init,
payer = player,
space = 8 + std::mem::size_of::<Game>(), seeds = [b"game", player.key().as_ref()], bump
)]
pub game: Account<'info, Game>,
#[account(mut, address = Pubkey::from_str(FEE_WALLET).unwrap())]
/// CHECK: This is not dangerous because we don't read or write
pub deposit_wallet: AccountInfo<'info>,
/// CHECK:
#[account(mut, seeds = [b"withdraw-fund"], bump=withdraw_bump)]
pub withdraw_fund: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
pub fn register_player(ctx: Context<RegisterPlayer>, _withdraw_bump: u8) -> Result<()> {
let player_stats = &mut ctx.accounts.player_stats;
player_stats.level = 0;
player_stats.win_balance = 0;
player_stats.latest_result_status = 0;
player_stats.bump = *ctx.bumps.get("player_stats").unwrap();
let game = &mut ctx.accounts.game;
game.bump = *ctx.bumps.get("game").unwrap();
Ok(())
}
Game Play Instruction (works ok):
pub struct PlayGame<'info> {
pub player: Signer<'info>,
#[account(mut, seeds = [b"player-stats", player.key().as_ref()], bump = player_stats.bump)]
pub player_stats: Account<'info, PlayerStats>,
#[account(mut, seeds = [b"game", player.key().as_ref()], bump = game.bump)]
pub game: Account<'info, Game>,
#[account(mut, address = Pubkey::from_str(FEE_WALLET).unwrap())]
/// CHECK: Not modifying
pub deposit_wallet: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
impl<'info> PlayGame<'info> {
fn transfer_fee_cpi(&self, amount:u32) -> Result<()> {
// logic to transfer fee from user account to house account
// works as expected
Ok(())
}
fn play_game(..) {
// logic to play game
this.transfer_fee_cpi(amount)
}
}
Claim win amount (throws error):
#[derive(Accounts)]
#[instruction(withdraw_bump: u8)]
pub struct ClaimWin<'info> {
pub player: Signer<'info>,
#[account(
mut,
seeds = [b"player-stats", player.key().as_ref()],
bump = player_stats.bump
)]
pub player_stats: Account<'info, PlayerStats>,
/// CHECK:
#[account(mut, seeds = [b"withdraw-fund"], bump=withdraw_bump)]
pub withdraw_fund: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
impl<'info> ClaimWin<'info> {
fn transfer_win_cpi(&self, amount: u64) -> Result<()> {
msg!("cpi transfering win amount {} lambot from [{}] to [{}]", amount, self.withdraw_fund.key , self.player.key);
msg!("owner of withdraw fund:: {}", self.withdraw_fund.owner);
let cpi_context = CpiContext::new(
self.system_program.to_account_info(),
system_program::Transfer {
from: self.withdraw_fund.to_account_info(),
to: self.player.to_account_info(),
});
system_program::transfer(cpi_context, amount)?;
Ok(())
}
pub fn claim_win(ctx: Context<ClaimWin>, _withdraw_bump: u8) -> Result<()> {
const MAXIMUM_LIMIT: u64 = 10000000000;
let current_balance = ctx.accounts.player_stats.win_balance;
if current_balance > MAXIMUM_LIMIT {
return Err(GameError::MaxClaimAmountExceeded.into())
}
// transfer to player
ctx.accounts.transfer_win_cpi(current_balance.clone())?;
ctx.accounts.player_stats.win_balance = 0;
Ok(())
}
}
Issue
Although initialize and transfer of sol from player to house wallet in play instruction is working fine, withdrawal from house wallet to player is not working fine with following error
Cross-program invocation with unauthorized signer or writable account
Some of my thoughts regarding the issue:
since house wallet private key is not available in the program, planned to use a PDA account instead "withdraw_fund" which in my understanding in owned by the program to make the transfer possible.
PDA
/// CHECK: // TODO: make better seed
#[account(mut, seeds = [b"withdraw-fund"], bump=withdraw_bump)]
pub withdraw_fund: AccountInfo<'info>,
Tried using custom state struct instead of AccountInfo but need of init didn't make sense for this.
Transfer from withdraw_fund PDA to Player
let cpi_context = CpiContext::new(
self.system_program.to_account_info(),
system_program::Transfer {
from: self.withdraw_fund.to_account_info(),
to: self.player.to_account_info(),
});
system_program::transfer(cpi_context, amount)?;
The operator can airdop sol to PDA account "withdraw_fund" on timely basis to keepup with the withdrawal request.
Is this approach sensible/possible? What is other/appropriate way to implement above feature of transfer of sol from house wallet to players without the program saving private key of house wallet which is a security issue?

Make a Solana Data Account read only

I'm writing a program in Solana, I have to save data on the chain but make the data read only.
Right now I'm using anchor and I've wrote this:
pub fn save_data(ctx: Context<SaveData>, content: String, actor: String) -> Result<()> {
let service_account = &mut ctx.accounts.service_account;
let data_account = &mut ctx.accounts.data_account;
let company_account = &mut ctx.accounts.company_account;
let authority = &mut ctx.accounts.authority;
data_account.content = content;
data_account.actor = actor;
data_account.company = company_account.key();
data_account.authority = authority.key();
data_account.pre_data_key = service_account.current_data_key;
service_account.current_data_key = data_account.key();
Ok(())
}
#[derive(Accounts)]
pub struct SaveData<'info> {
#[account(init, payer = authority, space = 8 + 50 + 32 + 32 + 32 + 32 )]
pub data_account: Account<'info, DataState>,
#[account(mut, has_one = authority)]
pub company_account: Account<'info, CompanyState>,
#[account(mut)]
pub service_account: Account<'info, ServiceState>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct DataState {
content: String,
actor: String,
company: Pubkey,
pub pre_data_key: Pubkey,
pub authority: Pubkey,
}
I didn't implement any update or delete method but I would like to know if it would be possible to make the data content just read only.
Also, right now, if the creator of the dataAccount wants to update the data can update it outside of my smart contract?
Thanks
The account cant be updated outside of your contract since its a PDA(program derived account) and only your program can modify it through the instructions youve written, If you want to make it read only you have to write the program in such a way that it doesnt change the data of the account(just dont make a function that does that)

Solana token transfer using Anchor

I'm having problems doing what I thought would be a straightforward token transfer.
First some code:
#[derive(Accounts)]
#[instruction(amount: u64)]
pub struct TransferTokens<'info> {
#[account(mut)]
pub sender: Signer<'info>,
#[account(mut)]
pub sender_tokens: Account<'info, TokenAccount>,
pub recipient_tokens: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
#[account(address = SYSTEM_PROGRAM_ID)]
pub system_program: Program<'info, System>,
#[account(address = TOKEN_PROGRAM_ID)]
pub token_program: Program<'info, Token>,
}
pub fn transfer_tokens(ctx: Context<TransferTokens>, amount: u64) -> ProgramResult {
let sender = &ctx.accounts.sender;
let sender_tokens = &ctx.accounts.sender_tokens;
let recipient_tokens = &ctx.accounts.recipient_tokens;
let token_program = &ctx.accounts.token_program;
transfer(
CpiContext::new(
token_program.to_account_info(),
Transfer {
from: sender_tokens.to_account_info(),
to: recipient_tokens.to_account_info(),
authority: sender.to_account_info(),
},
),
amount,
)?;
return Ok(());
}
const mint = new PublicKey("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"); // USDC devnet
const sender = program.provider.wallet.publicKey;
const recipient = new PublicKey(otherPublicKey);
const senderATA = await getOrCreateAssociatedTokenAccount(...);
const recipientATA = await getOrCreateAssociatedTokenAccount(...);
let instructions: TransactionInstruction[];
if (senderATA.instruction) instructions.push(senderATA.instruction);
if (recipientATA.instruction) instructions.push(recipientATA.instruction);
if (instructions.length === 0) instructions = undefined;
const price = 1000;
await program.rpc.transferTokens(new BN(price), {
accounts: {
sender: sender,
senderTokens: senderATA.address,
recipientTokens: recipientATA.address,
mint,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID
},
instructions
});
When I'm running this I get:
Transaction simulation failed: Error processing Instruction 0: Cross-program invocation with unauthorized signer or writable account
Program XXX invoke [1]
recipientATA.address's writable privilege escalated
Program XXX consumed 9908 of 200000 compute units
Program XXX failed: Cross-program invocation with unauthorized signer or writable account
Obviously the sender has to sign the transaction, but I guess I don't really understand why I need permission to send tokens to someone else.
The JS code is slightly abbreviated, but what I'm doing is that I'm adding instructions to create the token accounts unless they exist. The weird thing is that this works the first time when I'm including the instructions to create the token accounts, but after that the "writable privilege escalated" error happens.
Can anyone see what I'm doing wrong?
When you transfer tokens from one user to another, both TokenAccounts must be marked as mutable, because under the hood you're subtracting from the amount in one and adding to the amount in the other!
In other words, you need to add #[account(mut)] to both the receiving token account AND the mint it's associated with.
#[account(mut)]
pub recipient_tokens: Account<'info, TokenAccount>,
// ...
#[account(mut)]
pub mint: Account<'info, Mint>,
Both the sender_tokens and recipient_tokens need to be mut (writable) because the token program needs to change both of their balances.
The mint account can be omitted completely because the token program just makes sure both accounts are from the same mint pubkey. The actual data contained in the mint account is not needed for anything during a transfer. Same goes for system program, it is not needed. And the sender doesn't need to be writable, only sender tokens. Also the address constraint on the token program is redundant with Program<'info, Token>. Also I don't see a need for the amount parameter being provided in the accounts struct.
#[derive(Accounts)]
pub struct TransferTokens<'info> {
pub sender: Signer<'info>,
#[account(mut)]
pub sender_tokens: Account<'info, TokenAccount>,
#[account(mut)]
pub recipient_tokens: Account<'info, TokenAccount>,
pub token_program: Program<'info, Token>,
}

Solana Anchor passing Non Owned Account Info to Transfer SOL

I need a program that distributes SOLs owned by PDA to another account.
Defined the context for distribute instruction as below.
#[derive(Accounts)]
pub struct Pool<'info> {
#[account(
mut,
seeds = [
b"pool_account".as_ref()],
bump = pool_account.pool_account_bump
)
]
pool_account: Account<'info, PoolState>,
#[account()]
soho_account: Account<'info, PoolState>
}
The distribute instruction is
pub fn distribute(ctx: Context<Pool>) -> ProgramResult {
let pool_account = &ctx.accounts.pool_account;
let pool_account_balance = pool_account.to_account_info().lamports();
let soho_account = &ctx.accounts.soho_account;
println!("Distributing {:?} lamports ", pool_account_balance,);
let from_pub_key = pool_account.to_account_info().key;
let to_pub_key = soho_account.to_account_info().key;
let ix = system_instruction::transfer(from_pub_key, to_pub_key, pool_account_balance);
anchor_lang::solana_program::program::invoke(
&ix,
&[
ctx.accounts.pool_account.to_account_info(),
soho_account.to_account_info()
],
)?;
Ok(())
}
The test
it(`Distrubutes funds to shareholders`, async () => {
let [poolAccount, poolAccountBump] = await web3.PublicKey.findProgramAddress(
[Buffer.from("pool_account")],
program.programId
)
// Airdrop SOL to pool account PDA
const connection = await anchor.getProvider().connection;
let signature = await connection.requestAirdrop(poolAccount, LAMPORTS_PER_SOL)
await connection.confirmTransaction(signature);
balance = await connection.getBalance(poolAccount);
console.log(`Pool Account: ${poolAccount} balance: ${balance}`)
assert.ok(balance == 1000953520, `Program account has ${balance}`)
const sohoAccount = anchor.web3.Keypair.generate();
signature = await connection.requestAirdrop(sohoAccount.publicKey, LAMPORTS_PER_SOL)
await connection.confirmTransaction(signature);
await program.rpc.distribute({
accounts: {
poolAccount: poolAccount,
sohoAccount: sohoAccount.publicKey,
// soho2: soho2.publicKey,
}
})
})
When I run the test for the distribute instruction, the test fails w/
Error: 3007: The given account is owned by a different program than expected message.
Basically, it expects the soho_account to be owned by the program. however it should be an external account.
The system transfer requires that, at a minimum, the from account is a System Owned account. Not a PDA and not a Program Owned account.
If the PDA is owned by your program then your program can transfer from it to ANY account without invoking the system transfer.
See the cookbook: https://solanacookbook.com/references/programs.html#transferring-lamports

Anchor: Error when trying to initialize a program derived account (PDA)

I get the below error whenever try to initialize PDA account:
Error: failed to send transaction: Transaction simulation failed: Error processing Instruction 0: Cross-program invocation with unauthorized signer or writable account
#[program]
pub mod myprogram {
use super::*;
pub fn initialize(ctx: Context<Initialize>, bump:u8) -> ProgramResult {
let base_account: &mut Account<BaseAccount> = &mut ctx.accounts.base_account;
base_account.bump = bump;
base_account.counter = Some(0);
return Ok(());
}
}
#[derive(Accounts)]
#[instruction(bump:u8)]
pub struct Initialize<'info> {
#[account(
seeds = [b"seed".as_ref()],
bump, init, payer = creator, space = 20000)]
pub base_account: Account<'info, BaseAccount>,
#[account(mut)]
pub creator: Signer<'info>,
#[account(address = system_program::ID)]
pub system_program: AccountInfo<'info>,
}
#[account]
#[derive(Default)]
pub struct BaseAccount {
pub counter: Option<u64>,
pub bump: u8,
}
My test code looks like this:
const [baseAccountPDA, baseAccountPDABump] = await anchor.web3.PublicKey.findProgramAddress(
[Buffer.from("seed")],
program.programId
);
await program.rpc.initialize(baseAccountPDABump, {
accounts: {
baseAccount: baseAccountPDA,
creator: program.provider.wallet.publicKey,
systemProgram: anchor.web3.SystemProgram.programId,
},
signers: [],
});
I have tried using a newly generated keypair as the creator, and adding that keypair to the signers, but i cannot seem to get this to work.
Turns out this code is the correct way to create a PDA :) I had a test-validator running so it was trying to execute against a previously existing contract!
as you mentioned you should close test-validator terminal before execute
anchor test
anchor run test-validator for you by itself and after test you can see the test-ledger folder inside your solana anchor project root
another point is you should add test-ledger folder to your .gitignore file.
maybe it helps.

Resources