Last active
November 16, 2022 05:46
-
-
Save dabit3/7cbd18b8bc4b495c4831f8674902eb42 to your computer and use it in GitHub Desktop.
Persisting a keypair for reading across clients
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* this file includes both a node.js script for creating a keypair | |
* as well as the client code for using it | |
*/ | |
/* createKeypair.js */ | |
const fs = require('fs') | |
const anchor = require("@project-serum/anchor"); | |
const web3 = require('@solana/web3.js') | |
const account = anchor.web3.Keypair.generate(); | |
fs.writeFileSync('./app/src/keypair.json', JSON.stringify(account)) | |
/* App.js */ | |
import './App.css'; | |
import { useEffect, useState } from 'react'; | |
import { | |
Program, | |
Provider, | |
web3, | |
} from '@project-serum/anchor' | |
import { | |
Connection, | |
PublicKey, | |
} from '@solana/web3.js' | |
import idl from './idl.json' | |
import kp from './keypair.json' | |
const arr = Object.values(kp._keypair.secretKey) | |
const secret = new Uint8Array(arr) | |
const pair = web3.Keypair.fromSecretKey(secret) | |
const opts = { | |
preflightCommitment: "processed" | |
} | |
const { SystemProgram } = web3 | |
const programID = new PublicKey(idl.metadata.address) | |
function App() { | |
const [value, setValue] = useState(null) | |
const [connected, setConnected] = useState(false) | |
useEffect(() => { | |
console.log('solana:', window.solana) | |
if (window.solana) { | |
window.solana.on("connect", () => { | |
console.log('updated...') | |
}) | |
} | |
return () => { | |
window.solana.disconnect(); | |
} | |
}, []) | |
async function getProvider() { | |
const wallet = window.solana | |
const network = "http://127.0.0.1:8899" | |
const connection = new Connection(network, opts.preflightCommitment); | |
const provider = new Provider( | |
connection, wallet, opts.preflightCommitment, | |
) | |
return provider | |
} | |
async function createCounter() { | |
const provider = await getProvider() | |
const program = new Program(idl, programID, provider); | |
try { | |
await program.rpc.create({ | |
accounts: { | |
baseAccount: pair.publicKey, | |
user: provider.wallet.publicKey, | |
systemProgram: SystemProgram.programId, | |
}, | |
signers: [pair] | |
}) | |
const account = await program.account.baseAccount.fetch(pair.publicKey) | |
console.log('account: ', account) | |
setValue(account.count.toString()) | |
} catch (err) { | |
console.log("Transaction error: ", err) | |
} | |
} | |
async function increment() { | |
const provider = await getProvider() | |
const program = new Program(idl, programID, provider); | |
await program.rpc.increment({ | |
accounts: { | |
baseAccount: pair.publicKey | |
} | |
}); | |
const account = await program.account.baseAccount.fetch(pair.publicKey); | |
console.log('acc: ', account) | |
setValue(account.count.toString()) | |
} | |
async function getWallet() { | |
await window.solana.connect(); | |
try { | |
const wallet = typeof window !== 'undefined' && window.solana; | |
await wallet.connect() | |
setConnected(true) | |
} catch (err) { | |
console.log('err: ', err) | |
} | |
} | |
console.log("value:", value) | |
if (!connected) { | |
return ( | |
<div className="App"> | |
<button onClick={getWallet}>Connect wallet</button> | |
</div> | |
) | |
} | |
if (connected) return ( | |
<div className="App"> | |
<header> | |
<button onClick={createCounter}>Create counter</button> | |
<button onClick={increment}>Increment counter</button> | |
{ | |
value >= 0 ? ( | |
<h2>{value}</h2> | |
) : ( | |
<h2>Please create the counter.</h2> | |
) | |
} | |
</header> | |
</div> | |
); | |
} | |
export default App; |
@matanwrites the solution here is to use a program derived address (derived from user public key) and not generating any private key on the client.
@knivets triple kudos! thanks for the hint!
Your articles and this one was especially helpful https://www.brianfriel.xyz/understanding-program-derived-addresses/
JS side
const initialize = async () => {
const { pda, bump } = await getProgramDerivedAddress();
const program = await getProgram();
try {
await program.rpc.initialize(new BN(bump), {
accounts: {
user: getProvider().wallet.publicKey,
baseAccount: pda,
systemProgram: web3.SystemProgram.programId,
},
});
}
...
};
const getProgramDerivedAddress = async () => {
const [pda, bump] = await PublicKey.findProgramAddress(
[Buffer.from('my_seed')],
programAddress
);
console.log(`Got ProgramDerivedAddress: bump: ${bump}, pubkey: ${pda.toBase58()}`);
return { pda, bump };
};
const getBaseAccount = async () => {
const { pda } = await getProgramDerivedAddress();
const program = await getProgram();
try {
return await program.account.baseAccount.fetch(pda);;
}
...
};
Solana side
#[program]
pub mod moon {
use super::*;
pub fn initialize(ctx: Context<Initialize>, base_account_bump: u8) -> ProgramResult {
ctx.accounts.base_account.bump = base_account_bump;
Ok(())
}
}
#[derive(Accounts)]
#[instruction(base_account_bump: u8)]
pub struct Initialize<'info> {
// space: depends on what you gonna store and is required if you use a vec or array
#[account(init, seeds = [b"my_seed".as_ref()], bump = base_account_bump, payer = user, space = 9000)]
pub base_account: Account<'info, BaseAccount>,
pub user: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(Default)]
pub struct BaseAccount {
pub bump: u8,
...
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@dabit3 great stuff, still new to this but I have a question regarding security
Is there a way to not leak the secret key in the frontend code?
const baseAccount = web3.Keypair.fromSecretKey(secret);
In my web2 mind I thought about wrapping the fetch account behind a web2 api but in your experience have you found best practices to prevent having your baseAccount's secret key on the frontend code? I have tried injecting with env variables but of course it will still end up in the front end code and can be inspected by looking at the page source.