diff --git a/package-lock.json b/package-lock.json index 30acce797f7..8140b621a13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126142,7 +126142,7 @@ "@metaplex-foundation/umi": "1.2.0", "@metaplex-foundation/umi-bundle-defaults": "1.2.0", "@metaplex-foundation/umi-uploader-irys": "1.2.0", - "@meteora-ag/cp-amm-sdk": "^1.1.7", + "@meteora-ag/cp-amm-sdk": "1.1.7", "@meteora-ag/dynamic-bonding-curve-sdk": "1.3.8", "@noble/curves": "1.4.2", "@pedalboard/basekit": "*", @@ -148781,6 +148781,9 @@ "bs58": "6.0.0" }, "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0", "vitest": "2.1.1" }, "peerDependencies": { @@ -149419,6 +149422,15 @@ "dev": true, "license": "MIT" }, + "packages/spl/node_modules/@types/node": { + "version": "20.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.17.tgz", + "integrity": "sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, "packages/spl/node_modules/@vitest/expect": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", diff --git a/packages/spl/QUICKSTART.md b/packages/spl/QUICKSTART.md new file mode 100644 index 00000000000..dc8aea32c0b --- /dev/null +++ b/packages/spl/QUICKSTART.md @@ -0,0 +1,179 @@ +# Quick Start: Initialize Reward Manager + +This guide will help you initialize a Reward Manager program on Solana. + +## Prerequisites + +1. **Node.js & npm** installed +2. **Solana CLI** installed (for generating keypairs and airdrops) +3. **Funded wallet** with SOL + +## Step-by-Step Guide + +### 1. Install Dependencies + +```bash +cd packages/spl +npm install +``` + +### 2. Generate Keypairs + +You'll need two keypairs: + +- **Payer**: Pays for transactions and account creation +- **Manager**: Admin account that can manage senders + +```bash +# Generate payer keypair (or use existing ~/.config/solana/id.json) +solana-keygen new --outfile ./payer.json --no-bip39-passphrase + +# Generate manager keypair +solana-keygen new --outfile ./manager.json --no-bip39-passphrase +``` + +### 3. Fund Your Payer (Devnet) + +```bash +# Get your payer address +solana-keygen pubkey ./payer.json + +# Airdrop SOL +solana airdrop 2 --url devnet + +# Check balance +solana balance --url devnet +``` + +### 4. Run the Init Script + +```bash +npm run init-reward-manager -- \ + --payer ./payer.json \ + --manager ./manager.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --min-votes 3 \ + --cluster devnet +``` + +### 5. Save the Output + +The script will output important addresses. **Save these!** + +``` +Reward Manager: ABC123... ← Save this! +Token Account: DEF456... ← Save this! +Authority PDA: GHI789... ← Save this! +``` + +The script also automatically saves generated keypairs to files: + +- `reward-manager-.json` +- `token-account-.json` + +## Common Use Cases + +### Devnet Testing + +```bash +npm run init-reward-manager -- \ + --payer ~/.config/solana/id.json \ + --manager ./manager.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --cluster devnet +``` + +### Mainnet Deployment + +```bash +npm run init-reward-manager -- \ + --payer ./mainnet-payer.json \ + --manager ./mainnet-manager.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --min-votes 5 \ + --cluster mainnet-beta +``` + +### With Pre-Generated Accounts + +```bash +npm run init-reward-manager -- \ + --payer ./payer.json \ + --manager ./manager.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --reward-manager-keypair ./reward-manager.json \ + --token-account-keypair ./token-account.json \ + --cluster devnet +``` + +## What Gets Created? + +1. **Reward Manager Account** - Stores program state: + + - Token account reference + - Manager authority + - Minimum votes required + +2. **Token Account** - SPL token account that holds reward tokens + +3. **Authority PDA** - Program-derived address that: + - Owns sender accounts + - Signs token transfers + - Cannot be controlled by any private key + +## Next Steps + +After initialization, you can: + +1. **Fund the Token Account** - Transfer reward tokens to the token account +2. **Create Senders** - Add discovery nodes as authorized attestors +3. **Submit Attestations** - Have senders attest to reward eligibility +4. **Disburse Rewards** - Evaluate attestations and transfer tokens + +See the main [README](./README.md) for more information on these operations. + +## Troubleshooting + +### Error: "Payer has no balance" + +```bash +# Check balance +solana balance ./payer.json --url devnet + +# Request airdrop +solana airdrop 2 $(solana-keygen pubkey ./payer.json) --url devnet +``` + +### Error: "Keypair file not found" + +Make sure you're running from the correct directory and the file paths are correct. + +```bash +# Use absolute paths if needed +npm run init-reward-manager -- \ + --payer /Users/yourname/payer.json \ + --manager /Users/yourname/manager.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM +``` + +### Error: "Account already exists" + +The reward manager or token account keypair is already initialized. Use different keypairs or generate new ones. + +### View Transaction Details + +Check the Solana Explorer link printed at the end: + +``` +šŸ”— View transaction: https://explorer.solana.com/tx/?cluster=devnet +``` + +## Help + +For more options: + +```bash +npm run init-reward-manager -- --help +``` + +For detailed documentation, see [scripts/README.md](./scripts/README.md). diff --git a/packages/spl/package.json b/packages/spl/package.json index f36b37a4f50..612125d1e25 100644 --- a/packages/spl/package.json +++ b/packages/spl/package.json @@ -9,7 +9,8 @@ "start": "tsc --build --verbose --watch tsconfig.all.json", "build": "tsc --build --verbose tsconfig.all.json", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "init-reward-manager": "ts-node --project scripts/tsconfig.json scripts/initRewardManager.ts" }, "repository": { "type": "git", @@ -33,6 +34,9 @@ "@solana/web3.js": "^1.95.8" }, "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0", "vitest": "2.1.1" } } diff --git a/packages/spl/scripts/.gitignore b/packages/spl/scripts/.gitignore new file mode 100644 index 00000000000..065b7af2fc2 --- /dev/null +++ b/packages/spl/scripts/.gitignore @@ -0,0 +1,7 @@ +# Ignore generated keypair files +reward-manager-*.json +token-account-*.json + +# But keep the example +!example-keypair.json + diff --git a/packages/spl/scripts/README.md b/packages/spl/scripts/README.md new file mode 100644 index 00000000000..1e003fad1f2 --- /dev/null +++ b/packages/spl/scripts/README.md @@ -0,0 +1,223 @@ +# Reward Manager Scripts + +This directory contains utility scripts for working with the Reward Manager Solana program. + +## Prerequisites + +```bash +# Install dependencies from the spl package root +cd packages/spl +npm install + +# Make sure you have ts-node installed globally (optional) +npm install -g ts-node typescript +``` + +## Scripts + +### `initRewardManager.ts` + +Initialize a new Reward Manager program on Solana. + +#### Quick Start + +```bash +# Run from packages/spl directory +npm run init-reward-manager -- \ + --payer ~/.config/solana/id.json \ + --manager ./manager-keypair.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM +``` + +#### Usage + +```bash +ts-node scripts/initRewardManager.ts [OPTIONS] +``` + +#### Required Options + +- `--payer, -p ` - Path to payer keypair JSON file +- `--manager, -m ` - Path to manager keypair JSON file +- `--mint
` - Mint address for reward token (e.g., AUDIO) + +#### Optional Options + +- `--min-votes ` - Minimum votes required (default: 3) +- `--cluster, -c ` - Cluster: devnet, testnet, mainnet-beta, or custom URL (default: devnet) +- `--reward-manager-keypair ` - Use existing reward manager keypair (generates new if omitted) +- `--token-account-keypair ` - Use existing token account keypair (generates new if omitted) +- `--help, -h` - Show help message + +#### Examples + +**Initialize on devnet with minimum config:** + +```bash +npm run init-reward-manager -- \ + --payer ~/.config/solana/id.json \ + --manager ./manager-keypair.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM +``` + +**Initialize on mainnet with 5 required votes:** + +```bash +npm run init-reward-manager -- \ + --payer ~/.config/solana/id.json \ + --manager ./manager-keypair.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --min-votes 5 \ + --cluster mainnet-beta +``` + +**Use pre-generated keypairs:** + +```bash +npm run init-reward-manager -- \ + --payer ~/.config/solana/id.json \ + --manager ./manager-keypair.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --reward-manager-keypair ./reward-manager.json \ + --token-account-keypair ./token-account.json +``` + +**Use custom RPC endpoint:** + +```bash +npm run init-reward-manager -- \ + --payer ~/.config/solana/id.json \ + --manager ./manager-keypair.json \ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + --cluster https://api.custom-rpc.com +``` + +#### Keypair File Format + +The script accepts keypair files in standard Solana format (array of 64 bytes): + +```json +[1,2,3,4,...,64] +``` + +Or object format: + +```json +{ + "secretKey": [1,2,3,4,...,64] +} +``` + +#### Output + +The script will: + +1. āœ… Validate all inputs +2. āœ… Check payer balance +3. āœ… Calculate rent-exempt amounts +4. āœ… Create the reward manager account +5. āœ… Create the token account +6. āœ… Initialize the reward manager +7. āœ… Display all addresses and signature +8. āœ… Save generated keypairs to files (if not provided) + +Example output: + +``` +šŸš€ Initializing Reward Manager... + +šŸ“” Connecting to https://api.devnet.solana.com +šŸ”‘ Loading keypairs... + Payer: 7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU + Manager: 9xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU + Payer balance: 2.5 SOL + Reward Manager: ABC123... (new) + Token Account: DEF456... (new) + Mint: 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM + Min Votes: 3 + +šŸ’° Calculating rent... + Reward Manager: 0.00143232 SOL + Token Account: 0.00203928 SOL + Total: 0.0034716 SOL + +šŸ”Ø Building transaction... + āœ“ Added create reward manager account instruction + āœ“ Added create token account instruction + āœ“ Added init reward manager instruction + +šŸ“¤ Sending transaction... + +āœ… Success! + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +šŸ“‹ Results: +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Signature: 5Jqw...xyz +Reward Manager: ABC123... +Token Account: DEF456... +Manager: 9xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU +Mint: 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM +Min Votes: 3 +Cluster: devnet +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +šŸ“ Derived Addresses: +Authority PDA: GHI789... + +šŸ’¾ Saved reward manager keypair to: reward-manager-1234567890.json +šŸ’¾ Saved token account keypair to: token-account-1234567890.json + +šŸ”— View transaction: https://explorer.solana.com/tx/5Jqw...xyz?cluster=devnet +``` + +## Generating Keypairs + +If you need to generate keypairs for testing: + +```bash +# Generate a keypair +solana-keygen new --outfile ./my-keypair.json --no-bip39-passphrase + +# Or use Node.js +node -e "const {Keypair} = require('@solana/web3.js'); const kp = Keypair.generate(); require('fs').writeFileSync('keypair.json', JSON.stringify(Array.from(kp.secretKey)))" +``` + +## Troubleshooting + +### "Payer has no balance" + +Fund your payer account: + +```bash +# For devnet +solana airdrop 2 --url devnet + +# For testnet +solana airdrop 2 --url testnet +``` + +### "Keypair file not found" + +Make sure the path is correct. Use absolute paths if relative paths aren't working: + +```bash +--payer /Users/yourname/.config/solana/id.json +``` + +### "Invalid keypair format" + +Ensure your keypair file is valid JSON in one of the supported formats. + +### "Account already exists" + +If you're reusing keypairs and they're already initialized, generate new ones or use different keypairs. + +## Contributing + +When adding new scripts: + +1. Add them to this directory +2. Update this README +3. Add npm scripts to `package.json` if appropriate +4. Follow the same patterns for argument parsing and error handling diff --git a/packages/spl/scripts/example-keypair.json b/packages/spl/scripts/example-keypair.json new file mode 100644 index 00000000000..d03ce5d2a75 --- /dev/null +++ b/packages/spl/scripts/example-keypair.json @@ -0,0 +1,6 @@ +[ + 174, 47, 154, 16, 202, 193, 206, 113, 199, 190, 53, 133, 169, 175, 31, 56, + 222, 53, 138, 189, 224, 216, 117, 173, 10, 149, 53, 45, 73, 251, 237, 246, 15, + 185, 186, 82, 177, 240, 148, 69, 241, 227, 167, 80, 141, 89, 240, 121, 121, + 35, 172, 247, 68, 251, 226, 218, 48, 63, 176, 109, 168, 89, 238, 135 +] diff --git a/packages/spl/scripts/initRewardManager.ts b/packages/spl/scripts/initRewardManager.ts new file mode 100644 index 00000000000..c12ac7445cf --- /dev/null +++ b/packages/spl/scripts/initRewardManager.ts @@ -0,0 +1,367 @@ +#!/usr/bin/env ts-node + +/** + * Initialize a Reward Manager program + * + * Usage: + * ts-node scripts/initRewardManager.ts \ + * --payer ./keypair.json \ + * --manager ./manager.json \ + * --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \ + * --min-votes 3 \ + * --cluster devnet + */ + +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, + clusterApiUrl +} from '@solana/web3.js' +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { RewardManagerProgram } from '../src/reward-manager/RewardManagerProgram' +import * as fs from 'fs' +import * as path from 'path' + +// Constants +const REWARD_MANAGER_SIZE = 66 // 1 (version) + 32 (token_account) + 32 (manager) + 1 (min_votes) +const TOKEN_ACCOUNT_SIZE = 165 + +interface ScriptArgs { + payer: string + manager: string + mint: string + minVotes: number + cluster: 'devnet' | 'testnet' | 'mainnet-beta' | string + rewardManagerKeypair?: string + tokenAccountKeypair?: string +} + +/** + * Load a keypair from a JSON file + */ +function loadKeypair(filepath: string): Keypair { + const resolvedPath = path.resolve(filepath) + if (!fs.existsSync(resolvedPath)) { + throw new Error(`Keypair file not found: ${resolvedPath}`) + } + + const keypairData = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8')) + + // Handle both array format and object format + let secretKey: Uint8Array + if (Array.isArray(keypairData)) { + secretKey = Uint8Array.from(keypairData) + } else if (keypairData.secretKey) { + secretKey = Uint8Array.from(keypairData.secretKey) + } else { + throw new Error(`Invalid keypair format in ${filepath}`) + } + + return Keypair.fromSecretKey(secretKey) +} + +/** + * Parse command line arguments + */ +function parseArgs(): ScriptArgs { + const args = process.argv.slice(2) + const parsed: Partial = { + cluster: 'devnet', + minVotes: 3 + } + + for (let i = 0; i < args.length; i += 2) { + const flag = args[i] + const value = args[i + 1] + + switch (flag) { + case '--payer': + case '-p': + parsed.payer = value + break + case '--manager': + case '-m': + parsed.manager = value + break + case '--mint': + parsed.mint = value + break + case '--min-votes': + parsed.minVotes = parseInt(value, 10) + break + case '--cluster': + case '-c': + parsed.cluster = value as ScriptArgs['cluster'] + break + case '--reward-manager-keypair': + parsed.rewardManagerKeypair = value + break + case '--token-account-keypair': + parsed.tokenAccountKeypair = value + break + case '--help': + case '-h': + printHelp() + process.exit(0) + default: + console.error(`Unknown flag: ${flag}`) + printHelp() + process.exit(1) + } + } + + // Validate required args + if (!parsed.payer || !parsed.manager || !parsed.mint) { + console.error('Missing required arguments\n') + printHelp() + process.exit(1) + } + + return parsed as ScriptArgs +} + +/** + * Print help message + */ +function printHelp() { + console.log(` +Initialize a Reward Manager program + +USAGE: + ts-node scripts/initRewardManager.ts [OPTIONS] + +REQUIRED OPTIONS: + --payer, -p Path to payer keypair JSON file + --manager, -m Path to manager keypair JSON file + --mint
Mint address for reward token + +OPTIONAL: + --min-votes Minimum votes required (default: 3) + --cluster, -c Cluster to use: devnet, testnet, mainnet-beta, or custom URL + (default: devnet) + --reward-manager-keypair Use existing reward manager keypair (generates new if omitted) + --token-account-keypair Use existing token account keypair (generates new if omitted) + --help, -h Show this help message + +EXAMPLES: + # Initialize with minimum config + ts-node scripts/initRewardManager.ts \\ + --payer ~/.config/solana/id.json \\ + --manager ./manager-keypair.json \\ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM + + # Initialize with custom cluster and votes + ts-node scripts/initRewardManager.ts \\ + --payer ~/.config/solana/id.json \\ + --manager ./manager-keypair.json \\ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \\ + --min-votes 5 \\ + --cluster mainnet-beta + + # Use existing reward manager and token account keypairs + ts-node scripts/initRewardManager.ts \\ + --payer ~/.config/solana/id.json \\ + --manager ./manager-keypair.json \\ + --mint 9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM \\ + --reward-manager-keypair ./reward-manager.json \\ + --token-account-keypair ./token-account.json + `) +} + +/** + * Main script + */ +async function main() { + console.log('šŸš€ Initializing Reward Manager...\n') + + // Parse arguments + const args = parseArgs() + + // Setup connection + const rpcUrl = ['devnet', 'testnet', 'mainnet-beta'].includes(args.cluster) + ? clusterApiUrl(args.cluster as 'devnet' | 'testnet' | 'mainnet-beta') + : args.cluster + + console.log(`šŸ“” Connecting to ${rpcUrl}`) + const connection = new Connection(rpcUrl, 'confirmed') + + // Load keypairs + console.log('šŸ”‘ Loading keypairs...') + const payer = loadKeypair(args.payer) + const manager = loadKeypair(args.manager) + console.log(` Payer: ${payer.publicKey.toBase58()}`) + console.log(` Manager: ${manager.publicKey.toBase58()}`) + + // Check payer balance + const payerBalance = await connection.getBalance(payer.publicKey) + console.log(` Payer balance: ${payerBalance / 1e9} SOL`) + if (payerBalance === 0) { + throw new Error('Payer has no balance. Please fund the account first.') + } + + // Generate or load reward manager and token account keypairs + const rewardManager = args.rewardManagerKeypair + ? loadKeypair(args.rewardManagerKeypair) + : Keypair.generate() + + const tokenAccount = args.tokenAccountKeypair + ? loadKeypair(args.tokenAccountKeypair) + : Keypair.generate() + + console.log( + ` Reward Manager: ${rewardManager.publicKey.toBase58()} ${args.rewardManagerKeypair ? '(existing)' : '(new)'}` + ) + console.log( + ` Token Account: ${tokenAccount.publicKey.toBase58()} ${args.tokenAccountKeypair ? '(existing)' : '(new)'}` + ) + + // Parse mint + const mint = new PublicKey(args.mint) + console.log(` Mint: ${mint.toBase58()}`) + console.log(` Min Votes: ${args.minVotes}\n`) + + // Get rent-exempt minimums + console.log('šŸ’° Calculating rent...') + const rewardManagerRent = + await connection.getMinimumBalanceForRentExemption(REWARD_MANAGER_SIZE) + const tokenAccountRent = + await connection.getMinimumBalanceForRentExemption(TOKEN_ACCOUNT_SIZE) + const totalRent = rewardManagerRent + tokenAccountRent + console.log(` Reward Manager: ${rewardManagerRent / 1e9} SOL`) + console.log(` Token Account: ${tokenAccountRent / 1e9} SOL`) + console.log(` Total: ${totalRent / 1e9} SOL\n`) + + // Build transaction + console.log('šŸ”Ø Building transaction...') + const transaction = new Transaction() + + // 1. Create reward manager account + transaction.add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: rewardManager.publicKey, + lamports: rewardManagerRent, + space: REWARD_MANAGER_SIZE, + programId: RewardManagerProgram.programId + }) + ) + console.log(' āœ“ Added create reward manager account instruction') + + // 2. Create token account + transaction.add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: tokenAccount.publicKey, + lamports: tokenAccountRent, + space: TOKEN_ACCOUNT_SIZE, + programId: TOKEN_PROGRAM_ID + }) + ) + console.log(' āœ“ Added create token account instruction') + + // 3. Initialize reward manager + const initInstruction = RewardManagerProgram.createInitInstruction({ + rewardManager: rewardManager.publicKey, + tokenAccount: tokenAccount.publicKey, + mint, + manager: manager.publicKey, + minVotes: args.minVotes + }) + + // Debug: Show the derived authority and all accounts + const derivedAuthority = RewardManagerProgram.deriveAuthority({ + programId: RewardManagerProgram.programId, + rewardManagerState: rewardManager.publicKey + }) + console.log(`\n šŸ” Debug Information:`) + console.log(` Program ID: ${RewardManagerProgram.programId.toBase58()}`) + console.log(` Reward Manager: ${rewardManager.publicKey.toBase58()}`) + console.log(` Token Account: ${tokenAccount.publicKey.toBase58()}`) + console.log(` Mint: ${mint.toBase58()}`) + console.log(` Manager: ${manager.publicKey.toBase58()}`) + console.log(` Derived Authority: ${derivedAuthority.toBase58()}`) + console.log(` Min Votes: ${args.minVotes}`) + + console.log(`\n šŸ“ Init Instruction Accounts (in order):`) + initInstruction.keys.forEach((key, idx) => { + console.log( + ` ${idx}: ${key.pubkey.toBase58()} (${key.isWritable ? 'writable' : 'readonly'}${key.isSigner ? ', signer' : ''})` + ) + }) + console.log( + ` šŸ“¦ Instruction Data (hex): ${initInstruction.data.toString('hex')}` + ) + + transaction.add(initInstruction) + console.log('\n āœ“ Added init reward manager instruction\n') + + // Send and confirm transaction + console.log('šŸ“¤ Sending transaction...') + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [payer, rewardManager, tokenAccount], + { + commitment: 'confirmed', + skipPreflight: false + } + ) + + console.log('\nāœ… Success!\n') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log('šŸ“‹ Results:') + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + console.log(`Signature: ${signature}`) + console.log(`Reward Manager: ${rewardManager.publicKey.toBase58()}`) + console.log(`Token Account: ${tokenAccount.publicKey.toBase58()}`) + console.log(`Manager: ${manager.publicKey.toBase58()}`) + console.log(`Mint: ${mint.toBase58()}`) + console.log(`Min Votes: ${args.minVotes}`) + console.log(`Cluster: ${args.cluster}`) + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n') + + // Derive and display authority + const authority = RewardManagerProgram.deriveAuthority({ + programId: RewardManagerProgram.programId, + rewardManagerState: rewardManager.publicKey + }) + console.log('šŸ“ Derived Addresses:') + console.log(`Authority PDA: ${authority.toBase58()}\n`) + + // Save keypairs if they were generated + if (!args.rewardManagerKeypair) { + const filename = `reward-manager-${Date.now()}.json` + fs.writeFileSync( + filename, + JSON.stringify(Array.from(rewardManager.secretKey)) + ) + console.log(`šŸ’¾ Saved reward manager keypair to: ${filename}`) + } + + if (!args.tokenAccountKeypair) { + const filename = `token-account-${Date.now()}.json` + fs.writeFileSync( + filename, + JSON.stringify(Array.from(tokenAccount.secretKey)) + ) + console.log(`šŸ’¾ Saved token account keypair to: ${filename}`) + } + + console.log( + `\nšŸ”— View transaction: https://explorer.solana.com/tx/${signature}?cluster=${args.cluster}` + ) +} + +// Run the script +main().catch((error) => { + console.error('\nāŒ Error:', error.message) + if (error.logs) { + console.error('\nšŸ“‹ Program logs:') + error.logs.forEach((log: string) => console.error(` ${log}`)) + } + process.exit(1) +}) diff --git a/packages/spl/scripts/tsconfig.json b/packages/spl/scripts/tsconfig.json new file mode 100644 index 00000000000..8fed5bdb952 --- /dev/null +++ b/packages/spl/scripts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "Node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["*.ts"], + "ts-node": { + "compilerOptions": { + "module": "CommonJS" + } + } +} diff --git a/packages/spl/src/reward-manager/INIT_EXAMPLE.md b/packages/spl/src/reward-manager/INIT_EXAMPLE.md new file mode 100644 index 00000000000..b80ef6694eb --- /dev/null +++ b/packages/spl/src/reward-manager/INIT_EXAMPLE.md @@ -0,0 +1,137 @@ +# Init Instruction Example + +> **Quick Start**: For a ready-to-run script, see [`../../QUICKSTART.md`](../../QUICKSTART.md) or [`../../scripts/initRewardManager.ts`](../../scripts/initRewardManager.ts) + +Here's how to use the newly implemented `Init` instruction to initialize a Reward Manager program: + +```typescript +import { Connection, Keypair, PublicKey, SystemProgram } from '@solana/web3.js' +import { TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { RewardManagerProgram } from '@audius/spl' + +async function initializeRewardManager() { + const connection = new Connection('https://api.devnet.solana.com') + const payer = Keypair.fromSecretKey(/* your keypair */) + const manager = Keypair.fromSecretKey(/* manager keypair */) + + // Create new accounts + const rewardManager = Keypair.generate() + const tokenAccount = Keypair.generate() + + // The mint for the rewards token (e.g., AUDIO token) + const mint = new PublicKey('9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM') + + // Minimum number of discovery node attestations required + const minVotes = 3 + + // Get rent-exempt minimums + const rewardManagerRent = await connection.getMinimumBalanceForRentExemption( + /* RewardManager size */ 74 + ) + const tokenAccountRent = await connection.getMinimumBalanceForRentExemption( + 165 // Token Account size + ) + + // Create the transaction + const transaction = new Transaction() + + // 1. Create the reward manager account + transaction.add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: rewardManager.publicKey, + lamports: rewardManagerRent, + space: 74, // RewardManager::LEN + programId: RewardManagerProgram.programId + }) + ) + + // 2. Create the token account + transaction.add( + SystemProgram.createAccount({ + fromPubkey: payer.publicKey, + newAccountPubkey: tokenAccount.publicKey, + lamports: tokenAccountRent, + space: 165, // Token Account size + programId: TOKEN_PROGRAM_ID + }) + ) + + // 3. Initialize the reward manager + transaction.add( + RewardManagerProgram.createInitInstruction({ + rewardManager: rewardManager.publicKey, + tokenAccount: tokenAccount.publicKey, + mint, + manager: manager.publicKey, + minVotes + }) + ) + + // Send and confirm + const signature = await connection.sendTransaction( + transaction, + [payer, rewardManager, tokenAccount], + { skipPreflight: false } + ) + + console.log('Reward Manager initialized!') + console.log('Signature:', signature) + console.log('Reward Manager:', rewardManager.publicKey.toBase58()) + console.log('Token Account:', tokenAccount.publicKey.toBase58()) + + // Decode the instruction + const instructions = transaction.instructions + const initInstruction = instructions[2] + const decoded = RewardManagerProgram.decodeInstruction(initInstruction) + + if (RewardManagerProgram.isInitInstruction(decoded)) { + console.log('Decoded Init instruction:') + console.log(' Min votes:', decoded.data.minVotes) + console.log( + ' Reward Manager:', + decoded.keys.rewardManager.pubkey.toBase58() + ) + console.log(' Manager:', decoded.keys.manager.pubkey.toBase58()) + } +} + +// Run the example +initializeRewardManager().catch(console.error) +``` + +## What the Init Instruction Does + +The `Init` instruction: + +1. **Initializes the Reward Manager State** - Sets up the main program state with: + + - Token account for holding rewards + - Manager authority who can create/delete senders + - Minimum number of validator votes required for disbursement + +2. **Initializes the Token Account** - Sets up an SPL token account to hold the reward tokens + +3. **Derives the Authority PDA** - Automatically calculates the program-derived address that will own sender accounts and sign token transfers + +## Key Parameters + +- **rewardManager**: The account to store the program state +- **tokenAccount**: The SPL token account to hold reward tokens +- **mint**: The mint of the reward token (e.g., AUDIO) +- **manager**: The admin who can create/delete senders +- **minVotes**: Number of discovery node attestations required (typically 3) + +## Comparison with Rust CLI + +This matches the Rust CLI `init` command: + +```bash +reward-manager-cli init \ + --token-mint \ + --min-votes 3 \ + --keypair \ + --token-keypair +``` + +See: `solana-programs/reward-manager/cli/src/main.rs` lines 69-134 diff --git a/packages/spl/src/reward-manager/RewardManagerProgram.ts b/packages/spl/src/reward-manager/RewardManagerProgram.ts index 04f47a83453..faf2272d7f0 100644 --- a/packages/spl/src/reward-manager/RewardManagerProgram.ts +++ b/packages/spl/src/reward-manager/RewardManagerProgram.ts @@ -25,10 +25,13 @@ import { DecodedCreateSenderPublicInstruction, DecodedDeleteSenderPublicInstruction, DecodedEvaluateAttestationsInstruction, + DecodedInitRewardManagerInstruction, DecodedRewardManagerInstruction, DecodedSubmitAttestationsInstruction, EvaluateAttestationsInstructionData, EvaluateRewardAttestationsParams, + InitRewardManagerInstructionData, + InitRewardManagerParams, RewardManagerStateData, SubmitAttestationInstructionData, SubmitRewardAttestationParams, @@ -51,6 +54,10 @@ export class RewardManagerProgram { ) public static readonly layouts = { + initRewardManagerInstructionData: struct([ + u8('instruction'), + u8('minVotes') + ]), createSenderInstructionData: struct([ u8('instruction'), ethAddress('senderEthAddress'), @@ -101,6 +108,76 @@ export class RewardManagerProgram { ]) } + public static createInitInstruction({ + rewardManager, + tokenAccount, + mint, + manager, + minVotes, + rewardManagerProgramId = RewardManagerProgram.programId + }: InitRewardManagerParams) { + const data = Buffer.alloc( + RewardManagerProgram.layouts.initRewardManagerInstructionData.span + ) + RewardManagerProgram.layouts.initRewardManagerInstructionData.encode( + { + instruction: RewardManagerInstruction.Init, + minVotes + }, + data + ) + + const authority = RewardManagerProgram.deriveAuthority({ + programId: rewardManagerProgramId, + rewardManagerState: rewardManager + }) + + const keys: AccountMeta[] = [ + { pubkey: rewardManager, isSigner: false, isWritable: true }, + { pubkey: tokenAccount, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: manager, isSigner: false, isWritable: false }, + { pubkey: authority, isSigner: false, isWritable: false }, + { pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: SYSVAR_RENT_PUBKEY, isSigner: false, isWritable: false } + ] + return new TransactionInstruction({ + programId: rewardManagerProgramId, + keys, + data + }) + } + + public static decodeInitInstruction({ + programId, + keys: [ + rewardManager, + tokenAccount, + mint, + manager, + authority, + tokenProgram, + rent + ], + data + }: TransactionInstruction): DecodedInitRewardManagerInstruction { + return { + programId, + keys: { + rewardManager, + tokenAccount, + mint, + manager, + authority, + tokenProgram, + rent + }, + data: RewardManagerProgram.layouts.initRewardManagerInstructionData.decode( + data + ) + } + } + public static createSenderInstruction({ senderEthAddress, operatorEthAddress, @@ -434,6 +511,7 @@ export class RewardManagerProgram { ): DecodedRewardManagerInstruction { switch (instruction.data[0]) { case RewardManagerInstruction.Init: + return RewardManagerProgram.decodeInitInstruction(instruction) case RewardManagerInstruction.ChangeManagerAccount: throw new Error('Not Implemented') case RewardManagerInstruction.CreateSender: @@ -461,6 +539,12 @@ export class RewardManagerProgram { } } + public static isInitInstruction( + decoded: DecodedRewardManagerInstruction + ): decoded is DecodedInitRewardManagerInstruction { + return decoded.data.instruction === RewardManagerInstruction.Init + } + public static isCreateSenderInstruction( decoded: DecodedRewardManagerInstruction ): decoded is DecodedCreateSenderInstruction { diff --git a/packages/spl/src/reward-manager/types.ts b/packages/spl/src/reward-manager/types.ts index 99c088ba2d5..1061776b5e7 100644 --- a/packages/spl/src/reward-manager/types.ts +++ b/packages/spl/src/reward-manager/types.ts @@ -2,6 +2,49 @@ import { AccountMeta, PublicKey } from '@solana/web3.js' import { RewardManagerInstruction } from './constants' +export type InitRewardManagerParams = { + /** The account to initialize as the reward manager state. */ + rewardManager: PublicKey + /** The token account to hold rewards. */ + tokenAccount: PublicKey + /** The mint for the token account. */ + mint: PublicKey + /** The admin account that will manage the reward manager. */ + manager: PublicKey + /** Minimum number of votes required to disburse rewards. */ + minVotes: number + /** The programId of the Reward Manager Program. */ + rewardManagerProgramId?: PublicKey +} + +export type InitRewardManagerInstructionData = { + /** The instruction identifier. */ + instruction: RewardManagerInstruction + /** Minimum number of votes required to disburse rewards. */ + minVotes: number +} + +export type DecodedInitRewardManagerInstruction = { + programId: PublicKey + keys: { + /** The account to initialize as the reward manager state. */ + rewardManager: AccountMeta + /** The token account to hold rewards. */ + tokenAccount: AccountMeta + /** The mint for the token account. */ + mint: AccountMeta + /** The admin account that will manage the reward manager. */ + manager: AccountMeta + /** The reward manager authority PDA. */ + authority: AccountMeta + /** The SPL Token program. */ + tokenProgram: AccountMeta + /** The rent sysvar account. */ + rent: AccountMeta + } + data: InitRewardManagerInstructionData +} + export type CreateRewardSenderParams = { /** The node's Ethereum wallet address. */ senderEthAddress: string @@ -259,6 +302,7 @@ export type DecodedEvaluateAttestationsInstruction = { } export type DecodedRewardManagerInstruction = + | DecodedInitRewardManagerInstruction | DecodedCreateSenderInstruction | DecodedCreateSenderPublicInstruction | DecodedDeleteSenderPublicInstruction