From 57e2bb03e0796cd124c0186ca7c2bb694f25b84c Mon Sep 17 00:00:00 2001 From: moon Date: Mon, 4 Nov 2024 03:27:35 -0800 Subject: [PATCH] marc swap by CA --- README.md | 2 +- core/.env.example | 2 +- core/src/actions/swap.ts | 294 ++++++++++++++++++++++++++----- core/src/actions/swapDao.ts | 41 +---- core/src/actions/swapUtils.ts | 46 ++++- core/src/cli/index.ts | 7 +- core/src/clients/direct/index.ts | 27 ++- core/src/core/generation.ts | 36 ++++ core/src/core/parsing.ts | 3 +- core/src/index.ts | 4 +- core/src/providers/token.ts | 2 +- docs/docs/api/index.md | 2 +- 12 files changed, 364 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index c35d4fd431..818abfb9f3 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ XAI_MODEL= # For asking Claude stuff ANTHROPIC_API_KEY= -WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY +WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY BIRDEYE_API_KEY= diff --git a/core/.env.example b/core/.env.example index dacbc26f9e..a26bd8b87d 100644 --- a/core/.env.example +++ b/core/.env.example @@ -30,7 +30,7 @@ XAI_MODEL= # For asking Claude stuff ANTHROPIC_API_KEY= -WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY +WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY BIRDEYE_API_KEY= diff --git a/core/src/actions/swap.ts b/core/src/actions/swap.ts index a8540afb42..9cc480654f 100644 --- a/core/src/actions/swap.ts +++ b/core/src/actions/swap.ts @@ -1,45 +1,130 @@ -import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import { Connection, Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; import fetch from "cross-fetch"; import { ActionExample, IAgentRuntime, Memory, type Action, + State, + ModelClass, + HandlerCallback } from "../core/types.ts"; +import { walletProvider } from "../providers/wallet.ts"; +import { composeContext } from "../core/context.ts"; +import { generateObject, generateObjectArray } from "../core/generation.ts"; +import { getTokenDecimals } from "./swapUtils.ts"; +import settings from "../core/settings.ts"; +import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes/index.js"; async function swapToken( connection: Connection, walletPublicKey: PublicKey, - inputTokenSymbol: string, - outputTokenSymbol: string, + inputTokenCA: string, + outputTokenCA: string, amount: number ): Promise { - const quoteResponse = await fetch( - `https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenSymbol}&outputMint=${outputTokenSymbol}&amount=${amount * 10 ** 6}&slippageBps=50` - ); - const quoteData = await quoteResponse.json(); - - const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - quoteResponse: quoteData.data, + try { + // Get the decimals for the input token + const decimals = inputTokenCA === settings.SOL_ADDRESS ? 9 : + await getTokenDecimals(connection, inputTokenCA); + + console.log("Decimals:", decimals); + + const adjustedAmount = amount * (10 ** decimals); + + console.log("Fetching quote with params:", { + inputMint: inputTokenCA, + outputMint: outputTokenCA, + amount: adjustedAmount + }); + + const quoteResponse = await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${inputTokenCA}&outputMint=${outputTokenCA}&amount=${adjustedAmount}&slippageBps=50` + ); + const quoteData = await quoteResponse.json(); + + if (!quoteData || quoteData.error) { + console.error("Quote error:", quoteData); + throw new Error(`Failed to get quote: ${quoteData?.error || 'Unknown error'}`); + } + + console.log("Quote received:", quoteData); + + const swapRequestBody = { + quoteResponse: quoteData, userPublicKey: walletPublicKey.toString(), wrapAndUnwrapSol: true, - }), - }); + computeUnitPriceMicroLamports: 1000, + dynamicComputeUnitLimit: true + }; + + console.log("Requesting swap with body:", swapRequestBody); + + const swapResponse = await fetch("https://quote-api.jup.ag/v6/swap", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(swapRequestBody) + }); + + const swapData = await swapResponse.json(); + + if (!swapData || !swapData.swapTransaction) { + console.error("Swap error:", swapData); + throw new Error(`Failed to get swap transaction: ${swapData?.error || 'No swap transaction returned'}`); + } + + console.log("Swap transaction received"); + return swapData; - return await swapResponse.json(); + } catch (error) { + console.error("Error in swapToken:", error); + throw error; + } } -async function promptConfirmation(): Promise { - // Implement your own confirmation logic here - // This is just a placeholder example - const confirmSwap = window.confirm("Confirm the token swap?"); - return confirmSwap; + +const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "inputTokenSymbol": "SOL", + "outputTokenSymbol": "USDC", + "inputTokenCA": "So11111111111111111111111111111111111111112", + "outputTokenCA": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "amount": 1.5 +} +\`\`\` + +{{recentMessages}} + +Given the recent messages and wallet information below: + +{{walletInfo}} + +Extract the following information about the requested token swap: +- Input token symbol (the token being sold) +- Output token symbol (the token being bought) +- Input token contract address if provided +- Output token contract address if provided +- Amount to swap + +Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. The result should be a valid JSON object with the following schema: +\`\`\`json +{ + "inputTokenSymbol": string | null, + "outputTokenSymbol": string | null, + "inputTokenCA": string | null, + "outputTokenCA": string | null, + "amount": number | string | null } +\`\`\``; + +// if we get the token symbol but not the CA, check walet for matching token, and if we have, get the CA for it + +// swapToken should took CA, not symbol export const executeSwap: Action = { name: "EXECUTE_SWAP", @@ -52,10 +137,73 @@ export const executeSwap: Action = { description: "Perform a token swap.", handler: async ( runtime: IAgentRuntime, - message: Memory + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback ): Promise => { - const { inputTokenSymbol, outputTokenSymbol, amount } = message.content; + + // composeState + if (!state) { + state = (await runtime.composeState(message)) as State; + } else { + state = await runtime.updateRecentMessageState(state); + } + + const walletInfo = await walletProvider.get(runtime, message, state); + + state.walletInfo = walletInfo; + + const swapContext = composeContext({ + state, + template: swapTemplate, + }); + const response = await generateObject({ + runtime, + context: swapContext, + modelClass: ModelClass.LARGE, + }); + + console.log("Response:", response); + + // Add SOL handling logic + if (response.inputTokenSymbol?.toUpperCase() === 'SOL') { + response.inputTokenCA = settings.SOL_ADDRESS; + } + if (response.outputTokenSymbol?.toUpperCase() === 'SOL') { + response.outputTokenCA = settings.SOL_ADDRESS; + } + + // if both contract addresses are set, lets execute the swap + // TODO: try to resolve CA from symbol based on existing symbol in wallet + if (!response.inputTokenCA || !response.outputTokenCA) { + console.log("No contract addresses provided, skipping swap"); + const responseMsg = { + text: "I need the contract addresses to perform the swap", + }; + callback?.(responseMsg); + return true; + } + + if (!response.amount) { + console.log("No amount provided, skipping swap"); + const responseMsg = { + text: "I need the amount to perform the swap", + }; + callback?.(responseMsg); + return true; + } + + // TODO: if response amount is half, all, etc, semantically retrieve amount and return as number + if (!response.amount) { + console.log("Amount is not a number, skipping swap"); + const responseMsg = { + text: "The amount must be a number", + }; + callback?.(responseMsg); + return true; + } try { const connection = new Connection( "https://api.mainnet-beta.solana.com" @@ -64,40 +212,94 @@ export const executeSwap: Action = { runtime.getSetting("WALLET_PUBLIC_KEY") ); + console.log("Wallet Public Key:", walletPublicKey); + console.log("inputTokenSymbol:", response.inputTokenCA); + console.log("outputTokenSymbol:", response.outputTokenCA); + console.log("amount:", response.amount); + const swapResult = await swapToken( connection, walletPublicKey, - inputTokenSymbol as string, - outputTokenSymbol as string, - amount as number + response.inputTokenCA as string, + response.outputTokenCA as string, + response.amount as number ); - console.log("Swap Quote:"); - console.log(swapResult.quote); + console.log("Deserializing transaction..."); + const transactionBuf = Buffer.from(swapResult.swapTransaction, "base64"); + const transaction = VersionedTransaction.deserialize(transactionBuf); + + console.log("Preparing to sign transaction..."); + const privateKeyString = runtime.getSetting("WALLET_PRIVATE_KEY"); + + // Handle different private key formats + let secretKey: Uint8Array; + try { + // First try to decode as base58 + secretKey = bs58.decode(privateKeyString); + } catch (e) { + try { + // If that fails, try base64 + secretKey = Uint8Array.from(Buffer.from(privateKeyString, 'base64')); + } catch (e2) { + throw new Error('Invalid private key format'); + } + } - const confirmSwap = await promptConfirmation(); - if (!confirmSwap) { - console.log("Swap canceled by user"); - return false; + // Verify the key length + if (secretKey.length !== 64) { + console.error("Invalid key length:", secretKey.length); + throw new Error(`Invalid private key length: ${secretKey.length}. Expected 64 bytes.`); } - const transaction = Transaction.from( - Buffer.from(swapResult.swapTransaction, "base64") - ); - const privateKey = runtime.getSetting("WALLET_PRIVATE_KEY"); - const keypair = Keypair.fromSecretKey( - Uint8Array.from(Buffer.from(privateKey, "base64")) - ); - transaction.sign(keypair); + console.log("Creating keypair..."); + const keypair = Keypair.fromSecretKey(secretKey); + + // Verify the public key matches what we expect + const expectedPublicKey = runtime.getSetting("WALLET_PUBLIC_KEY"); + if (keypair.publicKey.toBase58() !== expectedPublicKey) { + throw new Error("Generated public key doesn't match expected public key"); + } - const txid = await connection.sendRawTransaction( - transaction.serialize() - ); - await connection.confirmTransaction(txid); + console.log("Signing transaction..."); + transaction.sign([keypair]); + + console.log("Sending transaction..."); + + const latestBlockhash = await connection.getLatestBlockhash(); + + const txid = await connection.sendTransaction(transaction, { + skipPreflight: false, + maxRetries: 3, + preflightCommitment: 'confirmed' + }); + + console.log("Transaction sent:", txid); + + // Confirm transaction using the blockhash + const confirmation = await connection.confirmTransaction({ + signature: txid, + blockhash: latestBlockhash.blockhash, + lastValidBlockHeight: latestBlockhash.lastValidBlockHeight + }, 'confirmed'); + + if (confirmation.value.err) { + throw new Error(`Transaction failed: ${confirmation.value.err}`); + } + + if (confirmation.value.err) { + throw new Error(`Transaction failed: ${confirmation.value.err}`); + } console.log("Swap completed successfully!"); console.log(`Transaction ID: ${txid}`); + const responseMsg = { + text: `Swap completed successfully! Transaction ID: ${txid}`, + }; + + callback?.(responseMsg); + return true; } catch (error) { console.error("Error during token swap:", error); diff --git a/core/src/actions/swapDao.ts b/core/src/actions/swapDao.ts index 07651320eb..18d58187b5 100644 --- a/core/src/actions/swapDao.ts +++ b/core/src/actions/swapDao.ts @@ -6,46 +6,7 @@ import { Memory, type Action, } from "../core/types.ts"; - -async function getTokenDecimals( - connection: Connection, - mintAddress: string -): Promise { - const mintPublicKey = new PublicKey(mintAddress); - const tokenAccountInfo = - await connection.getParsedAccountInfo(mintPublicKey); - - // Check if the data is parsed and contains the expected structure - if ( - tokenAccountInfo.value && - typeof tokenAccountInfo.value.data === "object" && - "parsed" in tokenAccountInfo.value.data - ) { - const parsedInfo = tokenAccountInfo.value.data.parsed?.info; - if (parsedInfo && typeof parsedInfo.decimals === "number") { - return parsedInfo.decimals; - } - } - - throw new Error("Unable to fetch token decimals"); -} - -async function getQuote( - connection: Connection, - baseToken: string, - outputToken: string, - amount: number -): Promise { - const decimals = await getTokenDecimals(connection, baseToken); - const adjustedAmount = amount * 10 ** decimals; - - const quoteResponse = await fetch( - `https://quote-api.jup.ag/v6/quote?inputMint=${baseToken}&outputMint=${outputToken}&amount=${adjustedAmount}&slippageBps=50` - ); - const swapTransaction = await quoteResponse.json(); - const swapTransactionBuf = Buffer.from(swapTransaction, "base64"); - return new Uint8Array(swapTransactionBuf); -} +import { getQuote } from "./swapUtils.ts"; async function invokeSwapDao( connection: Connection, diff --git a/core/src/actions/swapUtils.ts b/core/src/actions/swapUtils.ts index ceceea7d26..8439b1a691 100644 --- a/core/src/actions/swapUtils.ts +++ b/core/src/actions/swapUtils.ts @@ -26,6 +26,46 @@ export async function delayedCall( return method(...args); } +export async function getTokenDecimals( + connection: Connection, + mintAddress: string +): Promise { + const mintPublicKey = new PublicKey(mintAddress); + const tokenAccountInfo = + await connection.getParsedAccountInfo(mintPublicKey); + + // Check if the data is parsed and contains the expected structure + if ( + tokenAccountInfo.value && + typeof tokenAccountInfo.value.data === "object" && + "parsed" in tokenAccountInfo.value.data + ) { + const parsedInfo = tokenAccountInfo.value.data.parsed?.info; + if (parsedInfo && typeof parsedInfo.decimals === "number") { + return parsedInfo.decimals; + } + } + + throw new Error("Unable to fetch token decimals"); +} + +export async function getQuote( + connection: Connection, + baseToken: string, + outputToken: string, + amount: number +): Promise { + const decimals = await getTokenDecimals(connection, baseToken); + const adjustedAmount = amount * 10 ** decimals; + + const quoteResponse = await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${baseToken}&outputMint=${outputToken}&amount=${adjustedAmount}&slippageBps=50` + ); + const swapTransaction = await quoteResponse.json(); + const swapTransactionBuf = Buffer.from(swapTransaction, "base64"); + return new Uint8Array(swapTransactionBuf); +} + export const executeSwap = async ( transaction: VersionedTransaction, type: "buy" | "sell" @@ -227,8 +267,7 @@ export const fetchBuyTransaction = async ( // deserialize the transaction const swapTransactionBuf = Buffer.from(swapTransaction, "base64"); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'swapTransactionBuf' implicitly has an 'any' type. - var transaction = VersionedTransaction.deserialize(swapTransactionBuf); + const transaction = VersionedTransaction.deserialize(swapTransactionBuf); // sign the transaction transaction.sign([wallet]); @@ -274,8 +313,7 @@ export const fetchSellTransaction = async ( // deserialize the transaction const swapTransactionBuf = Buffer.from(swapTransaction, "base64"); - // @ts-expect-error ts-migrate(7006) FIXME: Parameter 'swapTransactionBuf' implicitly has an 'any' type. - var transaction = VersionedTransaction.deserialize(swapTransactionBuf); + const transaction = VersionedTransaction.deserialize(swapTransactionBuf); // sign the transaction transaction.sign([wallet]); diff --git a/core/src/cli/index.ts b/core/src/cli/index.ts index 953bd04adb..5ecfd99cf2 100644 --- a/core/src/cli/index.ts +++ b/core/src/cli/index.ts @@ -166,7 +166,12 @@ export async function createDirectRuntime( modelProvider: character.modelProvider, evaluators: [], character, - providers: [Provider.timeProvider, Provider.boredomProvider], + providers: [ + Provider.timeProvider, + Provider.boredomProvider, + character.settings?.secrets?.WALLET_PUBLIC_KEY && + Provider.walletProvider, + ].filter(Boolean), actions: [ ...defaultActions, // Custom actions diff --git a/core/src/clients/direct/index.ts b/core/src/clients/direct/index.ts index 8be31891ec..cff77fc23a 100644 --- a/core/src/clients/direct/index.ts +++ b/core/src/clients/direct/index.ts @@ -18,11 +18,11 @@ const upload = multer({ storage: multer.memoryStorage() }); export const messageHandlerTemplate = // {{goals}} - // `# Action Examples - // {{actionExamples}} - // (Action examples are for reference only. Do not use the information from them in your response.) + `# Action Examples +{{actionExamples}} +(Action examples are for reference only. Do not use the information from them in your response.) - `# Task: Generate dialog and actions for the character {{agentName}}. +# Task: Generate dialog and actions for the character {{agentName}}. About {{agentName}}: {{bio}} {{lore}} @@ -205,8 +205,25 @@ class DirectClient { ); return; } + + let message = null as Content | null; + + const result = await runtime.processActions( + memory, + [responseMessage], + state, + async (newMessages) => { + message = newMessages; + return [memory]; + } + ) + + if (message) { + res.json([message, response]); + } else { + res.json([response]); + } - res.json(response); } ); diff --git a/core/src/core/generation.ts b/core/src/core/generation.ts index 0c7b728c4d..817dec242d 100644 --- a/core/src/core/generation.ts +++ b/core/src/core/generation.ts @@ -403,6 +403,42 @@ export async function generateTextArray({ } } +export async function generateObject({ + runtime, + context, + modelClass, +}: { + runtime: IAgentRuntime; + context: string; + modelClass: string; +}): Promise { + if (!context) { + prettyConsole.error("generateObject context is empty"); + return null; + } + let retryDelay = 1000; + + while (true) { + try { + // this is slightly different than generateObjectArray, in that we parse object, not object array + const response = await generateText({ + runtime, + context, + modelClass, + }); + const parsedResponse = parseJSONObjectFromText(response); + if (parsedResponse) { + return parsedResponse; + } + } catch (error) { + prettyConsole.error("Error in generateObject:", error); + } + + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + retryDelay *= 2; + } +} + export async function generateObjectArray({ runtime, context, diff --git a/core/src/core/parsing.ts b/core/src/core/parsing.ts index 5835e8ed54..1cf110868d 100644 --- a/core/src/core/parsing.ts +++ b/core/src/core/parsing.ts @@ -2,10 +2,11 @@ const jsonBlockPattern = /```json\n([\s\S]*?)\n```/; export const messageCompletionFooter = `\nResponse format should be formatted in a JSON block like this: \`\`\`json -{ "user": "{{agentName}}", "text": string, "action": string } +{ "user": "{{agentName}}", "text": string, "action": "string" } \`\`\``; export const shouldRespondFooter = `The available options are [RESPOND], [IGNORE], or [STOP]. Choose the most appropriate option. +If {{agentName}} is talking too much, you can choose [IGNORE] Your response must include one of the options.`; diff --git a/core/src/index.ts b/core/src/index.ts index f74ca17504..45d0997ec9 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -88,7 +88,9 @@ function chat() { ); const data = await response.json(); - console.log(`${characters[0].name}: ${data.text}`); + for (const message of data) { + console.log(`${characters[0].name}: ${message.text}`); + } chat(); }); } diff --git a/core/src/providers/token.ts b/core/src/providers/token.ts index be191b300e..615d5f2217 100644 --- a/core/src/providers/token.ts +++ b/core/src/providers/token.ts @@ -517,7 +517,7 @@ export class TokenProvider { const limit = 1000; let cursor; //HELIOUS_API_KEY needs to be added - const url = `https://mainnet.helius-rpc.com/?api-key=${settings.HELIOUS_API_KEY || ""}`; + const url = `https://mainnet.helius-rpc.com/?api-key=${settings.HELIUS_API_KEY || ""}`; console.log({ url }); try { diff --git a/docs/docs/api/index.md b/docs/docs/api/index.md index 4c89359413..9d416ea06e 100644 --- a/docs/docs/api/index.md +++ b/docs/docs/api/index.md @@ -101,7 +101,7 @@ XAI_MODEL= # For asking Claude stuff ANTHROPIC_API_KEY= -WALLET_SECRET_KEY=EXAMPLE_WALLET_SECRET_KEY +WALLET_PRIVATE_KEY=EXAMPLE_WALLET_PRIVATE_KEY WALLET_PUBLIC_KEY=EXAMPLE_WALLET_PUBLIC_KEY BIRDEYE_API_KEY=