diff --git a/.eslintrc.yml b/.eslintrc.yml index 9b20e5e..07becc7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -19,6 +19,7 @@ parserOptions: sourceType: module project: ./tsconfig.json rules: + "@typescript-eslint/explicit-function-return-type": "off" "@typescript-eslint/strict-boolean-expressions": - error - allowNullableObject: true diff --git a/packages/fund-sender/client/index.ts b/packages/fund-sender/client/index.ts index 364b6c5..2846d3f 100644 --- a/packages/fund-sender/client/index.ts +++ b/packages/fund-sender/client/index.ts @@ -6,22 +6,35 @@ import { Connection, AccountMeta, AddressLookupTableProgram, - TransactionMessage, TransactionInstruction, VersionedTransaction, AddressLookupTableAccount + TransactionMessage, + TransactionInstruction, + VersionedTransaction, } from "@solana/web3.js"; import BN from "bn.js"; import { FundSender } from "../../types/fund_sender"; import IDL from "../../idl/fund_sender.json"; import base58 from "bs58"; -import {AssetProof, getAsset, getAssetProof, getAssetsByOwner} from "./readAPI"; +import { + AssetProof, + getAsset, + getAssetProof, + getAssetsByOwner, +} from "./readAPI"; export const PROGRAM_ID = new PublicKey( - "sfsH2CVS2SaXwnrGwgTVrG7ytZAxSCsTnW82BvjWTGz" + "sfsH2CVS2SaXwnrGwgTVrG7ytZAxSCsTnW82BvjWTGz" ); -const SPL_NOOP_PROGRAM_ID = new PublicKey("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); +const SPL_NOOP_PROGRAM_ID = new PublicKey( + "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV" +); // Needed to set up the ALT only -const BUBBLEGUM_PROGRAM_ID = new PublicKey("BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY"); -const SPL_ACCOUNT_COMPRESSION_PROGRAM_ID = new PublicKey("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK") +const BUBBLEGUM_PROGRAM_ID = new PublicKey( + "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" +); +const SPL_ACCOUNT_COMPRESSION_PROGRAM_ID = new PublicKey( + "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" +); /** * Sets up an anchor provider read from the environment variable. @@ -64,8 +77,8 @@ export const confirm = (connection: Connection) => async (txSig: string) => { */ const getInputAccountForState = (stateAddress: PublicKey): PublicKey => { const [inputAccount] = PublicKey.findProgramAddressSync( - [Buffer.from("input_account"), stateAddress.toBuffer()], - PROGRAM_ID + [Buffer.from("input_account"), stateAddress.toBuffer()], + PROGRAM_ID ); return inputAccount; @@ -77,8 +90,8 @@ const decodeBase58 = (base58Input: string): number[] => { }; export const mapProof = (assetProof: AssetProof): AccountMeta[] => { - if (!assetProof.proof || assetProof.proof.length === 0) { - throw new Error('Proof is empty'); + if (assetProof.proof === undefined || assetProof.proof.length === 0) { + throw new Error("Proof is empty"); } return assetProof.proof.map((node) => ({ pubkey: new PublicKey(node), @@ -88,17 +101,19 @@ export const mapProof = (assetProof: AssetProof): AccountMeta[] => { }; async function createV0Tx( - connection: Connection, - txInstructions: TransactionInstruction[], - payer: PublicKey, - addressLookupTableAddress?: PublicKey + connection: Connection, + txInstructions: TransactionInstruction[], + payer: PublicKey, + addressLookupTableAddress?: PublicKey ) { - const addressLookupTable = addressLookupTableAddress ? (await connection.getAddressLookupTable(addressLookupTableAddress)).value : undefined; - const latestBlockhash = await connection.getLatestBlockhash('confirmed'); + const addressLookupTable = addressLookupTableAddress + ? (await connection.getAddressLookupTable(addressLookupTableAddress)).value + : undefined; + const latestBlockhash = await connection.getLatestBlockhash("confirmed"); const messageV0 = new TransactionMessage({ payerKey: payer, recentBlockhash: latestBlockhash.blockhash, - instructions: txInstructions + instructions: txInstructions, }).compileToV0Message(addressLookupTable ? [addressLookupTable] : []); return new VersionedTransaction(messageV0); } @@ -120,8 +135,8 @@ export class FundSenderClient { readonly program: Program; constructor( - readonly provider: AnchorProvider, - readonly stateAddress: PublicKey + readonly provider: AnchorProvider, + readonly stateAddress: PublicKey ) { this.program = new Program(IDL as FundSender, provider); } @@ -152,16 +167,16 @@ export class FundSenderClient { * */ public static getStateAddressFromSunriseAddress( - sunriseState: PublicKey, - destinationName: string + sunriseState: PublicKey, + destinationName: string ): PublicKey { const [state] = PublicKey.findProgramAddressSync( - [ - Buffer.from("state"), - Buffer.from(destinationName), - sunriseState.toBuffer(), - ], - PROGRAM_ID + [ + Buffer.from("state"), + Buffer.from(destinationName), + sunriseState.toBuffer(), + ], + PROGRAM_ID ); return state; @@ -188,12 +203,12 @@ export class FundSenderClient { * */ public static async fetch( - stateAddress: PublicKey, - provider?: AnchorProvider + stateAddress: PublicKey, + provider?: AnchorProvider ): Promise { const client = new FundSenderClient( - provider ?? setUpAnchor(), - stateAddress + provider ?? setUpAnchor(), + stateAddress ); await client.init(); @@ -217,17 +232,17 @@ export class FundSenderClient { * @returns Initialised fund sender client */ public static async register( - sunriseState: PublicKey, - updateAuthority: PublicKey, - destinationName: string, - destinationAccount: PublicKey, - certificateVault: PublicKey, - spendThreshold: BN + sunriseState: PublicKey, + updateAuthority: PublicKey, + destinationName: string, + destinationAccount: PublicKey, + certificateVault: PublicKey, + spendThreshold: BN ): Promise { // find state address const stateAddress = FundSenderClient.getStateAddressFromSunriseAddress( - sunriseState, - destinationName + sunriseState, + destinationName ); console.log("state address", stateAddress.toBase58()); const inputAccount = getInputAccountForState(stateAddress); @@ -252,18 +267,18 @@ export class FundSenderClient { }; console.log("Registering state"); await client.program.methods - .registerState(sunriseState, args) - .accounts(accounts) - .rpc() - .then(() => { - confirm(client.provider.connection); - }) - // Temporary - use this to get insight into failed transactions - // Can be removed after everything works, and re-added to debug as needed. - .catch((e) => { - console.log(e.logs); - throw e; - }); + .registerState(sunriseState, args) + .accounts(accounts) + .rpc() + .then(() => { + confirm(client.provider.connection); + }) + // Temporary - use this to get insight into failed transactions + // Can be removed after everything works, and re-added to debug as needed. + .catch((e) => { + console.log(e.logs); + throw e; + }); // now that the state is registered on chain, we can hydrate the client instance with its data await client.init(); @@ -281,8 +296,8 @@ export class FundSenderClient { * */ public async updateDestinationAccount( - destinationAccount: PublicKey, - spendThreshold: BN + destinationAccount: PublicKey, + spendThreshold: BN ): Promise { if (!this.config) { throw new Error("Client not initialized"); @@ -300,12 +315,12 @@ export class FundSenderClient { spendThreshold, }; await this.program.methods - .updateState(args) - .accounts(accounts) - .rpc() - .then(() => { - confirm(this.provider.connection); - }); + .updateState(args) + .accounts(accounts) + .rpc() + .then(() => { + confirm(this.provider.connection); + }); await this.init(); @@ -321,7 +336,7 @@ export class FundSenderClient { * */ public async updateCertificateVault( - certificateVault: PublicKey + certificateVault: PublicKey ): Promise { if (!this.config) { throw new Error("Client not initialized"); @@ -340,12 +355,12 @@ export class FundSenderClient { spendThreshold: this.config.spendThreshold, }; await this.program.methods - .updateState(args) - .accounts(accounts) - .rpc() - .then(() => { - confirm(this.provider.connection); - }); + .updateState(args) + .accounts(accounts) + .rpc() + .then(() => { + confirm(this.provider.connection); + }); await this.init(); @@ -361,7 +376,7 @@ export class FundSenderClient { * */ public async updateUpdateAuthority( - updateAuthority: PublicKey // the public key of the new update authority + updateAuthority: PublicKey // the public key of the new update authority ): Promise { // Check if the client is initialized, config should be avaliable in such case if (!this.config) { @@ -381,12 +396,12 @@ export class FundSenderClient { }; // call the updateState method from the program with the new update authority address await this.program.methods - .updateState(args) - .accounts(accounts) - .rpc() - .then(() => { - confirm(this.provider.connection); - }); + .updateState(args) + .accounts(accounts) + .rpc() + .then(() => { + confirm(this.provider.connection); + }); // repopulating the config with new data await this.init(); @@ -407,33 +422,32 @@ export class FundSenderClient { } await this.program.methods - .sendFund(amount) - .accounts({ - payer: this.provider.publicKey, - state: this.stateAddress, - destinationAccount: this.config.destinationAccount, - }) - .rpc() - .then(confirm(this.provider.connection)); + .sendFund(amount) + .accounts({ + payer: this.provider.publicKey, + state: this.stateAddress, + destinationAccount: this.config.destinationAccount, + }) + .rpc() + .then(confirm(this.provider.connection)); return this; } - public async sendFromState( - ): Promise { + public async sendFromState(): Promise { if (!this.config) { throw new Error("Client not initialized"); } await this.program.methods - .sendFromState() - .accounts({ - state: this.stateAddress, - }) - .rpc() - .then(confirm(this.provider.connection)); - - return this + .sendFromState() + .accounts({ + state: this.stateAddress, + }) + .rpc() + .then(confirm(this.provider.connection)); + + return this; } /** @@ -444,24 +458,24 @@ export class FundSenderClient { * */ public async storeCertificates( - inputTokenAccount: PublicKey, - certificateMint: PublicKey + inputTokenAccount: PublicKey, + certificateMint: PublicKey ): Promise { if (!this.config) { throw new Error("Client not initialized"); } await this.program.methods - .storeCertificates() - .accounts({ - payer: this.provider.publicKey, - state: this.stateAddress, - certificateMint, - inputTokenAccount, - certificateVault: this.config.certificateVault, - }) - .rpc() - .then(confirm(this.provider.connection)); + .storeCertificates() + .accounts({ + payer: this.provider.publicKey, + state: this.stateAddress, + certificateMint, + inputTokenAccount, + certificateVault: this.config.certificateVault, + }) + .rpc() + .then(confirm(this.provider.connection)); return this; } @@ -469,16 +483,21 @@ export class FundSenderClient { public async getCNFTCertificates() { if (!this.config) throw new Error("Client not initialized"); - const { items: assets } = await getAssetsByOwner(this.getInputAccount().toBase58()); + const { items: assets } = await getAssetsByOwner( + this.getInputAccount().toBase58() + ); - console.log("Found assets: ", assets.map((asset) => asset.content.metadata.name)); + console.log( + "Found assets: ", + assets.map((asset) => asset.content.metadata.name) + ); return assets; } public async storeCNFTCertificate( - assetId: string, - addressLookupTable: PublicKey + assetId: string, + addressLookupTable: PublicKey ): Promise { if (!this.config) throw new Error("Client not initialized"); @@ -493,19 +512,23 @@ export class FundSenderClient { const index = asset.compression.leaf_id; const ix = await this.program.methods - .storeCnftCertificate(root, dataHash, creatorHash, nonce, index) - .accounts({ - payer: this.provider.publicKey, - state: this.stateAddress, - certificateVault: this.config.certificateVault, - merkleTree: new PublicKey(proof.tree_id), - logWrapper: SPL_NOOP_PROGRAM_ID - }) - .remainingAccounts(proofPathAsAccounts) - .instruction(); - - const tx = await createV0Tx(this.provider.connection, [ix], this.provider.publicKey, addressLookupTable) - .then(tx => this.provider.wallet.signTransaction(tx)); + .storeCnftCertificate(root, dataHash, creatorHash, nonce, index) + .accounts({ + payer: this.provider.publicKey, + state: this.stateAddress, + certificateVault: this.config.certificateVault, + merkleTree: new PublicKey(proof.tree_id), + logWrapper: SPL_NOOP_PROGRAM_ID, + }) + .remainingAccounts(proofPathAsAccounts) + .instruction(); + + const tx = await createV0Tx( + this.provider.connection, + [ix], + this.provider.publicKey, + addressLookupTable + ).then(async (tx) => this.provider.wallet.signTransaction(tx)); const txHash = await this.provider.sendAndConfirm(tx); console.log("Stored CNFT certificate", txHash); @@ -520,34 +543,41 @@ export class FundSenderClient { if (!this.config) throw new Error("Client not initialized"); const slot = await this.provider.connection.getSlot(); const addresses = [ - this.stateAddress, - this.getInputAccount(), - this.config.certificateVault, - SPL_NOOP_PROGRAM_ID, - SystemProgram.programId, - BUBBLEGUM_PROGRAM_ID, - SPL_ACCOUNT_COMPRESSION_PROGRAM_ID + this.stateAddress, + this.getInputAccount(), + this.config.certificateVault, + SPL_NOOP_PROGRAM_ID, + SystemProgram.programId, + BUBBLEGUM_PROGRAM_ID, + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, ]; const [lookupTableIx, lookupTableAddress] = - AddressLookupTableProgram.createLookupTable({ - authority: this.provider.publicKey, - payer: this.provider.publicKey, - recentSlot: slot, - }); + AddressLookupTableProgram.createLookupTable({ + authority: this.provider.publicKey, + payer: this.provider.publicKey, + recentSlot: slot, + }); const addAddressesIx = AddressLookupTableProgram.extendLookupTable({ payer: this.provider.publicKey, authority: this.provider.publicKey, lookupTable: lookupTableAddress, - addresses + addresses, }); - const tx = await createV0Tx(this.provider.connection, [lookupTableIx, addAddressesIx], this.provider.publicKey) - .then(tx => this.provider.wallet.signTransaction(tx)); + const tx = await createV0Tx( + this.provider.connection, + [lookupTableIx, addAddressesIx], + this.provider.publicKey + ).then(async (tx) => this.provider.wallet.signTransaction(tx)); const txHash = await this.provider.sendAndConfirm(tx); - console.log("Created AddressLookupTable for CNFT transfer", lookupTableAddress.toBase58(), txHash); + console.log( + "Created AddressLookupTable for CNFT transfer", + lookupTableAddress.toBase58(), + txHash + ); return lookupTableAddress; } diff --git a/packages/fund-sender/client/readAPI.ts b/packages/fund-sender/client/readAPI.ts index 708b2ee..4368c14 100644 --- a/packages/fund-sender/client/readAPI.ts +++ b/packages/fund-sender/client/readAPI.ts @@ -4,103 +4,122 @@ // you might want to change that to your custom RPC import * as crypto from "node:crypto"; +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion const RPC_PATH = process.env.ANCHOR_PROVIDER_URL!; if (!RPC_PATH) { - throw new Error('ANCHOR_PROVIDER_URL is not defined'); + throw new Error("ANCHOR_PROVIDER_URL is not defined"); } -type JSONRPCResponse = { - jsonrpc: string; - id: string; - result: any; - error: { - code: number; - message: string; - }; -}; +interface JSONRPCResponse { + jsonrpc: string; + id: string; + result: any; + error?: { + code: number; + message: string; + }; +} -export type Asset = { - id: string, - compression: { - data_hash: string; - creator_hash: string; - leaf_id: number; +export interface Asset { + id: string; + compression: { + data_hash: string; + creator_hash: string; + leaf_id: number; + }; + content: { + json_uri: string; + files: Array<{ + uri: string; + mime: string; + }>; + links: { image: string }; + metadata: { + attributes: Array<{ + value: string; + trait_type: string; + }>; + description: string; + name: string; + symbol: string; + token_standard: string; }; - content: { - json_uri: string, - files: { - uri: string, - mime: string - }[], - links: { image: string } - metadata: { - attributes: { - value: string, - trait_type: string, - }[], - description: string, - name: string, - symbol: string, - token_standard: string, - } - }, - authorities: { - address: string, - scopes: string[] - }[] -}; + }; + authorities: Array<{ + address: string; + scopes: string[]; + }>; +} -export type AssetResponse = { - total: number, - limit: number, - cursor: string, - items: Asset[] +export interface AssetResponse { + total: number; + limit: number; + cursor: string; + items: Asset[]; } -export type AssetProof = { - proof: string[], - tree_id: string, - root: string +export interface AssetProof { + proof: string[]; + tree_id: string; + root: string; } -const fetchJsonRpc = async (method: string, params: any, rpcUrl = RPC_PATH): Promise => { - const response = await fetch(rpcUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - jsonrpc: '2.0', - method, - id: crypto.randomUUID(), - params, - }), - }); +const fetchJsonRpc = async ( + method: string, + params: any, + rpcUrl = RPC_PATH +): Promise => { + const response = await fetch(rpcUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method, + id: crypto.randomUUID(), + params, + }), + }); - // Handle HTTP errors - if (!response.ok) throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`); + // Handle HTTP errors + if (!response.ok) + throw new Error( + `HTTP error! Status: ${response.status} ${response.statusText}` + ); - const data = await response.json() as JSONRPCResponse; + const data = (await response.json()) as JSONRPCResponse; - // Handle JSON-RPC errors - if (data.error) { - if (data.error.code === -32601) { - throw new Error(`RPC method not found: ${method} - Ensure you are using a DAS-enabled Solana RPC`); - } - throw new Error(`RPC error: ${data.error.message}`); + // Handle JSON-RPC errors + if (data.error) { + if (data.error.code === -32601) { + throw new Error( + `RPC method not found: ${method} - Ensure you are using a DAS-enabled Solana RPC` + ); } + throw new Error(`RPC error: ${data.error.message}`); + } - return data.result; -} + return data.result; +}; -export async function getAsset(assetId: string, rpcUrl = RPC_PATH): Promise { - return fetchJsonRpc('getAsset', { id: assetId }, rpcUrl); +export async function getAsset( + assetId: string, + rpcUrl = RPC_PATH +): Promise { + return fetchJsonRpc("getAsset", { id: assetId }, rpcUrl); } -export async function getAssetProof(assetId: string, rpcUrl = RPC_PATH): Promise { - return fetchJsonRpc('getAssetProof', { id: assetId }, rpcUrl); +export async function getAssetProof( + assetId: string, + rpcUrl = RPC_PATH +): Promise { + return fetchJsonRpc("getAssetProof", { id: assetId }, rpcUrl); } -export async function getAssetsByOwner(owner: string, rpcUrl = RPC_PATH): Promise { - return fetchJsonRpc('getAssetsByOwner', { ownerAddress: owner }, rpcUrl); -} \ No newline at end of file +export async function getAssetsByOwner( + owner: string, + rpcUrl = RPC_PATH +): Promise { + return fetchJsonRpc("getAssetsByOwner", { ownerAddress: owner }, rpcUrl); +} diff --git a/packages/fund-sender/scripts/getStateFromAddress.ts b/packages/fund-sender/scripts/getStateFromAddress.ts index 27421a8..38859a0 100644 --- a/packages/fund-sender/scripts/getStateFromAddress.ts +++ b/packages/fund-sender/scripts/getStateFromAddress.ts @@ -1,6 +1,6 @@ import { FundSenderClient } from "../client"; import { logBalance } from "./lib/util"; -import {PublicKey} from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; // USAGE: yarn ts-node packages/fund-sender/getStateFromAddress.ts stateAddress const stateAddress = process.argv[2]; diff --git a/packages/fund-sender/scripts/sendFromState.ts b/packages/fund-sender/scripts/sendFromState.ts index cf6eb6c..1589807 100644 --- a/packages/fund-sender/scripts/sendFromState.ts +++ b/packages/fund-sender/scripts/sendFromState.ts @@ -13,15 +13,17 @@ const destinationName = process.argv[2]; (async () => { const stateAddress = FundSenderClient.getStateAddressFromSunriseAddress( - sunriseStateAddress, - destinationName + sunriseStateAddress, + destinationName ); const client = await FundSenderClient.fetch(stateAddress); console.log("state address", stateAddress.toBase58()); console.log("input address", client.getInputAccount().toBase58()); - const stateBalance = await client.provider.connection.getBalance(stateAddress); + const stateBalance = await client.provider.connection.getBalance( + stateAddress + ); console.log("state balance", stateBalance); console.log("Sending fund..."); diff --git a/packages/fund-sender/scripts/storeCNFTCertificates.ts b/packages/fund-sender/scripts/storeCNFTCertificates.ts index ddcdf3b..c52b419 100644 --- a/packages/fund-sender/scripts/storeCNFTCertificates.ts +++ b/packages/fund-sender/scripts/storeCNFTCertificates.ts @@ -1,12 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ // Set up anchor provider import { FundSenderClient } from "../client"; -import { logSplBalance } from "./lib/util"; import { PublicKey } from "@solana/web3.js"; -import { - getAssociatedTokenAddressSync, - TOKEN_PROGRAM_ID, -} from "@solana/spl-token"; // mainnet Sunrise const defaultSunriseStateAddress = @@ -15,8 +10,11 @@ const sunriseStateAddress = new PublicKey( process.env.STATE_ADDRESS ?? defaultSunriseStateAddress ); -const cnftAddressLookupTableAddress = process.env.CNFT_ALT ?? "FmV5V5C3kd9X7bXgFCeFbfBGyt46eUMy6s2kb3rZPudm"; -let cnftAddressLookupTable = cnftAddressLookupTableAddress ? new PublicKey(cnftAddressLookupTableAddress) : undefined; +const cnftAddressLookupTableAddress = + process.env.CNFT_ALT ?? "FmV5V5C3kd9X7bXgFCeFbfBGyt46eUMy6s2kb3rZPudm"; +let cnftAddressLookupTable = cnftAddressLookupTableAddress + ? new PublicKey(cnftAddressLookupTableAddress) + : undefined; // USAGE: yarn ts-node packages/fund-sender/storeCertificate.ts destinationName const destinationName = process.argv[2]; @@ -40,7 +38,10 @@ const destinationName = process.argv[2]; if (!cnftAddressLookupTable) { console.log("No CNFT address lookup table provided - creating..."); cnftAddressLookupTable = await client.createALTForCNFTTransfer(); - console.log("Created CNFT address lookup table", cnftAddressLookupTable.toBase58()); + console.log( + "Created CNFT address lookup table", + cnftAddressLookupTable.toBase58() + ); } console.log("Storing certificates..."); diff --git a/packages/yield-router/scripts/updateState.ts b/packages/yield-router/scripts/updateState.ts index be17329..ad6bfcd 100644 --- a/packages/yield-router/scripts/updateState.ts +++ b/packages/yield-router/scripts/updateState.ts @@ -17,7 +17,7 @@ const sunriseStateAddress = new PublicKey( (async () => { const stateAddress = - YieldRouterClient.getStateAddressFromSunriseAddress(sunriseStateAddress); + YieldRouterClient.getStateAddressFromSunriseAddress(sunriseStateAddress); const client = await YieldRouterClient.fetch(stateAddress); console.log("Updating yield router state:", stateAddress.toBase58()); diff --git a/programs/fund-sender/src/external_programs/mod.rs b/programs/fund-sender/src/external_programs/mod.rs index 177925c..90e3894 100644 --- a/programs/fund-sender/src/external_programs/mod.rs +++ b/programs/fund-sender/src/external_programs/mod.rs @@ -1,4 +1,2 @@ - pub(crate) mod mpl_bubblegum; pub(crate) mod spl_account_compression; - diff --git a/programs/fund-sender/src/external_programs/mpl_bubblegum.rs b/programs/fund-sender/src/external_programs/mpl_bubblegum.rs index 60af483..31fba68 100644 --- a/programs/fund-sender/src/external_programs/mpl_bubblegum.rs +++ b/programs/fund-sender/src/external_programs/mpl_bubblegum.rs @@ -8,4 +8,4 @@ impl anchor_lang::Id for MplBubblegum { fn id() -> Pubkey { MPL_BUBBLEGUM_ID } -} \ No newline at end of file +} diff --git a/programs/fund-sender/src/external_programs/spl_account_compression.rs b/programs/fund-sender/src/external_programs/spl_account_compression.rs index cb2786a..6f78d7b 100644 --- a/programs/fund-sender/src/external_programs/spl_account_compression.rs +++ b/programs/fund-sender/src/external_programs/spl_account_compression.rs @@ -7,4 +7,4 @@ impl anchor_lang::Id for SplAccountCompression { fn id() -> Pubkey { spl_account_compression::id() } -} \ No newline at end of file +} diff --git a/programs/fund-sender/src/lib.rs b/programs/fund-sender/src/lib.rs index 78b0d7b..560fc51 100644 --- a/programs/fund-sender/src/lib.rs +++ b/programs/fund-sender/src/lib.rs @@ -1,11 +1,11 @@ #![allow(clippy::result_large_err)] +use crate::utils::bubblegum::TRANSFER_DISCRIMINATOR; use crate::utils::errors::ErrorCode; use crate::utils::spend::*; use crate::utils::state::*; -use crate::utils::bubblegum::TRANSFER_DISCRIMINATOR; use anchor_lang::prelude::*; -mod utils; mod external_programs; +mod utils; declare_id!("sfsH2CVS2SaXwnrGwgTVrG7ytZAxSCsTnW82BvjWTGz"); @@ -43,9 +43,7 @@ pub mod fund_sender { Ok(()) } - pub fn send_from_state( - ctx: Context, - ) -> Result<()> { + pub fn send_from_state(ctx: Context) -> Result<()> { let state = &ctx.accounts.state; let amount = state.get_lamports(); @@ -165,7 +163,11 @@ pub mod fund_sender { let state_bytes = state.key().to_bytes(); let bump_bytes = &[state.input_account_bump]; - let seeds = &[crate::utils::seeds::INPUT_ACCOUNT, &state_bytes[..], bump_bytes][..]; + let seeds = &[ + crate::utils::seeds::INPUT_ACCOUNT, + &state_bytes[..], + bump_bytes, + ][..]; let signer_seeds = &[seeds]; msg!("manual cpi call"); @@ -178,6 +180,6 @@ pub mod fund_sender { &account_infos[..], signer_seeds, ) - .map_err(Into::into) + .map_err(Into::into) } } diff --git a/programs/fund-sender/src/utils/bubblegum.rs b/programs/fund-sender/src/utils/bubblegum.rs index 600860e..9e7e09f 100644 --- a/programs/fund-sender/src/utils/bubblegum.rs +++ b/programs/fund-sender/src/utils/bubblegum.rs @@ -1,2 +1,2 @@ // first 8 bytes of SHA256("global:transfer") -pub const TRANSFER_DISCRIMINATOR: &[u8; 8] = &[163, 52, 200, 231, 140, 3, 69, 186]; \ No newline at end of file +pub const TRANSFER_DISCRIMINATOR: &[u8; 8] = &[163, 52, 200, 231, 140, 3, 69, 186]; diff --git a/programs/fund-sender/src/utils/mod.rs b/programs/fund-sender/src/utils/mod.rs index 7bcc2a3..ab1f1f1 100644 --- a/programs/fund-sender/src/utils/mod.rs +++ b/programs/fund-sender/src/utils/mod.rs @@ -1,5 +1,5 @@ +pub(crate) mod bubblegum; pub(crate) mod errors; pub(crate) mod seeds; pub(crate) mod spend; pub(crate) mod state; -pub(crate) mod bubblegum; diff --git a/programs/fund-sender/src/utils/state.rs b/programs/fund-sender/src/utils/state.rs index dffadd6..1059d79 100644 --- a/programs/fund-sender/src/utils/state.rs +++ b/programs/fund-sender/src/utils/state.rs @@ -1,10 +1,10 @@ +use crate::external_programs::mpl_bubblegum::MplBubblegum; +use crate::external_programs::spl_account_compression::SplAccountCompression; use crate::utils::errors::ErrorCode; use crate::utils::seeds::{INPUT_ACCOUNT, STATE}; use anchor_lang::prelude::*; use anchor_spl::associated_token::AssociatedToken; use anchor_spl::token::{Mint, Token, TokenAccount}; -use crate::external_programs::mpl_bubblegum::MplBubblegum; -use crate::external_programs::spl_account_compression::SplAccountCompression; /* This struct will be used for both registering and updating the state account */ #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct GenericStateInput { @@ -176,7 +176,6 @@ pub struct StoreCNFTCertificates<'info> { /// CHECK: must match the one stated in the state, but can be any account pub certificate_vault: UncheckedAccount<'info>, - #[account( seeds = [merkle_tree.key().as_ref()], bump, @@ -184,7 +183,7 @@ pub struct StoreCNFTCertificates<'info> { )] /// CHECK: This account is neither written to nor read from. /// The bubblegum program checks its type - we don't need to do so here - pub tree_authority: UncheckedAccount<'info>,//Account<'info, TreeConfig>, + pub tree_authority: UncheckedAccount<'info>, //Account<'info, TreeConfig>, #[account(mut)] /// CHECK: This account is modified in the downstream program @@ -195,7 +194,7 @@ pub struct StoreCNFTCertificates<'info> { /// via CPI, to workaround the CPI size limit on Solana. /// The bubblegum program checks its type - we don't need to do so here /// While SplAccountCompression is using an older version of Anchor, we cannot get its ID here - pub log_wrapper: UncheckedAccount<'info>,//Program<'info, Noop>, + pub log_wrapper: UncheckedAccount<'info>, //Program<'info, Noop>, pub compression_program: Program<'info, SplAccountCompression>, pub bubblegum_program: Program<'info, MplBubblegum>, } diff --git a/scripts/manager.ts b/scripts/manager.ts index 90c1485..546faf0 100644 --- a/scripts/manager.ts +++ b/scripts/manager.ts @@ -1,6 +1,6 @@ -import {showData} from "./util"; -import {showMenu} from "./prompt"; +import { showData } from "./util"; +import { showMenu } from "./prompt"; await showData(); -showMenu(); \ No newline at end of file +await showMenu(); diff --git a/scripts/prompt.ts b/scripts/prompt.ts index 168c5e1..3f1f9c6 100644 --- a/scripts/prompt.ts +++ b/scripts/prompt.ts @@ -1,60 +1,62 @@ -import chalk from 'chalk'; -import readlineSync from 'readline-sync'; -import {showData} from "./util"; -import {submenuRouteToRecipient} from "./submenu/routeToRecipient"; -import {submenuAllocateYield} from "./submenu/allocateYield"; -import {submenuUpdateProportions} from "./submenu/updateProportions"; -import {submenuUpdateDestinationAddress} from "./submenu/updateDestinationAddress"; -import {submenuStoreCertificates} from "./submenu/storeCertificates"; +import chalk from "chalk"; +import readlineSync from "readline-sync"; +import { showData } from "./util"; +import { submenuRouteToRecipient } from "./submenu/routeToRecipient"; +import { submenuAllocateYield } from "./submenu/allocateYield"; +import { submenuUpdateProportions } from "./submenu/updateProportions"; +import { submenuUpdateDestinationAddress } from "./submenu/updateDestinationAddress"; +import { submenuStoreCertificates } from "./submenu/storeCertificates"; export const showMenu = async () => { - console.log(chalk.magentaBright('\nChoose an option:')); - console.log(chalk.cyanBright('1) Refresh')); - console.log(chalk.cyanBright('2) Allocate Yield')); - console.log(chalk.cyanBright('3) Route Funds to Recipient')); - console.log(chalk.cyanBright('4) Update Proportions')); - console.log(chalk.cyanBright('5) Update Recipient Address')); - console.log(chalk.cyanBright('6) Store Certificates')); - console.log(chalk.cyanBright('7) Add Recipient')); - console.log(chalk.cyanBright('8) Remove Recipient')); - console.log(chalk.cyanBright('9) Quit')); + console.log(chalk.magentaBright("\nChoose an option:")); + console.log(chalk.cyanBright("1) Refresh")); + console.log(chalk.cyanBright("2) Allocate Yield")); + console.log(chalk.cyanBright("3) Route Funds to Recipient")); + console.log(chalk.cyanBright("4) Update Proportions")); + console.log(chalk.cyanBright("5) Update Recipient Address")); + console.log(chalk.cyanBright("6) Store Certificates")); + console.log(chalk.cyanBright("7) Add Recipient")); + console.log(chalk.cyanBright("8) Remove Recipient")); + console.log(chalk.cyanBright("9) Quit")); - const choice = readlineSync.keyIn(chalk.yellow('\nEnter your choice: '), { limit: '$<1-8>' }); + const choice = readlineSync.keyIn(chalk.yellow("\nEnter your choice: "), { + limit: "$<1-8>", + }); - switch (choice) { - case '1': - console.log(chalk.green('Refreshing...')); - await showData(); - break; - case '2': - await submenuAllocateYield(); - break; - case '3': - await submenuRouteToRecipient(); - break; - case '4': - await submenuUpdateProportions(); - break; - case '5': - await submenuUpdateDestinationAddress(); - break; - case '6': - await submenuStoreCertificates(); - break; - case '7': - console.log(chalk.green('Adding recipient...')); - break; - case '8': - console.log(chalk.green('Removing recipient...')); - break; - case '9': - console.log(chalk.green('Exiting...')); - process.exit(0); - break; - default: - console.log(chalk.red('Invalid choice, please try again.')); - showMenu(); // Re-display menu for invalid input - break; - } - showMenu(); -}; \ No newline at end of file + switch (choice) { + case "1": + console.log(chalk.green("Refreshing...")); + await showData(); + break; + case "2": + await submenuAllocateYield(); + break; + case "3": + await submenuRouteToRecipient(); + break; + case "4": + await submenuUpdateProportions(); + break; + case "5": + await submenuUpdateDestinationAddress(); + break; + case "6": + await submenuStoreCertificates(); + break; + case "7": + console.log(chalk.green("Adding recipient...")); + break; + case "8": + console.log(chalk.green("Removing recipient...")); + break; + case "9": + console.log(chalk.green("Exiting...")); + process.exit(0); + break; + default: + console.log(chalk.red("Invalid choice, please try again.")); + await showMenu(); // Re-display menu for invalid input + break; + } + await showMenu(); +}; diff --git a/scripts/submenu/allocateYield.ts b/scripts/submenu/allocateYield.ts index 0057c74..b37ff46 100644 --- a/scripts/submenu/allocateYield.ts +++ b/scripts/submenu/allocateYield.ts @@ -1,45 +1,47 @@ import chalk from "chalk"; import { - getBalance, - getFundSenderData, printBalance, - selectAmount, - yieldRouterClient + getBalance, + getFundSenderData, + printBalance, + selectAmount, + yieldRouterClient, } from "../util"; import readlineSync from "readline-sync"; import BN from "bn.js"; export const submenuAllocateYield = async () => { - const availableAmount = await getBalance(yieldRouterClient.getInputYieldAccount()) + const availableAmount = await getBalance( + yieldRouterClient.getInputYieldAccount() + ); - // if there is no balance, cancel - if (!availableAmount) { - console.log(chalk.red('No balance available to allocate')); - return; - } + // if there is no balance, cancel + if (availableAmount === undefined || availableAmount === 0) { + console.log(chalk.red("No balance available to allocate")); + return; + } - console.log(chalk.magentaBright('\nChoose an amount to allocate:')); - const chosenAmountLamports = selectAmount(availableAmount); + console.log(chalk.magentaBright("\nChoose an amount to allocate:")); + const chosenAmountLamports = selectAmount(availableAmount); - const fundSenderData = await getFundSenderData(); + const fundSenderData = await getFundSenderData(); - // Calculate the allocations for each recipient based on the chosen amount - const allocations = fundSenderData.map(({ fundSenderName, allocation }) => ({ - name: fundSenderName, - allocation: chosenAmountLamports * (allocation / 100) - }) - ); + // Calculate the allocations for each recipient based on the chosen amount + const allocations = fundSenderData.map(({ fundSenderName, allocation }) => ({ + name: fundSenderName, + allocation: chosenAmountLamports * (allocation / 100), + })); - // present the allocations to the user and ask for confirmation: - console.log(chalk.magentaBright('\nAllocations:')); - allocations.forEach(({ name, allocation }) => { - console.log(chalk.cyanBright(`${name}: ${printBalance(allocation)}`)); - }); + // present the allocations to the user and ask for confirmation: + console.log(chalk.magentaBright("\nAllocations:")); + allocations.forEach(({ name, allocation }) => { + console.log(chalk.cyanBright(`${name}: ${printBalance(allocation)}`)); + }); - const confirm = readlineSync.question(chalk.yellow('Confirm (y/n): ')); - if (confirm === 'y') { - await yieldRouterClient.allocateYield(new BN(chosenAmountLamports)); - console.log(chalk.green(`Done`)); - } else { - console.log(chalk.red('Routing cancelled')); - } -} \ No newline at end of file + const confirm = readlineSync.question(chalk.yellow("Confirm (y/n): ")); + if (confirm === "y") { + await yieldRouterClient.allocateYield(new BN(chosenAmountLamports)); + console.log(chalk.green(`Done`)); + } else { + console.log(chalk.red("Routing cancelled")); + } +}; diff --git a/scripts/submenu/routeToRecipient.ts b/scripts/submenu/routeToRecipient.ts index 25409b3..674c4cb 100644 --- a/scripts/submenu/routeToRecipient.ts +++ b/scripts/submenu/routeToRecipient.ts @@ -1,42 +1,55 @@ import chalk from "chalk"; -import {fundSenderClients, fundSenderDestinations, getFundSenderAvailableAmount, selectAmount} from "../util"; +import { + fundSenderClients, + fundSenderDestinations, + getFundSenderAvailableAmount, + selectAmount, +} from "../util"; import readlineSync from "readline-sync"; import BN from "bn.js"; const destinations = Object.keys(fundSenderDestinations); export const submenuRouteToRecipient = async () => { - console.log(chalk.magentaBright('\nChoose a recipient:')); - destinations.forEach((destinationName, index) => { - console.log(chalk.cyanBright(`${index + 1}) ${destinationName}`)); - }); - console.log(chalk.cyanBright(`${destinations.length + 1}) Cancel`)); - - const choice = readlineSync.keyIn(chalk.yellow('\nEnter your choice: '), { limit: `$<1-${destinations.length}>` }); - - if (choice === `${destinations.length + 1}`) { - return; - } - - const destinationName = destinations[parseInt(choice) - 1]; - const client = fundSenderClients.find(c => c.config.destinationName === destinationName); - - if (!client) throw new Error('Client not found - trigger a refresh'); - - const availableAmount = await getFundSenderAvailableAmount(client); - - console.log(chalk.magentaBright('\nChoose an amount to route:')); - const chosenAmountLamports = selectAmount(availableAmount); - - // ask for confirmation: - const destinationAddress = client.config.destinationAccount.toBase58(); - console.log(chalk.yellow(`Routing ${chosenAmountLamports} lamports to ${destinationAddress}`)); - const confirm = readlineSync.question(chalk.yellow('Confirm (y/n): ')); - - if (confirm === 'y') { - await client.sendFunds(new BN(chosenAmountLamports)); - console.log(chalk.green(`Done`)); - } else { - console.log(chalk.red('Routing cancelled')); - } -} \ No newline at end of file + console.log(chalk.magentaBright("\nChoose a recipient:")); + destinations.forEach((destinationName, index) => { + console.log(chalk.cyanBright(`${index + 1}) ${destinationName}`)); + }); + console.log(chalk.cyanBright(`${destinations.length + 1}) Cancel`)); + + const choice = readlineSync.keyIn(chalk.yellow("\nEnter your choice: "), { + limit: `$<1-${destinations.length}>`, + }); + + if (choice === `${destinations.length + 1}`) { + return; + } + + const destinationName = destinations[parseInt(choice) - 1]; + const client = fundSenderClients.find( + (c) => c.config.destinationName === destinationName + ); + + if (!client) throw new Error("Client not found - trigger a refresh"); + + const availableAmount = await getFundSenderAvailableAmount(client); + + console.log(chalk.magentaBright("\nChoose an amount to route:")); + const chosenAmountLamports = selectAmount(availableAmount); + + // ask for confirmation: + const destinationAddress = client.config.destinationAccount.toBase58(); + console.log( + chalk.yellow( + `Routing ${chosenAmountLamports} lamports to ${destinationAddress}` + ) + ); + const confirm = readlineSync.question(chalk.yellow("Confirm (y/n): ")); + + if (confirm === "y") { + await client.sendFunds(new BN(chosenAmountLamports)); + console.log(chalk.green(`Done`)); + } else { + console.log(chalk.red("Routing cancelled")); + } +}; diff --git a/scripts/submenu/storeCertificates.ts b/scripts/submenu/storeCertificates.ts index 4352cd3..4419527 100644 --- a/scripts/submenu/storeCertificates.ts +++ b/scripts/submenu/storeCertificates.ts @@ -1,84 +1,94 @@ import chalk from "chalk"; -import {fundSenderClients, fundSenderDestinations} from "../util"; -import {FundSenderClient, FundSenderConfig} from "../../packages/fund-sender/client"; +import { + ConfiguredClient, + fundSenderClients, + fundSenderDestinations, +} from "../util"; import readlineSync from "readline-sync"; -import {TOKEN_PROGRAM_ID} from "@solana/spl-token"; -import {PublicKey} from "@solana/web3.js"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; const destinations = Object.keys(fundSenderDestinations); -type ConfiguredClient = (FundSenderClient & { - config: FundSenderConfig; -}) - const processSPLCertificates = async (client: ConfiguredClient) => { - const allInputTokenAccountsResponse = - await client.provider.connection.getParsedTokenAccountsByOwner( - client.getInputAccount(), - { - programId: TOKEN_PROGRAM_ID, - } - ); - - const allInputTokenAccounts = allInputTokenAccountsResponse.value; - - if (allInputTokenAccounts.length === 0) { - console.log("No certificates to store."); - return; - } - - console.log(`Storing ${allInputTokenAccounts.length} certificates...`); - for (const inputTokenAccount of allInputTokenAccounts) { - const mint = new PublicKey(inputTokenAccount.account.data.parsed.info.mint); - await client.storeCertificates(inputTokenAccount.pubkey, mint); - } -} - -const processCNFTCertificates = async (client: ConfiguredClient, destination: { lookupTable: PublicKey }) => { - const assets = await client.getCNFTCertificates(); - console.log("number of CNFT certificates", assets.length); - - if (assets.length === 0) { - console.log("No certificates to store."); - return; - } - - let cnftAddressLookupTable = destination.lookupTable; - if (!cnftAddressLookupTable) { - console.log("No CNFT address lookup table provided - creating..."); - cnftAddressLookupTable = await client.createALTForCNFTTransfer(); - console.log("Created CNFT address lookup table", cnftAddressLookupTable.toBase58()); - } - - console.log(`Storing ${assets.length} certificates...`); - for (const asset of assets) { - await client.storeCNFTCertificate(asset.id, cnftAddressLookupTable); - } -} + const allInputTokenAccountsResponse = + await client.provider.connection.getParsedTokenAccountsByOwner( + client.getInputAccount(), + { + programId: TOKEN_PROGRAM_ID, + } + ); + + const allInputTokenAccounts = allInputTokenAccountsResponse.value; + + if (allInputTokenAccounts.length === 0) { + console.log("No certificates to store."); + return; + } + + console.log(`Storing ${allInputTokenAccounts.length} certificates...`); + for (const inputTokenAccount of allInputTokenAccounts) { + const mint = new PublicKey(inputTokenAccount.account.data.parsed.info.mint); + await client.storeCertificates(inputTokenAccount.pubkey, mint); + } +}; + +const processCNFTCertificates = async ( + client: ConfiguredClient, + destination: { lookupTable?: PublicKey } +) => { + const assets = await client.getCNFTCertificates(); + console.log("number of CNFT certificates", assets.length); + + if (assets.length === 0) { + console.log("No certificates to store."); + return; + } + + let cnftAddressLookupTable = destination.lookupTable; + if (!cnftAddressLookupTable) { + console.log("No CNFT address lookup table provided - creating..."); + cnftAddressLookupTable = await client.createALTForCNFTTransfer(); + console.log( + "Created CNFT address lookup table", + cnftAddressLookupTable.toBase58() + ); + } + + console.log(`Storing ${assets.length} certificates...`); + for (const asset of assets) { + await client.storeCNFTCertificate(asset.id, cnftAddressLookupTable); + } +}; export const submenuStoreCertificates = async () => { - console.log(chalk.magentaBright('\nChoose a retirement certificate source:')); - destinations.forEach((destinationName, index) => { - console.log(chalk.cyanBright(`${index + 1}) ${destinationName}`)); - }); - console.log(chalk.cyanBright(`${destinations.length + 1}) Cancel`)); - - const choice = readlineSync.keyIn(chalk.yellow('\nEnter your choice: '), { limit: `$<1-${destinations.length}>` }); - - if (choice === `${destinations.length + 1}`) { - return; - } - - const destinationName = destinations[parseInt(choice) - 1] as keyof typeof fundSenderDestinations; - const client = fundSenderClients.find(c => c.config.destinationName === destinationName); - - if (!client) throw new Error('Client not found - trigger a refresh'); - - if (fundSenderDestinations[destinationName].type === "cnft") { - await processCNFTCertificates(client, fundSenderDestinations[destinationName] as { lookupTable: PublicKey }); - } else { - await processSPLCertificates(client); - } - - -} \ No newline at end of file + console.log(chalk.magentaBright("\nChoose a retirement certificate source:")); + destinations.forEach((destinationName, index) => { + console.log(chalk.cyanBright(`${index + 1}) ${destinationName}`)); + }); + console.log(chalk.cyanBright(`${destinations.length + 1}) Cancel`)); + + const choice = readlineSync.keyIn(chalk.yellow("\nEnter your choice: "), { + limit: `$<1-${destinations.length}>`, + }); + + if (choice === `${destinations.length + 1}`) { + return; + } + + const destinationName = destinations[parseInt(choice) - 1]; + const client = fundSenderClients.find( + (c) => c.config.destinationName === destinationName + ); + + if (!client) throw new Error("Client not found - trigger a refresh"); + + if (fundSenderDestinations[destinationName].type === "cnft") { + await processCNFTCertificates( + client, + fundSenderDestinations[destinationName] as { lookupTable: PublicKey } + ); + } else { + await processSPLCertificates(client); + } +}; diff --git a/scripts/submenu/updateDestinationAddress.ts b/scripts/submenu/updateDestinationAddress.ts index fd2fcc7..1820135 100644 --- a/scripts/submenu/updateDestinationAddress.ts +++ b/scripts/submenu/updateDestinationAddress.ts @@ -1,48 +1,61 @@ import chalk from "chalk"; -import {fundSenderClients, fundSenderDestinations} from "../util"; +import { fundSenderClients, fundSenderDestinations } from "../util"; import readlineSync from "readline-sync"; -import {PublicKey} from "@solana/web3.js"; +import { PublicKey } from "@solana/web3.js"; const destinations = Object.keys(fundSenderDestinations); export const submenuUpdateDestinationAddress = async () => { - console.log(chalk.magentaBright('\nChoose a recipient:')); - destinations.forEach((destinationName, index) => { - console.log(chalk.cyanBright(`${index + 1}) ${destinationName}`)); - }); - console.log(chalk.cyanBright(`${destinations.length + 1}) Cancel`)); - - const choice = readlineSync.keyIn(chalk.yellow('\nEnter your choice: '), { limit: `$<1-${destinations.length}>` }); - - if (choice === `${destinations.length + 1}`) { - return; - } - - const destinationName = destinations[parseInt(choice) - 1]; - const client = fundSenderClients.find(c => c.config.destinationName === destinationName); - - if (!client) throw new Error('Client not found - trigger a refresh'); - - // ask for address - const newDestinationAddress = readlineSync.question(chalk.yellow('Enter the new destination address: ')); - - // verify it is a valid Solana address - let newDestinationAddressKey: PublicKey; - try { - newDestinationAddressKey = new PublicKey(newDestinationAddress); - } catch (e) { - console.log(chalk.red('Invalid address')); - return; - } - - // ask for confirmation: - console.log(chalk.yellow(`New destination address: ${newDestinationAddressKey.toBase58()}`)); - const confirm = readlineSync.question(chalk.yellow('Confirm (y/n): ')); - - if (confirm === 'y') { - await client.updateDestinationAccount(newDestinationAddressKey, client.config.spendThreshold); - console.log(chalk.green(`Done`)); - } else { - console.log(chalk.red('Update cancelled')); - } -} \ No newline at end of file + console.log(chalk.magentaBright("\nChoose a recipient:")); + destinations.forEach((destinationName, index) => { + console.log(chalk.cyanBright(`${index + 1}) ${destinationName}`)); + }); + console.log(chalk.cyanBright(`${destinations.length + 1}) Cancel`)); + + const choice = readlineSync.keyIn(chalk.yellow("\nEnter your choice: "), { + limit: `$<1-${destinations.length}>`, + }); + + if (choice === `${destinations.length + 1}`) { + return; + } + + const destinationName = destinations[parseInt(choice) - 1]; + const client = fundSenderClients.find( + (c) => c.config.destinationName === destinationName + ); + + if (!client) throw new Error("Client not found - trigger a refresh"); + + // ask for address + const newDestinationAddress = readlineSync.question( + chalk.yellow("Enter the new destination address: ") + ); + + // verify it is a valid Solana address + let newDestinationAddressKey: PublicKey; + try { + newDestinationAddressKey = new PublicKey(newDestinationAddress); + } catch (e) { + console.log(chalk.red("Invalid address")); + return; + } + + // ask for confirmation: + console.log( + chalk.yellow( + `New destination address: ${newDestinationAddressKey.toBase58()}` + ) + ); + const confirm = readlineSync.question(chalk.yellow("Confirm (y/n): ")); + + if (confirm === "y") { + await client.updateDestinationAccount( + newDestinationAddressKey, + client.config.spendThreshold + ); + console.log(chalk.green(`Done`)); + } else { + console.log(chalk.red("Update cancelled")); + } +}; diff --git a/scripts/submenu/updateProportions.ts b/scripts/submenu/updateProportions.ts index 439d18f..5720df5 100644 --- a/scripts/submenu/updateProportions.ts +++ b/scripts/submenu/updateProportions.ts @@ -1,45 +1,61 @@ import chalk from "chalk"; -import { - getFundSenderData, - yieldRouterClient -} from "../util"; +import { getFundSenderData, yieldRouterClient } from "../util"; import readlineSync from "readline-sync"; export const submenuUpdateProportions = async () => { - // ask for confirmation: - const confirm = readlineSync.question(chalk.yellow('Update allocation proportions? (y/n): ')); - if (confirm !== 'y') return; - - const fundSenderData = await getFundSenderData(); - - const newAllocations = fundSenderData.map((data, index) => { - const destinationName = data.fundSenderName - const currentAllocation = data.allocation - - console.log(chalk.cyanBright(`${index + 1}: ${destinationName} Current allocation: ${currentAllocation}%. `)); - - // a regex that matches a number between 0 and 100 - return readlineSync.question('New allocation:', { limit: /^([0-9]|[1-9][0-9]|100)$/ }); + // ask for confirmation: + const confirm = readlineSync.question( + chalk.yellow("Update allocation proportions? (y/n): ") + ); + if (confirm !== "y") return; + + const fundSenderData = await getFundSenderData(); + + const newAllocations = fundSenderData.map((data, index) => { + const destinationName = data.fundSenderName; + const currentAllocation = data.allocation; + + console.log( + chalk.cyanBright( + `${ + index + 1 + }: ${destinationName} Current allocation: ${currentAllocation}%. ` + ) + ); + + // a regex that matches a number between 0 and 100 + return readlineSync.question("New allocation:", { + limit: /^([0-9]|[1-9][0-9]|100)$/, }); - - // ensure the new allocations sum to 100 - const sum = newAllocations.reduce((acc, curr) => acc + Number(curr), 0); - if (sum !== 100) { - console.log(chalk.red('New allocations do not sum to 100%')); - return; - } - - // ask for confirmation: - console.log(chalk.magentaBright('\nNew allocations:')); - newAllocations.forEach((allocation, index) => { - console.log(chalk.cyanBright(`${fundSenderData[index].fundSenderName}: ${allocation}%`)); - }); - const confirmUpdate = readlineSync.question(chalk.yellow('Confirm update (y/n): ')); - if (confirmUpdate !== 'y') return; - - const outputYieldAccounts = yieldRouterClient.config.outputYieldAccounts; - const spendProportions = newAllocations.map(a => Number(a)); - await yieldRouterClient.updateOutputYieldAccounts(outputYieldAccounts, spendProportions); - - console.log(chalk.green(`Done`)); -} \ No newline at end of file + }); + + // ensure the new allocations sum to 100 + const sum = newAllocations.reduce((acc, curr) => acc + Number(curr), 0); + if (sum !== 100) { + console.log(chalk.red("New allocations do not sum to 100%")); + return; + } + + // ask for confirmation: + console.log(chalk.magentaBright("\nNew allocations:")); + newAllocations.forEach((allocation, index) => { + console.log( + chalk.cyanBright( + `${fundSenderData[index].fundSenderName}: ${allocation}%` + ) + ); + }); + const confirmUpdate = readlineSync.question( + chalk.yellow("Confirm update (y/n): ") + ); + if (confirmUpdate !== "y") return; + + const outputYieldAccounts = yieldRouterClient.config.outputYieldAccounts; + const spendProportions = newAllocations.map((a) => Number(a)); + await yieldRouterClient.updateOutputYieldAccounts( + outputYieldAccounts, + spendProportions + ); + + console.log(chalk.green(`Done`)); +}; diff --git a/scripts/util.ts b/scripts/util.ts index 84a0cbe..d32ccae 100644 --- a/scripts/util.ts +++ b/scripts/util.ts @@ -1,188 +1,231 @@ -import {LAMPORTS_PER_SOL, PublicKey} from "@solana/web3.js"; -import {getAccount} from "@solana/spl-token"; +import { LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js"; +import { getAccount } from "@solana/spl-token"; import chalk from "chalk"; -import {InitialisedClient, YieldRouterClient} from "../packages/yield-router/client"; -import {FundSenderClient, FundSenderConfig} from "../packages/fund-sender/client"; +import { + InitialisedClient, + YieldRouterClient, +} from "../packages/yield-router/client"; +import { + FundSenderClient, + FundSenderConfig, +} from "../packages/fund-sender/client"; import Table from "cli-table3"; import readlineSync from "readline-sync"; -type FundSenderType = { - type: 'spl' -} | { - type: 'cnft', lookupTable: PublicKey +type FundSenderType = + | { + type: "spl"; + } + | { + type: "cnft"; + lookupTable: PublicKey; + }; + +export type ConfiguredClient = FundSenderClient & { + config: FundSenderConfig; }; export const fundSenderDestinations: Record = { - ecotoken: { type: 'cnft', lookupTable: new PublicKey('FmV5V5C3kd9X7bXgFCeFbfBGyt46eUMy6s2kb3rZPudm')}, - toucan: { type: 'spl' }, - loompact: { type: 'spl' } -} + ecotoken: { + type: "cnft", + lookupTable: new PublicKey("FmV5V5C3kd9X7bXgFCeFbfBGyt46eUMy6s2kb3rZPudm"), + }, + toucan: { type: "spl" }, + loompact: { type: "spl" }, +}; export const defaultSunriseStateAddress = - "43m66crxGfXSJpmx5wXRoFuHubhHA1GCvtHgmHW6cM1P"; + "43m66crxGfXSJpmx5wXRoFuHubhHA1GCvtHgmHW6cM1P"; export const sunriseStateAddress = new PublicKey( - process.env.STATE_ADDRESS ?? defaultSunriseStateAddress + process.env.STATE_ADDRESS ?? defaultSunriseStateAddress ); export const stateAddress = - YieldRouterClient.getStateAddressFromSunriseAddress(sunriseStateAddress); + YieldRouterClient.getStateAddressFromSunriseAddress(sunriseStateAddress); export let yieldRouterClient: InitialisedClient; -export const getBalance = - async (account: PublicKey): Promise => { - const accountInfo = await yieldRouterClient.provider.connection.getAccountInfo( - account - ); - return accountInfo?.lamports - }; +export const getBalance = async ( + account: PublicKey +): Promise => { + const accountInfo = + await yieldRouterClient.provider.connection.getAccountInfo(account); + return accountInfo?.lamports; +}; -export const getSplBalance = - async (account: PublicKey): Promise => { - const accountInfo = await getAccount(yieldRouterClient.provider.connection, account); - return accountInfo?.amount; - }; +export const getSplBalance = async ( + account: PublicKey +): Promise => { + const accountInfo = await getAccount( + yieldRouterClient.provider.connection, + account + ); + return accountInfo?.amount; +}; -export const printBalance = (balance: number | undefined, defaultVal = 0): string => { - const solVal = balance ? balance / 10 ** 9 : 0; +export const printBalance = (balance: number | undefined): string => { + const solVal = balance !== undefined ? balance / 10 ** 9 : 0; - let roundedSolVal: string; + let roundedSolVal: string; - if (solVal >= 1000) { - // Show no decimals for values >= 1000 - roundedSolVal = Math.floor(solVal).toLocaleString(); - } else if (solVal < 1) { - // Show two most significant decimal places for values < 1 - roundedSolVal = solVal.toPrecision(2); - } else { - // Show two decimal places for values between 1 and 999.99 - roundedSolVal = solVal.toFixed(2); - } + if (solVal >= 1000) { + // Show no decimals for values >= 1000 + roundedSolVal = Math.floor(solVal).toLocaleString(); + } else if (solVal < 1) { + // Show two most significant decimal places for values < 1 + roundedSolVal = solVal.toPrecision(2); + } else { + // Show two decimal places for values between 1 and 999.99 + roundedSolVal = solVal.toFixed(2); + } - return `${chalk.green(roundedSolVal)} SOL`; + return `${chalk.green(roundedSolVal)} SOL`; }; -export let fundSenderClients: (FundSenderClient & { +export let fundSenderClients: Array< + FundSenderClient & { config: FundSenderConfig; -})[]; + } +>; -const getFundSenderClients = () => Promise.all( +const getFundSenderClients = async (): Promise => + Promise.all( Object.keys(fundSenderDestinations).map(async (destinationName) => { - const stateAddress = FundSenderClient.getStateAddressFromSunriseAddress( - sunriseStateAddress, - destinationName - ); - return await FundSenderClient.fetch(stateAddress); + const stateAddress = FundSenderClient.getStateAddressFromSunriseAddress( + sunriseStateAddress, + destinationName + ); + return FundSenderClient.fetch(stateAddress); }) -); + ); + +export const getFundSenderAvailableAmount = async ( + client: FundSenderClient +): Promise => { + const inputAccount = client.getInputAccount(); + const balance = await getBalance(inputAccount); + const minimumRentExemption = + await client.provider.connection.getMinimumBalanceForRentExemption(0); + return Math.max((balance ?? 0) - minimumRentExemption, 0); +}; -export const getFundSenderAvailableAmount = async (client: FundSenderClient): Promise => { - const inputAccount = client.getInputAccount(); - const balance = await getBalance(inputAccount); - const minimumRentExemption = await client.provider.connection.getMinimumBalanceForRentExemption(0); - return Math.max((balance ?? 0) - minimumRentExemption, 0); -} - -export const getFundSenderData = () => { - return Promise.all(yieldRouterClient.config.outputYieldAccounts.map(async (a, index) => { - const fundSenderClient = fundSenderClients.find(c => c.getInputAccount().equals(a)); - const balance = await getBalance(a); - const destinationAddress = fundSenderClient?.config.destinationAccount; - const destinationBalance = destinationAddress ? await getBalance(destinationAddress) : undefined; - const fundSenderThreshold = fundSenderClient?.config.spendThreshold?.toNumber() ?? 0; - const allocation = yieldRouterClient.config.spendProportions[index]; - return { - address: a.toBase58(), - allocation, - balance, - fundSender: fundSenderClient, - fundSenderName: fundSenderClient?.config.destinationName ?? "Not Fund Sender Destination", - destinationAddress: destinationAddress?.toBase58(), - destinationBalance, - fundSenderThreshold - } - })); -} +export const getFundSenderData = async () => { + return Promise.all( + yieldRouterClient.config.outputYieldAccounts.map(async (a, index) => { + const fundSenderClient = fundSenderClients.find((c) => + c.getInputAccount().equals(a) + ); + const balance = await getBalance(a); + const destinationAddress = fundSenderClient?.config.destinationAccount; + const destinationBalance = destinationAddress + ? await getBalance(destinationAddress) + : undefined; + const fundSenderThreshold = + fundSenderClient?.config.spendThreshold?.toNumber() ?? 0; + const allocation = yieldRouterClient.config.spendProportions[index]; + return { + address: a.toBase58(), + allocation, + balance, + fundSender: fundSenderClient, + fundSenderName: + fundSenderClient?.config.destinationName ?? + "Not Fund Sender Destination", + destinationAddress: destinationAddress?.toBase58(), + destinationBalance, + fundSenderThreshold, + }; + }) + ); +}; export const showData = async () => { - yieldRouterClient = await YieldRouterClient.fetch(stateAddress); - console.clear(); - - fundSenderClients = await getFundSenderClients(); - - const fundSenderData = await getFundSenderData(); - -// Table for state account data - const stateTable = new Table({ - head: [chalk.blueBright('Property'), chalk.blueBright('Value')], - colWidths: [30, 80] - }); - - const inputBalance = await getBalance(yieldRouterClient.getInputYieldAccount()); - -// Populate state account table - stateTable.push( - { 'State Address': yieldRouterClient.stateAddress.toBase58() }, - { 'Update Authority': yieldRouterClient.config.updateAuthority.toBase58() }, - { 'Spend Proportions': yieldRouterClient.config.spendProportions.join(', ') }, - { 'Spend Threshold': yieldRouterClient.config.spendThreshold.toString() }, - { 'Input Address': yieldRouterClient.getInputYieldAccount().toBase58() }, - { 'Balance': printBalance(inputBalance) } - ); - -// Display state account data - console.log(chalk.magentaBright('Yield Router:\n')); - console.log(stateTable.toString()); - -// Table for output yield accounts - const outputTable = new Table({ - head: [ - chalk.blueBright('Fund Sender Name'), - chalk.blueBright('Allocation'), - chalk.blueBright('Address'), - chalk.blueBright('Balance'), - chalk.blueBright('Destination'), - chalk.blueBright('Dest. Balance'), - chalk.blueBright('Min. Spend') - ], - colWidths: [25, 15, 50, 20, 50, 20, 20] - }); - -// Populate the output yield accounts table - fundSenderData.forEach((account) => { - outputTable.push([ - chalk.yellow(account.fundSenderName), - `${account.allocation}%`, - account.address, - printBalance(account.balance), - account.destinationAddress, - printBalance(account.destinationBalance), - printBalance(account.fundSenderThreshold) - ]); - }); - -// Display the output yield accounts table - console.log(chalk.magentaBright('Output Yield Accounts:')); - console.log(outputTable.toString()); -} + yieldRouterClient = await YieldRouterClient.fetch(stateAddress); + console.clear(); + + fundSenderClients = await getFundSenderClients(); + + const fundSenderData = await getFundSenderData(); + + // Table for state account data + const stateTable = new Table({ + head: [chalk.blueBright("Property"), chalk.blueBright("Value")], + colWidths: [30, 80], + }); + + const inputBalance = await getBalance( + yieldRouterClient.getInputYieldAccount() + ); + + // Populate state account table + stateTable.push( + { "State Address": yieldRouterClient.stateAddress.toBase58() }, + { "Update Authority": yieldRouterClient.config.updateAuthority.toBase58() }, + { + "Spend Proportions": yieldRouterClient.config.spendProportions.join(", "), + }, + { "Spend Threshold": yieldRouterClient.config.spendThreshold.toString() }, + { "Input Address": yieldRouterClient.getInputYieldAccount().toBase58() }, + { Balance: printBalance(inputBalance) } + ); + + // Display state account data + console.log(chalk.magentaBright("Yield Router:\n")); + console.log(stateTable.toString()); + + // Table for output yield accounts + const outputTable = new Table({ + head: [ + chalk.blueBright("Fund Sender Name"), + chalk.blueBright("Allocation"), + chalk.blueBright("Address"), + chalk.blueBright("Balance"), + chalk.blueBright("Destination"), + chalk.blueBright("Dest. Balance"), + chalk.blueBright("Min. Spend"), + ], + colWidths: [25, 15, 50, 20, 50, 20, 20], + }); + + // Populate the output yield accounts table + fundSenderData.forEach((account) => { + outputTable.push([ + chalk.yellow(account.fundSenderName), + `${account.allocation}%`, + account.address, + printBalance(account.balance), + account.destinationAddress, + printBalance(account.destinationBalance), + printBalance(account.fundSenderThreshold), + ]); + }); + + // Display the output yield accounts table + console.log(chalk.magentaBright("Output Yield Accounts:")); + console.log(outputTable.toString()); +}; export const selectAmount = (maxLamports: number): number => { - const maxSol = maxLamports / LAMPORTS_PER_SOL; - const formattedMaxSol = printBalance(maxLamports); // Display SOL according to your rules + const maxSol = maxLamports / LAMPORTS_PER_SOL; + const formattedMaxSol = printBalance(maxLamports); // Display SOL according to your rules - const inputSol = readlineSync.question( - chalk.yellow(`\nEnter amount in SOL (default: ${formattedMaxSol}): `), - { defaultInput: maxSol.toString() } // Set default as max SOL amount - ); + const inputSol = readlineSync.question( + chalk.yellow(`\nEnter amount in SOL (default: ${formattedMaxSol}): `), + { defaultInput: maxSol.toString() } // Set default as max SOL amount + ); - const selectedSol = Number(inputSol); + const selectedSol = Number(inputSol); - if (isNaN(selectedSol) || selectedSol <= 0 || selectedSol > maxSol) { - console.log(chalk.red(`Invalid amount. Please enter a number between 0 and ${formattedMaxSol}.`)); - return selectAmount(maxLamports); // Re-prompt the user if input is invalid - } + if (isNaN(selectedSol) || selectedSol <= 0 || selectedSol > maxSol) { + console.log( + chalk.red( + `Invalid amount. Please enter a number between 0 and ${formattedMaxSol}.` + ) + ); + return selectAmount(maxLamports); // Re-prompt the user if input is invalid + } - // Convert SOL back to lamports - return Math.floor(selectedSol * LAMPORTS_PER_SOL); + // Convert SOL back to lamports + return Math.floor(selectedSol * LAMPORTS_PER_SOL); };