-
Notifications
You must be signed in to change notification settings - Fork 3
Feat/redesign #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Feat/redesign #10
Changes from all commits
2c4a414
0609348
3a34c03
c12c92b
f7be03b
d46e2c2
a9d47da
021ab1a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,19 +1,20 @@ | ||
| # ───────────────────────────────────────────────────────────────────────────── | ||
| # BunkerCash program — compile-time governance keys (EXAMPLE — safe to commit) | ||
| # Copy this file to .env and fill in your real values before building. | ||
| # BunkerCash program — compile-time testnet governance keys (public addresses) | ||
| # Copy this file to .env before reproducing the current testnet build. | ||
| # ───────────────────────────────────────────────────────────────────────────── | ||
|
|
||
| # Squads v4 multisig address (created at https://devnet.squads.so) | ||
| SQUADS_MULTISIG_PUBKEY=<YOUR_SQUADS_MULTISIG_ADDRESS> | ||
| # Squads v4 multisig address on Solana testnet. | ||
| SQUADS_MULTISIG_PUBKEY=GD3jX4ixATyMN5QZGfhGZeYgz3b6bgPjSBF3qtzEfYew | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [major] Is the on-chain governance cutover meant to land in this PR? |
||
| # Squads v4 Vault PDA (vaultIndex = 0) — this is the signer used when executing | ||
| # multisig transactions, and is what you set as pool.admin after governance transfer. | ||
| # Derived by `@sqds/multisig` as: getVaultPda({ multisigPda, index: 0 }) with programId=SQDS4... | ||
| SQUADS_VAULT_PUBKEY=<YOUR_SQUADS_VAULT_ADDRESS> | ||
| SQUADS_VAULT_PUBKEY=FbNDhcb4hwa8bVDMptJrCZho8sCv4pzVqR1EVHKFza8m | ||
|
|
||
| # ── 4 Squads member signers ─────────────────────────────────────────────────── | ||
| # The 4 wallet addresses that are members of the Squads multisig. | ||
| SQUADS_MEMBER_1=<MEMBER_1_WALLET> | ||
| SQUADS_MEMBER_2=<MEMBER_2_WALLET> | ||
| SQUADS_MEMBER_3=<MEMBER_3_WALLET> | ||
| SQUADS_MEMBER_4=<MEMBER_4_WALLET> | ||
| SQUADS_MEMBER_1=3BXEsRgUmrTudbZDzQjDpA2mvwV7vDC73WGjhHPRGBee | ||
| SQUADS_MEMBER_2=8NSGVEq9CY25Fy6ujBMa9CRsmWz9AWhGV7URQF8Acof7 | ||
| SQUADS_MEMBER_3=Hmod5q5Egi1yqiRCAAgZBh1iD8o8kALVQV8WKBM84JhK | ||
| # Unused fourth slot; this is a valid public key with no signer in the multisig. | ||
| SQUADS_MEMBER_4=11111111111111111111111111111112 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| /** | ||
| * Approve (if needed) and execute a pending Squads v4 vault transaction. | ||
| * | ||
| * Intended for the second multisig member to approve the create_bunkercash_mint | ||
| * proposal created by propose-create-mint.ts, and execute it once the 2-of-3 | ||
| * threshold is met. Safe to run with the original proposer's key too — it skips | ||
| * the vote if the member already approved and just attempts execution. | ||
| * | ||
| * Run: | ||
| * cd ts/apps/web && TX_INDEX=<index> KEYPAIR_PATH=<member-keypair.json> \ | ||
| * npx tsx ../../../rs/scripts/approve-and-execute-mint-proposal.ts | ||
| * Env: RPC_URL (default testnet), TX_INDEX (required), KEYPAIR_PATH | ||
| */ | ||
| import { readFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { resolve } from "node:path"; | ||
| import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; | ||
| import * as multisig from "@sqds/multisig"; | ||
| import bs58 from "bs58"; | ||
|
|
||
| const MULTISIG_PDA = new PublicKey("GD3jX4ixATyMN5QZGfhGZeYgz3b6bgPjSBF3qtzEfYew"); | ||
| const RPC_URL = process.env.RPC_URL ?? "https://api.testnet.solana.com"; | ||
| const KEYPAIR_PATH = process.env.KEYPAIR_PATH ?? "~/.config/solana/id.json"; | ||
|
|
||
| /** Accepts a solana-keygen JSON array file OR a base58 private-key string | ||
| * (the format Phantom/Solflare export) on a single line. */ | ||
| function loadKeypair(path: string): Keypair { | ||
| const file = readFileSync( | ||
| path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : path, | ||
| "utf8", | ||
| ).trim(); | ||
| if (file.startsWith("[")) { | ||
| return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(file) as number[])); | ||
| } | ||
| return Keypair.fromSecretKey(bs58.decode(file)); | ||
| } | ||
|
|
||
| async function main() { | ||
| const txIndexRaw = process.env.TX_INDEX; | ||
| if (!txIndexRaw) throw new Error("TX_INDEX env var is required"); | ||
| const transactionIndex = BigInt(txIndexRaw); | ||
|
|
||
| const keypair = loadKeypair(KEYPAIR_PATH); | ||
| const connection = new Connection(RPC_URL, "confirmed"); | ||
|
|
||
| const [proposalPda] = multisig.getProposalPda({ | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| }); | ||
| const proposal = await multisig.accounts.Proposal.fromAccountAddress( | ||
| connection, | ||
| proposalPda, | ||
| ); | ||
| const approved = proposal.approved.map((k) => k.toBase58()); | ||
| console.log("Proposal:", proposalPda.toBase58()); | ||
| console.log("Current approvals:", approved.join(", ") || "(none)"); | ||
|
|
||
| const ms = await multisig.accounts.Multisig.fromAccountAddress(connection, MULTISIG_PDA); | ||
| const threshold = ms.threshold; | ||
|
|
||
| if (!approved.includes(keypair.publicKey.toBase58())) { | ||
| const approveIx = multisig.instructions.proposalApprove({ | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| member: keypair.publicKey, | ||
| }); | ||
| const tx = new Transaction().add(approveIx); | ||
| const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); | ||
| tx.recentBlockhash = blockhash; | ||
| tx.lastValidBlockHeight = lastValidBlockHeight; | ||
| tx.feePayer = keypair.publicKey; | ||
| tx.sign(keypair); | ||
| const sig = await connection.sendRawTransaction(tx.serialize()); | ||
| await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }); | ||
| console.log("Approved. Signature:", sig); | ||
| } else { | ||
| console.log("This member already approved — skipping vote."); | ||
| } | ||
|
|
||
| const refreshed = await multisig.accounts.Proposal.fromAccountAddress( | ||
| connection, | ||
| proposalPda, | ||
| ); | ||
| if (refreshed.approved.length < threshold) { | ||
| console.log( | ||
| `Approvals ${refreshed.approved.length}/${threshold} — threshold not met yet, cannot execute.`, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| const { instruction: executeIx, lookupTableAccounts } = | ||
| await multisig.instructions.vaultTransactionExecute({ | ||
| connection, | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| member: keypair.publicKey, | ||
| }); | ||
|
|
||
| const tx = new Transaction().add(executeIx); | ||
| const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); | ||
| tx.recentBlockhash = blockhash; | ||
| tx.lastValidBlockHeight = lastValidBlockHeight; | ||
| tx.feePayer = keypair.publicKey; | ||
| tx.sign(keypair); | ||
| void lookupTableAccounts; | ||
| const sig = await connection.sendRawTransaction(tx.serialize()); | ||
| await connection.confirmTransaction({ signature: sig, blockhash, lastValidBlockHeight }); | ||
| console.log("Executed. Signature:", sig); | ||
|
|
||
| const idlJson = require("../../ts/apps/web/lib/bunkercash.fixed.idl.json") as { | ||
| address: string; | ||
| }; | ||
| const [mintPda] = PublicKey.findProgramAddressSync( | ||
| [Buffer.from("bunkercash_mint")], | ||
| new PublicKey(idlJson.address), | ||
| ); | ||
| const mintInfo = await connection.getAccountInfo(mintPda); | ||
| console.log( | ||
| "Mint PDA", | ||
| mintPda.toBase58(), | ||
| mintInfo ? "now exists — buy flow is unblocked." : "still missing (execution may have failed).", | ||
| ); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| /** | ||
| * Propose `create_bunkercash_mint` through the Squads v4 vault that owns the pool. | ||
| * | ||
| * The pool's master_wallet on testnet is the Squads vault PDA (threshold 2-of-3), | ||
| * so the mint can only be created via a vault transaction. This script: | ||
| * 1. builds the create_bunkercash_mint instruction with the vault as admin | ||
| * 2. creates the vault transaction + proposal | ||
| * 3. approves it with the local keypair (1 of 2 required votes) | ||
| * | ||
| * After a second member approves (approve-and-execute-mint-proposal.ts), | ||
| * anyone can execute. | ||
| * | ||
| * Run: | ||
| * cd ts/apps/web && npx tsx ../../../rs/scripts/propose-create-mint.ts | ||
| * Env (optional): RPC_URL, KEYPAIR_PATH | ||
| */ | ||
| import { readFileSync } from "node:fs"; | ||
| import { homedir } from "node:os"; | ||
| import { resolve } from "node:path"; | ||
| import { AnchorProvider, Program, Wallet, type Idl } from "@coral-xyz/anchor"; | ||
| import { | ||
| Connection, | ||
| Keypair, | ||
| PublicKey, | ||
| SystemProgram, | ||
| Transaction, | ||
| TransactionMessage, | ||
| } from "@solana/web3.js"; | ||
| import * as multisig from "@sqds/multisig"; | ||
|
|
||
| const TOKEN_2022_PROGRAM_ID = new PublicKey("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"); | ||
| const MULTISIG_PDA = new PublicKey("GD3jX4ixATyMN5QZGfhGZeYgz3b6bgPjSBF3qtzEfYew"); | ||
| const RPC_URL = process.env.RPC_URL ?? "https://api.testnet.solana.com"; | ||
| const KEYPAIR_PATH = process.env.KEYPAIR_PATH ?? "~/.config/solana/id.json"; | ||
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| const idlJson = require("../../ts/apps/web/lib/bunkercash.fixed.idl.json") as { | ||
| address: string; | ||
| } & Idl; | ||
| const PROGRAM_ID = new PublicKey(idlJson.address); | ||
|
|
||
| function loadKeypair(path: string): Keypair { | ||
| const file = readFileSync( | ||
| path.startsWith("~/") ? resolve(homedir(), path.slice(2)) : path, | ||
| "utf8", | ||
| ); | ||
| return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(file) as number[])); | ||
| } | ||
|
|
||
| async function main() { | ||
| const keypair = loadKeypair(KEYPAIR_PATH); | ||
| const connection = new Connection(RPC_URL, "confirmed"); | ||
| const provider = new AnchorProvider(connection, new Wallet(keypair), { | ||
| commitment: "confirmed", | ||
| }); | ||
| const program = new Program(idlJson as unknown as Idl, provider); | ||
|
|
||
| const [poolPda] = PublicKey.findProgramAddressSync([Buffer.from("pool")], PROGRAM_ID); | ||
| const [mintPda] = PublicKey.findProgramAddressSync( | ||
| [Buffer.from("bunkercash_mint")], | ||
| PROGRAM_ID, | ||
| ); | ||
| const [vaultPda] = multisig.getVaultPda({ multisigPda: MULTISIG_PDA, index: 0 }); | ||
|
|
||
| const existing = await connection.getAccountInfo(mintPda); | ||
| if (existing) { | ||
| console.log("Mint already exists:", mintPda.toBase58()); | ||
| return; | ||
| } | ||
|
|
||
| const ms = await multisig.accounts.Multisig.fromAccountAddress(connection, MULTISIG_PDA); | ||
| const transactionIndex = BigInt(ms.transactionIndex.toString()) + 1n; | ||
| console.log("Multisig:", MULTISIG_PDA.toBase58()); | ||
| console.log("Vault (pool admin):", vaultPda.toBase58()); | ||
| console.log("New transaction index:", transactionIndex.toString()); | ||
|
|
||
| const createMintIx = await (program.methods as any) | ||
| .createBunkercashMint() | ||
| .accounts({ | ||
| pool: poolPda, | ||
| bunkercashMint: mintPda, | ||
| admin: vaultPda, | ||
| tokenProgram: TOKEN_2022_PROGRAM_ID, | ||
| systemProgram: SystemProgram.programId, | ||
| }) | ||
| .instruction(); | ||
|
|
||
| const { blockhash } = await connection.getLatestBlockhash(); | ||
| const vaultMessage = new TransactionMessage({ | ||
| payerKey: vaultPda, | ||
| recentBlockhash: blockhash, | ||
| instructions: [createMintIx], | ||
| }); | ||
|
|
||
| const createVaultTxIx = multisig.instructions.vaultTransactionCreate({ | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| creator: keypair.publicKey, | ||
| vaultIndex: 0, | ||
| ephemeralSigners: 0, | ||
| transactionMessage: vaultMessage, | ||
| memo: "create_bunkercash_mint (testnet bootstrap)", | ||
| }); | ||
| const proposalCreateIx = multisig.instructions.proposalCreate({ | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| creator: keypair.publicKey, | ||
| }); | ||
| const approveIx = multisig.instructions.proposalApprove({ | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| member: keypair.publicKey, | ||
| }); | ||
|
|
||
| const tx = new Transaction().add(createVaultTxIx, proposalCreateIx, approveIx); | ||
| const sig = await provider.sendAndConfirm(tx, [keypair]); | ||
|
|
||
| const [proposalPda] = multisig.getProposalPda({ | ||
| multisigPda: MULTISIG_PDA, | ||
| transactionIndex, | ||
| }); | ||
| console.log("\nProposal created and approved by", keypair.publicKey.toBase58()); | ||
| console.log("Signature:", sig); | ||
| console.log("Proposal PDA:", proposalPda.toBase58()); | ||
| console.log("Transaction index:", transactionIndex.toString()); | ||
| console.log( | ||
| "\nNext: a second multisig member must approve, then execute:\n" + | ||
| ` RPC_URL=${RPC_URL} TX_INDEX=${transactionIndex} KEYPAIR_PATH=<member-keypair.json> \\\n` + | ||
| " npx tsx ../../../rs/scripts/approve-and-execute-mint-proposal.ts", | ||
| ); | ||
| } | ||
|
|
||
| main().catch((error) => { | ||
| console.error(error); | ||
| process.exit(1); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[major] Can the intent of this PR be reconstructed from an empty description? The body is empty for a 79-file, +6409/-3021 change spanning the Anchor program cluster config, two Workers' deploy configs (routes, KV/D1 bindings, RPC cluster), a rewritten account decoder, and admin auth code. A reviewer cannot tell an intentional testnet cutover from an accidental local-dev leftover. Fix: a PR description enumerating the deploy/governance/decoder changes, or split them out. Umbra gate: verified major.