diff --git a/src/eliza/packages/plugin-solana/src/actions/airdrop.ts b/src/eliza/packages/plugin-solana/src/actions/airdrop.ts index 125bf12..5b8a4ed 100644 --- a/src/eliza/packages/plugin-solana/src/actions/airdrop.ts +++ b/src/eliza/packages/plugin-solana/src/actions/airdrop.ts @@ -45,6 +45,17 @@ export const airdrop: Action = { return true; }, description: 'Perform claim airdrop for the user agent account', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + const response = convertNullStrings(parameters); + if (!response.programName) { + const responseMsg = { + text: 'Please tell me the program name of the airdrop', + }; + callback?.(responseMsg); + return {status: 'incomplete info', parameters: parameters}; + } + return {status: 'success', parameters: parameters}; + }, handler: async ( runtime: IAgentRuntime, message: Memory, @@ -59,14 +70,6 @@ export const airdrop: Action = { } const response = convertNullStrings(state.actionParameters); elizaLogger.log('Response:', response); - if (!response.programName) { - const responseMsg = { - text: 'Please tell me the program name of the airdrop', - action: 'CLAIM_AIRDROP', - }; - callback?.(responseMsg); - return 'pending'; - } const airdrops = await getAirdrops(runtime, message); if (!airdrops) { diff --git a/src/eliza/packages/plugin-solana/src/actions/analyze.ts b/src/eliza/packages/plugin-solana/src/actions/analyze.ts index 5721580..4f5e5aa 100644 --- a/src/eliza/packages/plugin-solana/src/actions/analyze.ts +++ b/src/eliza/packages/plugin-solana/src/actions/analyze.ts @@ -49,6 +49,20 @@ export const analyze: Action = { }, description: 'Analyze the token trade info, twitter binding and news about the token by given symbol or contract address', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + const formattedParameters = convertNullStrings(parameters) as any; + if (formattedParameters.tokenSymbol && !formattedParameters.tokenAddress) { + const tokens = await getTokensBySymbol(runtime, formattedParameters.tokenSymbol); + formattedParameters.tokenAddress = tokens?.[0]?.address; + } + if (!formattedParameters.tokenAddress) { + callback?.({ + text: `Please provide either token symbol or contract address to analyze.`, + }); + return {status: 'incomplete info', parameters: formattedParameters}; + } + return {status: 'success', parameters: formattedParameters}; + }, handler: async ( runtime: IAgentRuntime, message: Memory, @@ -59,18 +73,6 @@ export const analyze: Action = { let response = convertNullStrings(state.actionParameters) as any; elizaLogger.log('ANALYZE_TOKEN Response:', response); - if (response.tokenSymbol && !response.tokenAddress) { - const tokens = await getTokensBySymbol(runtime, response.tokenSymbol); - response.tokenAddress = tokens?.[0]?.address; - } - - if (!response.tokenAddress) { - callback?.({ - text: `Please provide either token symbol or contract address to analyze.`, - }); - return 'pending'; - } - const analyzeResult = await getTokenInfo( response.tokenSymbol, response.tokenAddress, diff --git a/src/eliza/packages/plugin-solana/src/actions/autoSwap.ts b/src/eliza/packages/plugin-solana/src/actions/autoSwap.ts index b0c9033..e90263a 100644 --- a/src/eliza/packages/plugin-solana/src/actions/autoSwap.ts +++ b/src/eliza/packages/plugin-solana/src/actions/autoSwap.ts @@ -51,6 +51,7 @@ export interface LimitOrderTask { priceCondition: 'below' | 'above' | null; targetPrice: number | null; targetToken: string | null; + pendingConfirmation: boolean | null; } @@ -134,6 +135,92 @@ export const autoTask: Action = { }, description: 'Perform auto token swap. Enables the agent to automatically execute trades when specified conditions are met, such as limit orders, scheduled transactions, or other custom triggers, optimizing trading strategies without manual intervention.', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + elizaLogger.log('parameters (formatParameters): ', parameters); + const formattedParameters = parameters as LimitOrderTask; + if (formattedParameters.inputTokenSymbol?.toUpperCase() === 'SOL') { + formattedParameters.inputTokenCA = NATIVE_MINT.toBase58(); + } + if (formattedParameters.outputTokenSymbol?.toUpperCase() === 'SOL') { + formattedParameters.outputTokenCA = NATIVE_MINT.toBase58(); + } + formattedParameters.inputTokenCA = validateAndAssignCA( + formattedParameters.inputTokenSymbol, + formattedParameters.inputTokenCA, + ); + formattedParameters.outputTokenCA = validateAndAssignCA( + formattedParameters.outputTokenSymbol, + formattedParameters.outputTokenCA, + ); + + formattedParameters.inputTokenCA = formattedParameters.inputTokenCA || + formattedParameters.inputTokenSymbol? await getTokenCABySymbol( + runtime, + formattedParameters.inputTokenSymbol, + ) : null; + formattedParameters.outputTokenCA = formattedParameters.outputTokenCA || + formattedParameters.outputTokenSymbol? await getTokenCABySymbol( + runtime, + formattedParameters.outputTokenSymbol, + ) : null; + formattedParameters.targetTokenCA = + (formattedParameters.targetToken === NATIVE_MINT.toBase58() ? NATIVE_MINT.toBase58() : null) || + formattedParameters.targetTokenCA || + (formattedParameters.targetToken === formattedParameters.inputTokenSymbol ? formattedParameters.inputTokenCA : null) || + (formattedParameters.targetToken === formattedParameters.outputTokenSymbol ? formattedParameters.outputTokenCA : null) || + formattedParameters.targetToken ? await getTokenCABySymbol(runtime, formattedParameters.targetToken) : null; + + if (!formattedParameters.inputTokenCA || !isValidSPLTokenAddress(formattedParameters.inputTokenCA)) { + callback?.({ + text: 'Please provide a valid inputToken CA you want to sell', + }); + return {status: 'incomplete info', parameters: parameters}; + } + + if (!formattedParameters.outputTokenCA || !isValidSPLTokenAddress(formattedParameters.outputTokenCA)) { + callback?.({ + text: 'Please provide a valid outputToken CA you want to buy', + }); + return {status: 'incomplete info', parameters: parameters}; + } + + if (!formattedParameters.targetTokenCA || !isValidSPLTokenAddress(formattedParameters.targetTokenCA)) { + callback?.({ + text: `Please specify which token's price you want to monitor: ${formattedParameters.inputTokenCA} or ${formattedParameters.outputTokenCA}?`, + }); + return {status: 'incomplete info', parameters: parameters}; + } + + if ( + Number.isFinite(formattedParameters.outputTokenAmount) && + formattedParameters.outputTokenAmount != 0 + ) { + callback?.({ + text: `Specify the buy amount of a token is not supported now, ${formattedParameters.outputTokenAmount} will be ignored.`, + }); + return {status: 'incomplete info', parameters: parameters}; + } + + if (!formattedParameters.targetPrice && !formattedParameters.delay) { + callback?.({ + text: "If you'd like to create an autotask, please specify the target price for the swap or provide a time delay, such as 'after 5 minutes' or 'below 0.00169' ", + }); + return {status: 'incomplete info', parameters: parameters}; + } + + const client = await getSolanaClient(runtime); + + if ( + Number.isFinite(formattedParameters.inputTokenPercentage) && + formattedParameters.inputTokenPercentage != 0 + ) { + const balance = await client.getUIBalance(formattedParameters.inputTokenCA); + formattedParameters.inputTokenAmount = balance * formattedParameters.inputTokenPercentage; + } + + + return {status: 'success', parameters: formattedParameters}; + }, handler: async ( runtime: IAgentRuntime, message: Memory, @@ -193,94 +280,10 @@ async function checkResponse( callback?.(NotAgentAdminResponse); return {status: 'rejected'}; } - - // generate formatted response from chat - let swapReq = convertNullStrings(state.actionParameters) as LimitOrderTask; - swapReq.inputTokenPercentage = Number(swapReq.inputTokenPercentage); - swapReq.inputTokenAmount = Number(swapReq.inputTokenAmount); - - elizaLogger.log(`Response:`, swapReq); - - if (swapReq.inputTokenSymbol?.toUpperCase() === 'SOL') { - swapReq.inputTokenCA = NATIVE_MINT.toBase58(); - } - if (swapReq.outputTokenSymbol?.toUpperCase() === 'SOL') { - swapReq.outputTokenCA = NATIVE_MINT.toBase58(); - } - swapReq.inputTokenCA = validateAndAssignCA( - swapReq.inputTokenSymbol, - swapReq.inputTokenCA, - ); - swapReq.outputTokenCA = validateAndAssignCA( - swapReq.outputTokenSymbol, - swapReq.outputTokenCA, - ); - - swapReq.inputTokenCA = swapReq.inputTokenCA || await getTokenCABySymbol( - runtime, - swapReq.inputTokenSymbol, - ); - swapReq.outputTokenCA = swapReq.outputTokenCA || await getTokenCABySymbol( - runtime, - swapReq.outputTokenSymbol, - ); - swapReq.targetTokenCA = swapReq.targetTokenCA || await getTokenCABySymbol( - runtime, - swapReq.targetToken, - ) || swapReq.targetToken === swapReq.inputTokenSymbol ? swapReq.inputTokenCA : swapReq.outputTokenCA; - - if (!swapReq.inputTokenCA || !isValidSPLTokenAddress(swapReq.inputTokenCA)) { - callback?.({ - text: 'Please provide a valid inputToken CA you want to sell', - }); - return {status: 'pending'}; - } - - if (!swapReq.outputTokenCA || !isValidSPLTokenAddress(swapReq.outputTokenCA)) { - callback?.({ - text: 'Please provide a valid outputToken CA you want to buy', - }); - return {status: 'pending'}; - } - - if (!swapReq.targetTokenCA || !isValidSPLTokenAddress(swapReq.targetTokenCA)) { - callback?.({ - text: `Please specify which token's price you want to monitor: ${swapReq.inputTokenCA} or ${swapReq.outputTokenCA}?`, - }); - return {status: 'pending'}; - } - - if ( - Number.isFinite(swapReq.outputTokenAmount) && - swapReq.outputTokenAmount != 0 - ) { - callback?.({ - text: `Specify the buy amount of a token is not supported now, ${swapReq.outputTokenAmount} will be ignored.`, - }); - return {status: 'pending'}; - } - + const swapReq = state.actionParameters as LimitOrderTask; + elizaLogger.info(`swapReq: ${JSON.stringify(swapReq)}`); + const client = await getSolanaClient(runtime); - - if ( - Number.isFinite(swapReq.inputTokenPercentage) && - swapReq.inputTokenPercentage != 0 - ) { - const balance = await client.getUIBalance(swapReq.inputTokenCA); - swapReq.inputTokenAmount = balance * swapReq.inputTokenPercentage; - } - - if ( - !Number.isFinite(swapReq.inputTokenAmount) || - swapReq.inputTokenAmount <= 0 - ) { - callback?.({ - text: `Please provide a valid ${swapReq.inputTokenSymbol} input amount to perform the swap`, - action: 'AUTO_TASK', - }); - return {status: 'pending'}; - } - const balance = await client.getUIBalance(swapReq.inputTokenCA); if (!balance) { callback?.({ @@ -289,7 +292,7 @@ async function checkResponse( return {status: 'failed'}; } - if (balance < swapReq.inputTokenAmount) { + if (balance < Number(swapReq.inputTokenAmount)) { callback?.({ text: `Insufficient balance for swap, required: ${swapReq.inputTokenAmount} but only ${balance} available.`, }); @@ -311,7 +314,7 @@ async function checkResponse( }); return {status: 'failed'}; } - } else if (WSOL_AMOUNT - swapReq.inputTokenAmount < GAS_BALANCE) { + } else if (WSOL_AMOUNT - Number(swapReq.inputTokenAmount) < GAS_BALANCE) { // buy with SOL const requiredAmount = GAS_BALANCE + Number(swapReq.inputTokenAmount); elizaLogger.error('Insufficient balance for swap gas fee'); @@ -323,13 +326,6 @@ async function checkResponse( return {status: 'failed'}; } - if (!swapReq.targetPrice && !swapReq.delay) { - callback?.({ - text: "If you'd like to create an autotask, please specify the target price for the swap or provide a time delay, such as 'after 5 minutes' or 'below 0.00169' ", - }); - return {status: 'pending'}; - } - if (swapReq.delay) { const getSecondsValue = (value: string): number | null => { const match = value.match(/^(\d+)s$/); @@ -345,28 +341,42 @@ async function checkResponse( elizaLogger.info(`checking if user confirm to create task`); - const confirmContext = composeContext({ - state, - template: userConfirmTemplate, - }); - - const confirmResponse = await generateObjectDeprecated({ - runtime, - context: confirmContext, - modelClass: ModelClass.LARGE, - }); - elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); + if (swapReq.pendingConfirmation === true) { + const confirmContext = composeContext({ + state, + template: userConfirmTemplate, + }); - if (confirmResponse.userAcked == 'rejected') { - callback?.({ - text: 'ok. I will not set the autotask.', - action: 'AUTO_TASK', + const confirmResponse = await generateObjectDeprecated({ + runtime, + context: confirmContext, + modelClass: ModelClass.LARGE, }); - return {status: 'cancelled'}; - } + elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); - if (confirmResponse.userAcked == 'pending') { - swapReq.inputTokenPercentage = (swapReq.inputTokenAmount/balance); + if (confirmResponse.userAcked == 'rejected') { + callback?.({ + text: 'ok. I will not set the autotask.', + action: 'AUTO_TASK', + }); + return {status: 'cancelled'}; + } else if (confirmResponse.userAcked == 'confirmed') { + return {status: 'success', task: swapReq}; + } else if (confirmResponse.userAcked == "pending") { + callback?.({ + text: "I repeatedly asked you to confirm the task although you have already confirmed it. It was my mistake. Please try again.", + action: "AUTO_TASK" + }); + return { status: "pending" }; + } else { + callback?.({ + text: "I failed to recognize your confirmation. Please try again.", + action: "AUTO_TASK" + }); + return { status: "failed" }; + } + } else { + swapReq.inputTokenPercentage = Number(swapReq.inputTokenAmount)/balance; const swapInfo = formatTaskInfo(swapReq); callback?.({ text: `${swapInfo}`, @@ -375,8 +385,6 @@ async function checkResponse( }); return {status: 'pending'}; } - - return {status: 'success', task: swapReq}; } diff --git a/src/eliza/packages/plugin-solana/src/actions/copyTrade.ts b/src/eliza/packages/plugin-solana/src/actions/copyTrade.ts index 3c4f344..a4fbe3b 100644 --- a/src/eliza/packages/plugin-solana/src/actions/copyTrade.ts +++ b/src/eliza/packages/plugin-solana/src/actions/copyTrade.ts @@ -27,6 +27,7 @@ type CopyTradeParameters = { walletAddress: string; expiredAt: number | undefined; agentId: string; + pendingConfirmation: boolean | null; }; const userConfirmTemplate = ` @@ -118,6 +119,35 @@ export const copyTrade: Action = { }, similes: [], description: 'Copy the trade of a given account', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + const formattedParameters = convertNullStrings(parameters) as CopyTradeParameters; + if (!formattedParameters.name) { + formattedParameters.name = `COPY_TRADE-${formattedParameters.walletAddress}`; + } + if (!isValidAddress(formattedParameters.targetAddress)) { + callback?.({ + text: `Please provide a valid wallet address to copy trade.`, + action: 'COPY_TRADE', + }); + return {status: 'incomplete info', parameters: formattedParameters}; + } + if (Number.isFinite(formattedParameters.fixedAmount) && formattedParameters.fixedAmount > 0) { + formattedParameters.mode = 'fixedAmount'; + } else if ( + Number.isFinite(formattedParameters.percentage) && + formattedParameters.percentage > 0 && + formattedParameters.percentage <= 1 + ) { + formattedParameters.mode = 'percentage'; + } else { + callback?.({ + text: `Please provide a valid input amount or percentage to copy trade.`, + action: 'COPY_TRADE', + }); + return {status: 'incomplete info', parameters: formattedParameters}; + } + return {status: 'success', parameters: formattedParameters}; + }, handler: async ( runtime: IAgentRuntime, message: Memory, @@ -136,33 +166,6 @@ export const copyTrade: Action = { state.actionParameters, ) as CopyTradeParameters; - if (!response.name) { - response.name = `COPY_TRADE-${response.walletAddress}`; - } - if (!isValidAddress(response.targetAddress)) { - callback?.({ - text: `Please provide a valid wallet address to copy trade.`, - action: 'COPY_TRADE', - }); - return 'pending'; - } - - if (Number.isFinite(response.fixedAmount) && response.fixedAmount > 0) { - response.mode = 'fixedAmount'; - } else if ( - Number.isFinite(response.percentage) && - response.percentage > 0 && - response.percentage <= 1 - ) { - response.mode = 'percentage'; - } else { - callback?.({ - text: `Please provide a valid input amount or percentage to copy trade.`, - action: 'COPY_TRADE', - }); - return 'pending'; - } - const wallet = await getWalletKey(runtime, true); response.walletAddress = wallet.keypair.publicKey.toBase58(); response.agentId = runtime.agentId; @@ -181,27 +184,60 @@ export const copyTrade: Action = { } elizaLogger.log('COPY_TRADE:', response); - const confirmContext = composeContext({ - state, - template: userConfirmTemplate, - }); + if (response.pendingConfirmation === true) { + const confirmContext = composeContext({ + state, + template: userConfirmTemplate, + }); - const confirmResponse = await generateObjectDeprecated({ - runtime, - context: confirmContext, - modelClass: ModelClass.LARGE, - }); - elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); + const confirmResponse = await generateObjectDeprecated({ + runtime, + context: confirmContext, + modelClass: ModelClass.LARGE, + }); + elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); - if (confirmResponse.userAcked == 'rejected') { - const responseMsg = { - text: 'ok. I will not set this.', - }; - callback?.(responseMsg); - return 'cancelled'; - } + if (confirmResponse.userAcked == 'rejected') { + const responseMsg = { + text: 'ok. I will not set this.', + }; + callback?.(responseMsg); + return 'cancelled'; + } else if (confirmResponse.userAcked == "pending") { + callback?.({ + text: "I repeatedly asked you to confirm the task although you have already confirmed it. It was my mistake. Please try again.", + action: "COPY_TRADE" + }); + return "pending"; + } else if (confirmResponse.userAcked == "confirmed") { + const { id } = await SharedProvider.get( + 'tradeMonitorService', + ).createCopyTrade({ + chain: response.chain, + targetAddress: response.targetAddress, + walletAddress: response.walletAddress, + expiredAt: response.expiredAt || 0, + }); + await runtime.databaseAdapter.insert?.('copyTrades', { + ...response, + id, + status: 'running', + createdAt: new Date(), + }); - if (confirmResponse.userAcked == 'pending') { + callback?.({ + text: `Copy trade created successfully.`, + action: `COPY_TRADE`, + }); + return 'success'; + } else { + callback?.({ + text: "I failed to recognize your confirmation. Please try again.", + action: "COPY_TRADE" + }); + return "failed"; + } + } else { const responseMsg = { text: `${formatConfirmMessage(response)}`, result: 'Pending user confirmation', @@ -211,26 +247,6 @@ export const copyTrade: Action = { return 'pending'; } - const { id } = await SharedProvider.get( - 'tradeMonitorService', - ).createCopyTrade({ - chain: response.chain, - targetAddress: response.targetAddress, - walletAddress: response.walletAddress, - expiredAt: response.expiredAt || 0, - }); - await runtime.databaseAdapter.insert?.('copyTrades', { - ...response, - id, - status: 'running', - createdAt: new Date(), - }); - - callback?.({ - text: `Copy trade created successfully.`, - action: `COPY_TRADE`, - }); - return 'success'; }, examples: [] as ActionExample[][], diff --git a/src/eliza/packages/plugin-solana/src/actions/pumpfun.ts b/src/eliza/packages/plugin-solana/src/actions/pumpfun.ts index b5e5ec7..7f3c5da 100644 --- a/src/eliza/packages/plugin-solana/src/actions/pumpfun.ts +++ b/src/eliza/packages/plugin-solana/src/actions/pumpfun.ts @@ -218,6 +218,45 @@ export default { }, description: 'Create a new token on pumpfun and buy a specified amount using SOL. Requires the token name, symbol and image url, buy amount after create in SOL.', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + elizaLogger.log('parameters (formatParameters): ', parameters); + const formattedParameters = parameters as any; + if (formattedParameters.symbol?.startsWith('$')) { + formattedParameters.symbol = formattedParameters.symbol.slice(1); + } + if (formattedParameters.name?.startsWith('$')) { + formattedParameters.name = formattedParameters.name.slice(1); + } + const { + name, + symbol, + imageUrl, + description, + twitter, + website, + telegram, + buyAmountSol, + } = formattedParameters; + if (!imageUrl || !fs.existsSync(imageUrl)) { + callback({ + text: `Please provide an image for the token.`, + }); + return {status: 'incomplete info', parameters: formattedParameters}; + } + if (!name) { + callback({ + text: `Please provide a name for the token.`, + }); + return {status: 'incomplete info', parameters: formattedParameters}; + } + if (!symbol) { + callback({ + text: `Please provide a symbol for the token.`, + }); + return {status: 'incomplete info', parameters: formattedParameters}; + } + return {status: 'success', parameters: formattedParameters}; + }, handler: async ( runtime: IAgentRuntime, message: Memory, @@ -232,12 +271,6 @@ export default { return 'rejected'; } const content = convertNullStrings(state.actionParameters) as any; - if (content.symbol?.startsWith('$')) { - content.symbol = content.symbol.slice(1); - } - if (content.name?.startsWith('$')) { - content.name = content.name.slice(1); - } elizaLogger.info('Generated content:', content); const { @@ -261,48 +294,143 @@ export default { elizaLogger.info( `Content for CREATE_AND_BUY_TOKEN action: ${JSON.stringify(content)}`, ); - if (!imageUrl || !fs.existsSync(imageUrl)) { - callback({ - text: `Please provide an image for the token.`, - }); - return 'pending'; - } - if (!name) { - callback({ - text: `Please provide a name for the token.`, - }); - return 'pending'; - } - if (!symbol) { - callback({ - text: `Please provide a symbol for the token.`, - }); - return 'pending'; - } elizaLogger.info(`checking if user confirm to execute`); - const confirmContext = composeContext({ - state, - template: userConfirmTemplate, - }); - - const confirmResponse = await generateObjectDeprecated({ - runtime, - context: confirmContext, - modelClass: ModelClass.LARGE, - }); - elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); - - if (confirmResponse.userAcked == 'rejected') { - const responseMsg = { - text: 'ok. I will cancel the task.', - }; - callback?.(responseMsg); - return 'cancelled'; - } + if (content.pendingConfirmation === true) { + const confirmContext = composeContext({ + state, + template: userConfirmTemplate, + }); - if (confirmResponse.userAcked == 'pending') { + const confirmResponse = await generateObjectDeprecated({ + runtime, + context: confirmContext, + modelClass: ModelClass.LARGE, + }); + elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); + + if (confirmResponse.userAcked == 'rejected') { + const responseMsg = { + text: 'ok. I will cancel the task.', + }; + callback?.(responseMsg); + return 'cancelled'; + } else if (confirmResponse.userAcked == "pending") { + callback?.({ + text: "I repeatedly asked you to confirm the task although you have already confirmed it. It was my mistake. Please try again.", + action: "CREATE_TOKEN" + }); + return "pending"; + } else if (confirmResponse.userAcked == "confirmed") { + const file = imageUrl ? await fs.openAsBlob(imageUrl) : null; + const fullTokenMetadata: CreateTokenMetadata = { + name: tokenMetadata.name, + symbol: tokenMetadata.symbol, + description: tokenMetadata.description, + twitter: tokenMetadata.twitter, + telegram: tokenMetadata.telegram, + website: tokenMetadata.website, + file: file, + }; + + // Default priority fee for high network load + const priorityFee = { + unitLimit: 500_000, + unitPrice: 200_000, + }; + const slippage = '400'; + + // Get private key from settings and create deployer keypair + const { keypair: deployerKeypair } = await getWalletKey(runtime, true); + elizaLogger.log(`deployer: ${deployerKeypair.publicKey.toBase58()}`); + // Generate new mint keypair + const mintKeypair = Keypair.generate(); + elizaLogger.log( + `Generated mint address: ${mintKeypair.publicKey.toBase58()}`, + ); + + // Setup connection and SDK + const rpcUrl = getRuntimeKey(runtime, 'SOLANA_RPC_URL'); + const connection = new Connection(rpcUrl, { + commitment: 'confirmed', + confirmTransactionInitialTimeout: 120000, // 120 seconds + wsEndpoint: settings.SOLANA_RPC_URL!.replace('https', 'wss'), + }); + + elizaLogger.log( + `rpc connection: ${rpcUrl}, ${deployerKeypair.publicKey.toBase58()}`, + ); + + const wallet = new Wallet(deployerKeypair); + + const provider: AnchorProvider = new AnchorProvider(connection, wallet, { + commitment: 'confirmed', + }); + const sdk = new PumpFunSDK(provider); + const lamports = Math.floor(Number(buyAmountSol) * LAMPORTS_PER_SOL); + + elizaLogger.log( + 'Executing create and buy transaction...', + deployerKeypair.publicKey, + mintKeypair.publicKey, + ); + if (!fullTokenMetadata.name) { + throw new Error('fullTokenMetadata Token name is required'); + } + + SharedProvider.get('tradeMonitorService').registerAgentCreatedToken({ + chain: 'solana', + address: mintKeypair.publicKey.toBase58(), + creatorAddress: deployerKeypair.publicKey.toBase58(), + nftId: getRuntimeKey(runtime, 'NFT_ID'), + }); + + const result = await createAndBuyToken({ + deployer: deployerKeypair, + mint: mintKeypair, + tokenMetadata: fullTokenMetadata, + buyAmountSol: BigInt(lamports), + priorityFee, + allowOffCurve: false, + sdk, + slippage, + }); + + if (result.success) { + callback({ + text: `Transaction submitted, please wait for confirmation.\nCheck token on: https://pump.fun/${mintKeypair.publicKey.toBase58()}\nTransaction hash: ${result.signature}`, + content: { + tokenInfo: { + symbol: tokenMetadata.symbol, + address: result.ca, + creator: result.creator, + name: tokenMetadata.name, + description: tokenMetadata.description, + timestamp: Date.now(), + }, + }, + }); + return 'success'; + } else { + callback({ + text: `Failed to create token: ${result.error}\nAttempted mint address: ${result.ca}`, + isError: true, + content: { + error: result.error, + mintAddress: result.ca, + }, + }); + return 'failed'; + } + } else { + callback?.({ + text: "I failed to recognize your confirmation. Please try again.", + action: "CREATE_TOKEN" + }); + return "failed"; + } + } else { const confirmMessage = formatCreateTokenInfo(content); const responseMsg = { text: `${confirmMessage}`, @@ -317,106 +445,6 @@ export default { callback?.(responseMsg); return 'pending'; } - const file = imageUrl ? await fs.openAsBlob(imageUrl) : null; - const fullTokenMetadata: CreateTokenMetadata = { - name: tokenMetadata.name, - symbol: tokenMetadata.symbol, - description: tokenMetadata.description, - twitter: tokenMetadata.twitter, - telegram: tokenMetadata.telegram, - website: tokenMetadata.website, - file: file, - }; - - // Default priority fee for high network load - const priorityFee = { - unitLimit: 500_000, - unitPrice: 200_000, - }; - const slippage = '400'; - - // Get private key from settings and create deployer keypair - const { keypair: deployerKeypair } = await getWalletKey(runtime, true); - elizaLogger.log(`deployer: ${deployerKeypair.publicKey.toBase58()}`); - // Generate new mint keypair - const mintKeypair = Keypair.generate(); - elizaLogger.log( - `Generated mint address: ${mintKeypair.publicKey.toBase58()}`, - ); - - // Setup connection and SDK - const rpcUrl = getRuntimeKey(runtime, 'SOLANA_RPC_URL'); - const connection = new Connection(rpcUrl, { - commitment: 'confirmed', - confirmTransactionInitialTimeout: 120000, // 120 seconds - wsEndpoint: settings.SOLANA_RPC_URL!.replace('https', 'wss'), - }); - - elizaLogger.log( - `rpc connection: ${rpcUrl}, ${deployerKeypair.publicKey.toBase58()}`, - ); - - const wallet = new Wallet(deployerKeypair); - - const provider: AnchorProvider = new AnchorProvider(connection, wallet, { - commitment: 'confirmed', - }); - const sdk = new PumpFunSDK(provider); - const lamports = Math.floor(Number(buyAmountSol) * LAMPORTS_PER_SOL); - - elizaLogger.log( - 'Executing create and buy transaction...', - deployerKeypair.publicKey, - mintKeypair.publicKey, - ); - if (!fullTokenMetadata.name) { - throw new Error('fullTokenMetadata Token name is required'); - } - - SharedProvider.get('tradeMonitorService').registerAgentCreatedToken({ - chain: 'solana', - address: mintKeypair.publicKey.toBase58(), - creatorAddress: deployerKeypair.publicKey.toBase58(), - nftId: getRuntimeKey(runtime, 'NFT_ID'), - }); - - const result = await createAndBuyToken({ - deployer: deployerKeypair, - mint: mintKeypair, - tokenMetadata: fullTokenMetadata, - buyAmountSol: BigInt(lamports), - priorityFee, - allowOffCurve: false, - sdk, - slippage, - }); - - if (result.success) { - callback({ - text: `Transaction submitted, please wait for confirmation.\nCheck token on: https://pump.fun/${mintKeypair.publicKey.toBase58()}\nTransaction hash: ${result.signature}`, - content: { - tokenInfo: { - symbol: tokenMetadata.symbol, - address: result.ca, - creator: result.creator, - name: tokenMetadata.name, - description: tokenMetadata.description, - timestamp: Date.now(), - }, - }, - }); - return 'success'; - } else { - callback({ - text: `Failed to create token: ${result.error}\nAttempted mint address: ${result.ca}`, - isError: true, - content: { - error: result.error, - mintAddress: result.ca, - }, - }); - return 'failed'; - } }, examples: [ diff --git a/src/eliza/packages/plugin-solana/src/actions/swap.ts b/src/eliza/packages/plugin-solana/src/actions/swap.ts index 3b3e520..8193379 100644 --- a/src/eliza/packages/plugin-solana/src/actions/swap.ts +++ b/src/eliza/packages/plugin-solana/src/actions/swap.ts @@ -39,6 +39,7 @@ interface SwapTokenRequest { inputTokenAmount: number | null; inputTokenPercentage: number | null; outputTokenAmount: number | null; + pendingConfirmation: boolean | null; } const userConfirmTemplate = ` @@ -148,6 +149,94 @@ export const executeSwap: Action = { }, description: 'Perform a token swap. buy or sell tokens, supports SOL and SPL tokens swaps.', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + elizaLogger.log('parameters (formatParameters): ', parameters); + const defaultValues: SwapTokenRequest = { + inputTokenSymbol: null, + inputTokenCA: null, + outputTokenSymbol: null, + outputTokenCA: null, + inputTokenAmount: null, + inputTokenPercentage: null, + outputTokenAmount: null, + pendingConfirmation: null + }; + const formattedParameters = { + ...defaultValues, + ...convertNullStrings(parameters) + } as SwapTokenRequest; + + formattedParameters.inputTokenAmount = Number(formattedParameters.inputTokenAmount); + formattedParameters.inputTokenPercentage = Number(formattedParameters.inputTokenPercentage); + formattedParameters.outputTokenAmount = Number(formattedParameters.outputTokenAmount); + + if (formattedParameters.inputTokenSymbol?.toUpperCase() === 'SOL') { + formattedParameters.inputTokenCA = getRuntimeKey(runtime, 'SOL_ADDRESS'); + } + if (formattedParameters.outputTokenSymbol?.toUpperCase() === 'SOL') { + formattedParameters.outputTokenCA = getRuntimeKey(runtime, 'SOL_ADDRESS'); + } + formattedParameters.inputTokenCA = validateAndAssignCA( + formattedParameters.inputTokenSymbol, + formattedParameters.inputTokenCA, + ); + formattedParameters.outputTokenCA = validateAndAssignCA( + formattedParameters.outputTokenSymbol, + formattedParameters.outputTokenCA, + ); + + if (!formattedParameters.inputTokenCA) { + formattedParameters.inputTokenCA = await getTokenCABySymbol( + runtime, + formattedParameters.inputTokenSymbol, + ); + if (!formattedParameters.inputTokenCA) { + const responseMsg = { + text: 'Please provide a valid inputToken CA you want to sell', + }; + callback?.(responseMsg); + return { status: 'incomplete info', parameters: formattedParameters}; + } + } + + if (!formattedParameters.outputTokenCA) { + formattedParameters.outputTokenCA = await getTokenCABySymbol( + runtime, + formattedParameters.outputTokenSymbol, + ); + if (!formattedParameters.outputTokenCA) { + const responseMsg = { + text: 'Please provide a valid outputToken CA you want to buy', + }; + callback?.(responseMsg); + return { status: 'incomplete info', parameters: formattedParameters}; + } + } + + const client = await getSolanaClient(runtime); + const programId = await client.getTokenProgramId(formattedParameters.inputTokenCA); + + if ( + !Number.isFinite(formattedParameters.inputTokenAmount) && + Number.isFinite(formattedParameters.inputTokenPercentage) && + formattedParameters.inputTokenPercentage != 0 + ) { + const balance = await client.getUIBalance(formattedParameters.inputTokenCA); + formattedParameters.inputTokenAmount = balance * formattedParameters.inputTokenPercentage; + } + + if ( + !Number.isFinite(formattedParameters.inputTokenAmount) || + formattedParameters.inputTokenAmount <= 0 + ) { + const responseMsg = { + text: `Please provide a valid ${formattedParameters.inputTokenSymbol} input amount or output amount to perform the swap`, + }; + callback?.(responseMsg); + return { status: 'incomplete info', parameters: formattedParameters}; + } + return {status: 'success', parameters: formattedParameters}; + }, handler: handleExecuteSwap, examples: [] as ActionExample[][], } as Action; @@ -234,54 +323,6 @@ async function checkResponse( // generate formatted response from chat let swapReq = convertNullStrings(state.actionParameters) as SwapTokenRequest; elizaLogger.log('Swap request:', swapReq); - swapReq.inputTokenPercentage = Number(swapReq.inputTokenPercentage); - swapReq.inputTokenAmount = Number(swapReq.inputTokenAmount); - swapReq.outputTokenAmount = Number(swapReq.outputTokenAmount); - - if (swapReq.inputTokenSymbol?.toUpperCase() === 'SOL') { - swapReq.inputTokenCA = getRuntimeKey(runtime, 'SOL_ADDRESS'); - } - if (swapReq.outputTokenSymbol?.toUpperCase() === 'SOL') { - swapReq.outputTokenCA = getRuntimeKey(runtime, 'SOL_ADDRESS'); - } - swapReq.inputTokenCA = validateAndAssignCA( - swapReq.inputTokenSymbol, - swapReq.inputTokenCA, - ); - swapReq.outputTokenCA = validateAndAssignCA( - swapReq.outputTokenSymbol, - swapReq.outputTokenCA, - ); - - if (!swapReq.inputTokenCA) { - swapReq.inputTokenCA = await getTokenCABySymbol( - runtime, - swapReq.inputTokenSymbol, - ); - if (!swapReq.inputTokenCA) { - const responseMsg = { - text: 'Please provide a valid inputToken CA you want to sell', - result: 'Pending inputToken CA', - }; - callback?.(responseMsg); - return { status: 'pending'}; - } - } - - if (!swapReq.outputTokenCA) { - swapReq.outputTokenCA = await getTokenCABySymbol( - runtime, - swapReq.outputTokenSymbol, - ); - if (!swapReq.outputTokenCA) { - const responseMsg = { - text: 'Please provide a valid outputToken CA you want to buy', - result: 'Pending outputToken CA', - }; - callback?.(responseMsg); - return { status: 'pending'}; - } - } const client = await getSolanaClient(runtime); const programId = await client.getTokenProgramId(swapReq.inputTokenCA); @@ -292,34 +333,10 @@ async function checkResponse( ) { callback?.({ text: `Specify the buy amount of a token is not supported now, ${swapReq.outputTokenAmount} will be ignored.`, - result: 'Pending outputToken Amount', }); return { status: 'pending'}; } - if ( - !Number.isFinite(swapReq.inputTokenAmount) && - Number.isFinite(swapReq.inputTokenPercentage) && - swapReq.inputTokenPercentage != 0 - - ) { - const balance = await client.getUIBalance(swapReq.inputTokenCA); - swapReq.inputTokenAmount = balance * swapReq.inputTokenPercentage; - } - - if ( - !Number.isFinite(swapReq.inputTokenAmount) || - swapReq.inputTokenAmount <= 0 - ) { - const responseMsg = { - text: `Please provide a valid ${swapReq.inputTokenSymbol} input amount or output amount to perform the swap`, - action: 'EXECUTE_SWAP', - result: 'Pending inputToken Amount', - }; - callback?.(responseMsg); - return { status: 'pending'}; - } - const balance = await client.getUIBalance(swapReq.inputTokenCA); if (!balance) { const responseMsg = { @@ -372,30 +389,47 @@ async function checkResponse( elizaLogger.info(`checking if user confirm to execute swap`); - const confirmContext = composeContext({ - state, - template: userConfirmTemplate, - }); - - const confirmResponse = await generateObjectDeprecated({ - runtime, - context: confirmContext, - modelClass: ModelClass.LARGE, - }); - elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); + if (swapReq.pendingConfirmation === true) { + const confirmContext = composeContext({ + state, + template: userConfirmTemplate, + }); - if (confirmResponse.userAcked == 'rejected') { - const responseMsg = { - text: 'ok. I will not execute this transaction.', - result: 'User rejected the swap', - action: 'EXECUTE_SWAP', - }; - callback?.(responseMsg); - return { status: 'cancelled'}; - } + const confirmResponse = await generateObjectDeprecated({ + runtime, + context: confirmContext, + modelClass: ModelClass.LARGE, + }); + elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); - if (confirmResponse.userAcked == 'pending') { - const swapInfo = formatConfirmSwapInfo({ + if (confirmResponse.userAcked == 'rejected') { + const responseMsg = { + text: 'ok. I will not execute this transaction.', + result: 'User rejected the swap', + action: 'EXECUTE_SWAP', + }; + callback?.(responseMsg); + return { status: 'cancelled'}; + } else if (confirmResponse.userAcked == "pending") { + callback?.({ + text: "I repeatedly asked you to confirm the task although you have already confirmed it. It was my mistake. Please try again.", + action: "EXECUTE_SWAP" + }); + return { status: "pending" }; + } else if (confirmResponse.userAcked == "confirmed") { + return { + status: 'success', + parameters: { ...swapReq, programId }, + }; + } else { + callback?.({ + text: "I failed to recognize your confirmation. Please try again.", + action: "EXECUTE_SWAP" + }); + return { status: "failed" }; + } + } else { + const swapInfo = formatConfirmSwapInfo({ inputTokenSymbol: swapReq.inputTokenSymbol, inputTokenCA: swapReq.inputTokenCA, outputTokenSymbol: swapReq.outputTokenSymbol, @@ -411,11 +445,6 @@ async function checkResponse( callback?.(responseMsg); return { status: 'pending'}; } - - return { - status: 'success', - parameters: { ...swapReq, programId }, - }; } function formatConfirmSwapInfo(params: { diff --git a/src/eliza/packages/plugin-solana/src/actions/transfer.ts b/src/eliza/packages/plugin-solana/src/actions/transfer.ts index 53e45c7..50a7237 100644 --- a/src/eliza/packages/plugin-solana/src/actions/transfer.ts +++ b/src/eliza/packages/plugin-solana/src/actions/transfer.ts @@ -46,6 +46,7 @@ export interface TransferContent extends Content { tokenSymbol: string | null; recipient: string; amount: number | null; + pendingConfirmation: boolean | null; } const userConfirmTemplate = ` @@ -129,82 +130,255 @@ export const transfer: Action = { }, description: "Transfer SPL tokens or SOL from agent's wallet to another address, aka [send |withdraw|transfer] [amount] [tokenSymbol] [tokenCA] to [address] ", - handler: async ( - runtime: IAgentRuntime, - message: Memory, - state: State, - _options: { [key: string]: unknown }, - callback?: HandlerCallback, - ): Promise => { - const isAdmin = await isAgentAdmin(runtime, message); - if (!isAdmin) { - callback?.(NotAgentAdminResponse); - return 'rejected'; - } - const content = convertNullStrings( - state.actionParameters, - ) as TransferContent; - - if (!content.amount || isNaN(content.amount as number)) { + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + const formattedParameters = convertNullStrings(parameters) as TransferContent; + if (!formattedParameters.amount || isNaN(formattedParameters.amount as number)) { callback({ text: `Please provide the amount of tokens to transfer`, }); - return 'pending'; + return { status: 'incomplete info', parameters: formattedParameters}; } - if (!content.recipient) { + if (!formattedParameters.recipient) { callback({ text: `Please provide the address to transfer the tokens to`, }); - return 'pending'; + return { status: 'incomplete info', parameters: formattedParameters}; } - if ((!content.tokenAddress || content.tokenAddress === NATIVE_MINT.toBase58()) && content.tokenSymbol?.toUpperCase() === 'SOL') { - content.tokenAddress = STANDARD_SOL_ADDRESS; + if ((!formattedParameters.tokenAddress || formattedParameters.tokenAddress === NATIVE_MINT.toBase58()) && formattedParameters.tokenSymbol?.toUpperCase() === 'SOL') { + formattedParameters.tokenAddress = STANDARD_SOL_ADDRESS; } const { keypair: senderKeypair } = await getWalletKey(runtime, true); - if (!content.tokenAddress) { + if (!formattedParameters.tokenAddress) { const walletToken = await getWalletTokenBySymbol( runtime, senderKeypair.publicKey.toBase58(), - content.tokenSymbol, + formattedParameters.tokenSymbol, ); - content.tokenAddress = walletToken?.address; - if (!content.tokenAddress) { + formattedParameters.tokenAddress = walletToken?.address; + if (!formattedParameters.tokenAddress) { callback({ text: `Please provide the token CA to transfer`, }); - return 'pending'; + return { status: 'incomplete info', parameters: formattedParameters}; } } + return { status: 'success', parameters: formattedParameters}; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback, + ): Promise => { + const isAdmin = await isAgentAdmin(runtime, message); + if (!isAdmin) { + callback?.(NotAgentAdminResponse); + return 'rejected'; + } + const content = convertNullStrings( + state.actionParameters, + ) as TransferContent; const confirmContext = composeContext({ state, template: userConfirmTemplate, }); - const confirmResponse = await generateObjectDeprecated({ - runtime, - context: confirmContext, - modelClass: ModelClass.LARGE, - }); - elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); - - if (confirmResponse.userAcked == 'rejected') { - const responseMsg = { - text: 'ok. I will not execute this transaction.', - }; - callback?.(responseMsg); - return 'success'; - } - + const { keypair: senderKeypair } = await getWalletKey(runtime, true); const solanaClient = new SolanaClient( - getRuntimeKey(runtime, 'SOLANA_RPC_URL'), - senderKeypair.publicKey, + getRuntimeKey(runtime, 'SOLANA_RPC_URL'), + senderKeypair.publicKey, ); - if (confirmResponse.userAcked == 'pending') { + + if (content.pendingConfirmation === true) { + const confirmResponse = await generateObjectDeprecated({ + runtime, + context: confirmContext, + modelClass: ModelClass.LARGE, + }); + elizaLogger.info(`User confirm check: ${JSON.stringify(confirmResponse)}`); + + if (confirmResponse.userAcked == 'rejected') { + const responseMsg = { + text: 'ok. I will not execute this transaction.', + }; + callback?.(responseMsg); + return 'success'; + } else if (confirmResponse.userAcked == "pending") { + callback?.({ + text: "I repeatedly asked you to confirm the task although you have already confirmed it. It was my mistake. Please try again.", + action: "SEND_TOKEN" + }); + return "pending"; + } else if (confirmResponse.userAcked == "confirmed") { + try { + elizaLogger.log( + `${senderKeypair.publicKey.toBase58()} start transfer content:`, + content, + ); + + const connection = new Connection( + getRuntimeKey(runtime, 'SOLANA_RPC_URL'), + 'confirmed', + ); + const mintPubkey = new PublicKey(content.tokenAddress); + const recipientPubkey = new PublicKey(content.recipient); + + const mintDecimals = await solanaClient.getMintDecimals( + content.tokenAddress, + ); + if (!mintDecimals || isNaN(mintDecimals)) { + callback({ + text: `Token ${content.tokenAddress} not found. Please provide a valid token address.`, + }); + return 'pending'; + } + const mintAmount = BigInt( + new BigNumber(content.amount) + .multipliedBy(new BigNumber(10).pow(mintDecimals)) + .toFixed(0), + ); + + const solBalance = await connection.getBalance(senderKeypair.publicKey); + let solTransferOut = + content.tokenAddress === STANDARD_SOL_ADDRESS ? Number(mintAmount) : 0; + let estimatedFee = 0.001 * LAMPORTS_PER_SOL; + const rentExemption = + await connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE); + const transaction = new Transaction(); + if (content.tokenAddress === STANDARD_SOL_ADDRESS) { + if (solBalance < (solTransferOut + estimatedFee + rentExemption)) { + callback({ + text: `Insufficient sol balance. Sender has ${solBalance / LAMPORTS_PER_SOL} SOL, but tx needs ${(estimatedFee + solTransferOut + rentExemption) / LAMPORTS_PER_SOL} SOL to complete the transfer.(${rentExemption / LAMPORTS_PER_SOL} SOL for account rent exemption, ${estimatedFee / LAMPORTS_PER_SOL} SOL for transaction fee)`, + }); + return 'failed'; + } + transaction.add( + SystemProgram.transfer({ + fromPubkey: senderKeypair.publicKey, + toPubkey: recipientPubkey, + lamports: mintAmount, + }), + ); + } else { + const programId = await solanaClient.getTokenProgramId( + content.tokenAddress, + ); + const senderATA = getAssociatedTokenAddressSync( + mintPubkey, + senderKeypair.publicKey, + true, + programId, + ); + const recipientATA = getAssociatedTokenAddressSync( + mintPubkey, + recipientPubkey, + false, + programId, + ); + const recipientATAInfo = await connection.getAccountInfo(recipientATA); + const rentExemptAmount = recipientATAInfo + ? 0 + : await connection.getMinimumBalanceForRentExemption(165); + solTransferOut += rentExemptAmount; + if (solBalance < solTransferOut) { + callback({ + text: `Insufficient sol balance. Sender has ${solBalance / LAMPORTS_PER_SOL} SOL, but tx needs ${solTransferOut / LAMPORTS_PER_SOL} SOL to complete the transfer.`, + }); + return 'failed'; + } + const senderTokenBalance = + await connection.getTokenAccountBalance(senderATA); + if ( + BigInt(senderTokenBalance.value.amount) < + BigInt(mintAmount.toString()) + ) { + callback({ + text: `Insufficient token balance. Sender has ${senderTokenBalance.value.uiAmount} ${content.tokenSymbol}, but needs ${content.amount} to complete the transfer.`, + }); + return 'failed'; + } + const instructions = []; + if (!recipientATAInfo) { + instructions.push( + createAssociatedTokenAccountInstruction( + senderKeypair.publicKey, + recipientATA, + recipientPubkey, + mintPubkey, + programId, + ), + ); + } + + instructions.push( + createTransferInstruction( + senderATA, + recipientATA, + senderKeypair.publicKey, + mintAmount, + [], + programId, + ), + ); + transaction.add(...instructions); + } + const recentBlockhash = await connection.getLatestBlockhash('confirmed'); + transaction.feePayer = senderKeypair.publicKey; + transaction.recentBlockhash = recentBlockhash.blockhash; + estimatedFee = await transaction.getEstimatedFee(connection); + if (solBalance < solTransferOut + estimatedFee + rentExemption) { + callback({ + text: `Insufficient sol balance. Sender has ${solBalance / LAMPORTS_PER_SOL} SOL, but tx needs ${(estimatedFee + solTransferOut + rentExemption) / LAMPORTS_PER_SOL} SOL to complete the transfer.(${rentExemption / LAMPORTS_PER_SOL} SOL for account rent exemption, ${estimatedFee / LAMPORTS_PER_SOL} SOL for transaction fee)`, + }); + return 'failed'; + } + const signature = await sendAndConfirmTransaction( + connection, + transaction, + [senderKeypair], + { + commitment: 'confirmed', + maxRetries: 10, + preflightCommitment: 'confirmed', + }, + ); + + if (callback) { + callback({ + text: `Successfully sent ${content.amount} ${content.tokenSymbol || content.tokenAddress} to ${content.recipient}.\n\nTransaction hash: ${signature}`, + content: { + success: true, + signature, + amount: content.amount, + recipient: content.recipient, + }, + }); + } + return 'success'; + } catch (error) { + elizaLogger.error('Error during token transfer:', error); + if (callback) { + callback({ + text: `Issue with the transfer: ${error.message}`, + content: { error: error.message }, + }); + } + return 'failed'; + } + } else { + callback?.({ + text: "I failed to recognize your confirmation. Please try again.", + action: "SEND_TOKEN" + }); + return "failed"; + } + } else { const balance = await solanaClient.getUIBalance(content.tokenAddress); const transferPercentage = ( (Number(content.amount) / balance) * @@ -225,164 +399,8 @@ export const transfer: Action = { callback?.(responseMsg); return null; } - - try { - elizaLogger.log( - `${senderKeypair.publicKey.toBase58()} start transfer content:`, - content, - ); - - const connection = new Connection( - getRuntimeKey(runtime, 'SOLANA_RPC_URL'), - 'confirmed', - ); - const mintPubkey = new PublicKey(content.tokenAddress); - const recipientPubkey = new PublicKey(content.recipient); - - const mintDecimals = await solanaClient.getMintDecimals( - content.tokenAddress, - ); - if (!mintDecimals || isNaN(mintDecimals)) { - callback({ - text: `Token ${content.tokenAddress} not found. Please provide a valid token address.`, - }); - return 'pending'; - } - const mintAmount = BigInt( - new BigNumber(content.amount) - .multipliedBy(new BigNumber(10).pow(mintDecimals)) - .toFixed(0), - ); - - const solBalance = await connection.getBalance(senderKeypair.publicKey); - let solTransferOut = - content.tokenAddress === STANDARD_SOL_ADDRESS ? Number(mintAmount) : 0; - let estimatedFee = 0.001 * LAMPORTS_PER_SOL; - const rentExemption = - await connection.getMinimumBalanceForRentExemption(ACCOUNT_SIZE); - const transaction = new Transaction(); - if (content.tokenAddress === STANDARD_SOL_ADDRESS) { - if (solBalance < (solTransferOut + estimatedFee + rentExemption)) { - callback({ - text: `Insufficient sol balance. Sender has ${solBalance / LAMPORTS_PER_SOL} SOL, but tx needs ${(estimatedFee + solTransferOut + rentExemption) / LAMPORTS_PER_SOL} SOL to complete the transfer.(${rentExemption / LAMPORTS_PER_SOL} SOL for account rent exemption, ${estimatedFee / LAMPORTS_PER_SOL} SOL for transaction fee)`, - }); - return 'failed'; - } - transaction.add( - SystemProgram.transfer({ - fromPubkey: senderKeypair.publicKey, - toPubkey: recipientPubkey, - lamports: mintAmount, - }), - ); - } else { - const programId = await solanaClient.getTokenProgramId( - content.tokenAddress, - ); - const senderATA = getAssociatedTokenAddressSync( - mintPubkey, - senderKeypair.publicKey, - true, - programId, - ); - const recipientATA = getAssociatedTokenAddressSync( - mintPubkey, - recipientPubkey, - false, - programId, - ); - const recipientATAInfo = await connection.getAccountInfo(recipientATA); - const rentExemptAmount = recipientATAInfo - ? 0 - : await connection.getMinimumBalanceForRentExemption(165); - solTransferOut += rentExemptAmount; - if (solBalance < solTransferOut) { - callback({ - text: `Insufficient sol balance. Sender has ${solBalance / LAMPORTS_PER_SOL} SOL, but tx needs ${solTransferOut / LAMPORTS_PER_SOL} SOL to complete the transfer.`, - }); - return 'failed'; - } - const senderTokenBalance = - await connection.getTokenAccountBalance(senderATA); - if ( - BigInt(senderTokenBalance.value.amount) < - BigInt(mintAmount.toString()) - ) { - callback({ - text: `Insufficient token balance. Sender has ${senderTokenBalance.value.uiAmount} ${content.tokenSymbol}, but needs ${content.amount} to complete the transfer.`, - }); - return 'failed'; - } - const instructions = []; - if (!recipientATAInfo) { - instructions.push( - createAssociatedTokenAccountInstruction( - senderKeypair.publicKey, - recipientATA, - recipientPubkey, - mintPubkey, - programId, - ), - ); - } - - instructions.push( - createTransferInstruction( - senderATA, - recipientATA, - senderKeypair.publicKey, - mintAmount, - [], - programId, - ), - ); - transaction.add(...instructions); - } - const recentBlockhash = await connection.getLatestBlockhash('confirmed'); - transaction.feePayer = senderKeypair.publicKey; - transaction.recentBlockhash = recentBlockhash.blockhash; - estimatedFee = await transaction.getEstimatedFee(connection); - if (solBalance < solTransferOut + estimatedFee + rentExemption) { - callback({ - text: `Insufficient sol balance. Sender has ${solBalance / LAMPORTS_PER_SOL} SOL, but tx needs ${(estimatedFee + solTransferOut + rentExemption) / LAMPORTS_PER_SOL} SOL to complete the transfer.(${rentExemption / LAMPORTS_PER_SOL} SOL for account rent exemption, ${estimatedFee / LAMPORTS_PER_SOL} SOL for transaction fee)`, - }); - return 'failed'; - } - const signature = await sendAndConfirmTransaction( - connection, - transaction, - [senderKeypair], - { - commitment: 'confirmed', - maxRetries: 10, - preflightCommitment: 'confirmed', - }, - ); - - if (callback) { - callback({ - text: `Successfully sent ${content.amount} ${content.tokenSymbol || content.tokenAddress} to ${content.recipient}.\n\nTransaction hash: ${signature}`, - content: { - success: true, - signature, - amount: content.amount, - recipient: content.recipient, - }, - }); - } - return 'success'; - } catch (error) { - elizaLogger.error('Error during token transfer:', error); - if (callback) { - callback({ - text: `Issue with the transfer: ${error.message}`, - content: { error: error.message }, - }); - } - return 'failed'; - } }, - + examples: [] as ActionExample[][], } as Action; diff --git a/src/eliza/packages/plugin-solana/src/actions/wallet.ts b/src/eliza/packages/plugin-solana/src/actions/wallet.ts index 73a65e4..0b5c173 100644 --- a/src/eliza/packages/plugin-solana/src/actions/wallet.ts +++ b/src/eliza/packages/plugin-solana/src/actions/wallet.ts @@ -51,6 +51,10 @@ export const walletPortfolio: Action = { }, description: 'Get the wallet total balance or specific token balance in agent wallet', + formatParameters: async (runtime: IAgentRuntime, parameters: any, callback?: HandlerCallback) => { + const formattedParameters = convertNullStrings(parameters) as any; + return { status: 'success', parameters: formattedParameters}; + }, handler: async ( runtime: IAgentRuntime, message: Memory, @@ -85,7 +89,6 @@ export const walletPortfolio: Action = { }); return 'failed'; } - return 'success'; }, examples: [