From 9b6479a6bff0ec6936621514b22d014f1af24acd Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Mon, 4 Nov 2024 12:07:31 +0200 Subject: [PATCH 1/8] swapDao Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swapDao.ts | 168 ++++++++++++++++++++++++++++++++++ core/src/actions/swapUtils.ts | 2 + core/src/providers/token.ts | 6 +- 3 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 core/src/actions/swapDao.ts diff --git a/core/src/actions/swapDao.ts b/core/src/actions/swapDao.ts new file mode 100644 index 00000000000..9679137c121 --- /dev/null +++ b/core/src/actions/swapDao.ts @@ -0,0 +1,168 @@ +import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js"; +import fetch from "cross-fetch"; +import { + ActionExample, + IAgentRuntime, + Memory, + type Action, +} from "../core/types.ts"; + +async function getQuote( + baseToken: string, + outputToken: string, + amount: number +): Promise<any> { + const quoteResponse = await fetch( + `https://quote-api.jup.ag/v6/quote?inputMint=${baseToken}&outputMint=${outputToken}&amount=${amount * 10 ** 6}&slippageBps=50` + ); + const swapTransaction = await quoteResponse.json(); + const swapTransactionBuf = Buffer.from(swapTransaction, "base64"); + return new Uint8Array(swapTransactionBuf); +} + +async function invokeSwapDao( + connection: Connection, + authority: Keypair, + statePDA: PublicKey, + walletPDA: PublicKey, + instructionData: Buffer +): Promise<string> { + const discriminator = new Uint8Array([ + 25, 143, 207, 190, 174, 228, 130, 107, + ]); + + // Combine discriminator and instructionData into a single Uint8Array + const combinedData = new Uint8Array( + discriminator.length + instructionData.length + ); + combinedData.set(discriminator, 0); + combinedData.set(instructionData, discriminator.length); + + const transaction = new Transaction().add({ + programId: new PublicKey("PROGRAM_ID"), + keys: [ + { pubkey: authority.publicKey, isSigner: true, isWritable: true }, + { pubkey: statePDA, isSigner: false, isWritable: true }, + { pubkey: walletPDA, isSigner: false, isWritable: true }, + ], + data: Buffer.from(combinedData), + }); + + const signature = await connection.sendTransaction(transaction, [ + authority, + ]); + await connection.confirmTransaction(signature); + return signature; +} + +async function promptConfirmation(): Promise<boolean> { + // confirmation logic here + const confirmSwap = window.confirm("Confirm the token swap?"); + return confirmSwap; +} + +export const executeSwap: Action = { + name: "EXECUTE_SWAP_DAO", + similes: ["SWAP_TOKENS_DAO", "TOKEN_SWAP_DAO"], + validate: async (runtime: IAgentRuntime, message: Memory) => { + console.log("Message:", message); + return true; + }, + description: "Perform a DAO token swap using execute_invoke.", + handler: async ( + runtime: IAgentRuntime, + message: Memory + ): Promise<boolean> => { + const { inputToken, outputToken, amount } = message.content; + + try { + const connection = new Connection( + "https://api.mainnet-beta.solana.com" // better if we use a better rpc + ); + const authority = Keypair.fromSecretKey( + Uint8Array.from( + Buffer.from( + runtime.getSetting("WALLET_PRIVATE_KEY"), // should be the authority private key + "base64" + ) + ) + ); + const daoMint = new PublicKey(runtime.getSetting("DAO_MINT")); // DAO mint address + + // Derive PDAs + const [statePDA] = await PublicKey.findProgramAddress( + [Buffer.from("state"), daoMint.toBuffer()], + authority.publicKey + ); + const [walletPDA] = await PublicKey.findProgramAddress( + [Buffer.from("wallet"), daoMint.toBuffer()], + authority.publicKey + ); + + const quoteData = await getQuote( + inputToken as string, + outputToken as string, + amount as number + ); + console.log("Swap Quote:", quoteData); + + const confirmSwap = await promptConfirmation(); + if (!confirmSwap) { + console.log("Swap canceled by user"); + return false; + } + + // Prepare instruction data for swap + const instructionData = Buffer.from( + JSON.stringify({ + quote: quoteData.data, + userPublicKey: authority.publicKey.toString(), + wrapAndUnwrapSol: true, + }) + ); + + const txid = await invokeSwapDao( + connection, + authority, + statePDA, + walletPDA, + instructionData + ); + + console.log("DAO Swap completed successfully!"); + console.log(`Transaction ID: ${txid}`); + + return true; + } catch (error) { + console.error("Error during DAO token swap:", error); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + inputTokenSymbol: "SOL", + outputTokenSymbol: "USDC", + inputToken: "So11111111111111111111111111111111111111112", + outputToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + amount: 0.1, + }, + }, + { + user: "{{user2}}", + content: { + text: "Swapping 0.1 SOL for USDC using DAO...", + action: "TOKEN_SWAP_DAO", + }, + }, + { + user: "{{user2}}", + content: { + text: "DAO Swap completed successfully! Transaction ID: ...", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/core/src/actions/swapUtils.ts b/core/src/actions/swapUtils.ts index 1f025b3f6a7..ceceea7d26c 100644 --- a/core/src/actions/swapUtils.ts +++ b/core/src/actions/swapUtils.ts @@ -227,6 +227,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); // sign the transaction @@ -273,6 +274,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); // sign the transaction diff --git a/core/src/providers/token.ts b/core/src/providers/token.ts index d20aedaebae..be191b300e4 100644 --- a/core/src/providers/token.ts +++ b/core/src/providers/token.ts @@ -2,7 +2,7 @@ import { Connection } from "@solana/web3.js"; // import fetch from "cross-fetch"; import { IAgentRuntime, Memory, Provider, State } from "../core/types.ts"; import settings from "../core/settings.ts"; -import { toBN, BN } from '../utils/bignumber.js'; +import { toBN } from "../utils/bignumber.js"; import { ProcessedTokenData, TokenSecurityData, @@ -620,7 +620,9 @@ export class TokenProvider { }) .map((holder) => ({ holderAddress: holder.address, - balanceUsd: toBN(holder.balance).multipliedBy(tokenPriceUsd).toFixed(2), + balanceUsd: toBN(holder.balance) + .multipliedBy(tokenPriceUsd) + .toFixed(2), })); return highValueHolders; From f8ffc4dd5bf93e8439cbb55c5e4ee2041194274c Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Mon, 4 Nov 2024 12:21:03 +0200 Subject: [PATCH 2/8] get the decimal value from the blockchain Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swapDao.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/core/src/actions/swapDao.ts b/core/src/actions/swapDao.ts index 9679137c121..07651320ebf 100644 --- a/core/src/actions/swapDao.ts +++ b/core/src/actions/swapDao.ts @@ -7,13 +7,40 @@ import { type Action, } from "../core/types.ts"; +async function getTokenDecimals( + connection: Connection, + mintAddress: string +): Promise<number> { + 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<any> { + 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=${amount * 10 ** 6}&slippageBps=50` + `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"); @@ -100,6 +127,7 @@ export const executeSwap: Action = { ); const quoteData = await getQuote( + connection as Connection, inputToken as string, outputToken as string, amount as number From 57e2bb03e0796cd124c0186ca7c2bb694f25b84c Mon Sep 17 00:00:00 2001 From: moon <shawmakesmagic@gmail.com> Date: Mon, 4 Nov 2024 03:27:35 -0800 Subject: [PATCH 3/8] 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 c35d4fd431f..818abfb9f3e 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 dacbc26f9ef..a26bd8b87d6 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 a8540afb427..9cc480654f7 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<any> { - 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<boolean> { - // 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<boolean> => { - 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 07651320ebf..18d58187b5d 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<number> { - 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<any> { - 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 ceceea7d26c..8439b1a6914 100644 --- a/core/src/actions/swapUtils.ts +++ b/core/src/actions/swapUtils.ts @@ -26,6 +26,46 @@ export async function delayedCall<T>( return method(...args); } +export async function getTokenDecimals( + connection: Connection, + mintAddress: string +): Promise<number> { + 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<any> { + 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 953bd04adb4..5ecfd99cf28 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 8be31891ec1..cff77fc23a4 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 0c7b728c4da..817dec242d3 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<any> { + 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 5835e8ed547..1cf110868d3 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 f74ca175040..45d0997ec9c 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 be191b300e4..615d5f22170 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 4c893594131..9d416ea06e7 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= From e4af25fbd64f64ff9387f48b1b39833727a4a05e Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Mon, 4 Nov 2024 14:32:31 +0200 Subject: [PATCH 4/8] update Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swapUtils.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/src/actions/swapUtils.ts b/core/src/actions/swapUtils.ts index ceceea7d26c..1f025b3f6a7 100644 --- a/core/src/actions/swapUtils.ts +++ b/core/src/actions/swapUtils.ts @@ -227,7 +227,6 @@ 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); // sign the transaction @@ -274,7 +273,6 @@ 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); // sign the transaction From 50f3a4d9bd4b23ced8964cd96d30e0cd41acf4ef Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Mon, 4 Nov 2024 15:34:01 +0200 Subject: [PATCH 5/8] use big number Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swap.ts | 107 +++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 37 deletions(-) diff --git a/core/src/actions/swap.ts b/core/src/actions/swap.ts index 9cc480654f7..3d75c56094e 100644 --- a/core/src/actions/swap.ts +++ b/core/src/actions/swap.ts @@ -1,4 +1,10 @@ -import { Connection, Keypair, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js"; +import { + Connection, + Keypair, + PublicKey, + Transaction, + VersionedTransaction, +} from "@solana/web3.js"; import fetch from "cross-fetch"; import { ActionExample, @@ -7,7 +13,7 @@ import { type Action, State, ModelClass, - HandlerCallback + HandlerCallback, } from "../core/types.ts"; import { walletProvider } from "../providers/wallet.ts"; import { composeContext } from "../core/context.ts"; @@ -15,6 +21,7 @@ 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"; +import BigNumber from "bignumber.js"; async function swapToken( connection: Connection, @@ -25,17 +32,25 @@ async function swapToken( ): Promise<any> { 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); + const decimals = + inputTokenCA === settings.SOL_ADDRESS + ? new BigNumber(9) + : new BigNumber( + await getTokenDecimals(connection, inputTokenCA) + ); + + console.log("Decimals:", decimals.toString()); + + // Use BigNumber for adjustedAmount: amount * (10 ** decimals) + const amountBN = new BigNumber(amount); + const adjustedAmount = amountBN.multipliedBy( + new BigNumber(10).pow(decimals) + ); console.log("Fetching quote with params:", { inputMint: inputTokenCA, outputMint: outputTokenCA, - amount: adjustedAmount + amount: adjustedAmount, }); const quoteResponse = await fetch( @@ -45,7 +60,9 @@ async function swapToken( if (!quoteData || quoteData.error) { console.error("Quote error:", quoteData); - throw new Error(`Failed to get quote: ${quoteData?.error || 'Unknown error'}`); + throw new Error( + `Failed to get quote: ${quoteData?.error || "Unknown error"}` + ); } console.log("Quote received:", quoteData); @@ -55,7 +72,7 @@ async function swapToken( userPublicKey: walletPublicKey.toString(), wrapAndUnwrapSol: true, computeUnitPriceMicroLamports: 1000, - dynamicComputeUnitLimit: true + dynamicComputeUnitLimit: true, }; console.log("Requesting swap with body:", swapRequestBody); @@ -65,26 +82,26 @@ async function swapToken( headers: { "Content-Type": "application/json", }, - body: JSON.stringify(swapRequestBody) + 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'}`); + throw new Error( + `Failed to get swap transaction: ${swapData?.error || "No swap transaction returned"}` + ); } console.log("Swap transaction received"); return swapData; - } catch (error) { console.error("Error in swapToken:", error); throw error; } } - const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. Example response: @@ -142,7 +159,6 @@ export const executeSwap: Action = { _options: { [key: string]: unknown }, callback?: HandlerCallback ): Promise<boolean> => { - // composeState if (!state) { state = (await runtime.composeState(message)) as State; @@ -168,10 +184,10 @@ export const executeSwap: Action = { console.log("Response:", response); // Add SOL handling logic - if (response.inputTokenSymbol?.toUpperCase() === 'SOL') { + if (response.inputTokenSymbol?.toUpperCase() === "SOL") { response.inputTokenCA = settings.SOL_ADDRESS; } - if (response.outputTokenSymbol?.toUpperCase() === 'SOL') { + if (response.outputTokenSymbol?.toUpperCase() === "SOL") { response.outputTokenCA = settings.SOL_ADDRESS; } @@ -226,12 +242,16 @@ export const executeSwap: Action = { ); console.log("Deserializing transaction..."); - const transactionBuf = Buffer.from(swapResult.swapTransaction, "base64"); - const transaction = VersionedTransaction.deserialize(transactionBuf); - + 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 { @@ -240,25 +260,31 @@ export const executeSwap: Action = { } catch (e) { try { // If that fails, try base64 - secretKey = Uint8Array.from(Buffer.from(privateKeyString, 'base64')); + secretKey = Uint8Array.from( + Buffer.from(privateKeyString, "base64") + ); } catch (e2) { - throw new Error('Invalid private key format'); + throw new Error("Invalid private key format"); } } // 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.`); + throw new Error( + `Invalid private key length: ${secretKey.length}. Expected 64 bytes.` + ); } 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"); + throw new Error( + "Generated public key doesn't match expected public key" + ); } console.log("Signing transaction..."); @@ -271,24 +297,31 @@ export const executeSwap: Action = { const txid = await connection.sendTransaction(transaction, { skipPreflight: false, maxRetries: 3, - preflightCommitment: 'confirmed' + 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'); + 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}`); + throw new Error( + `Transaction failed: ${confirmation.value.err}` + ); } if (confirmation.value.err) { - throw new Error(`Transaction failed: ${confirmation.value.err}`); + throw new Error( + `Transaction failed: ${confirmation.value.err}` + ); } console.log("Swap completed successfully!"); @@ -299,7 +332,7 @@ export const executeSwap: Action = { }; callback?.(responseMsg); - + return true; } catch (error) { console.error("Error during token swap:", error); From fe0410e385d52e1a64d00c17af832ddfa551aeda Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Mon, 4 Nov 2024 16:43:40 +0200 Subject: [PATCH 6/8] add getTokensInWallet and add the logic to resolve CA from symbol based on existing symbol in wallet Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swap.ts | 76 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/core/src/actions/swap.ts b/core/src/actions/swap.ts index 3d75c56094e..66f5c5fbd2c 100644 --- a/core/src/actions/swap.ts +++ b/core/src/actions/swap.ts @@ -22,6 +22,7 @@ import { getTokenDecimals } from "./swapUtils.ts"; import settings from "../core/settings.ts"; import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes/index.js"; import BigNumber from "bignumber.js"; +import { WalletProvider } from "../providers/wallet.ts"; async function swapToken( connection: Connection, @@ -141,6 +142,34 @@ Respond with a JSON markdown block containing only the extracted values. Use nul // if we get the token symbol but not the CA, check walet for matching token, and if we have, get the CA for it +// get all the tokens in the wallet using the wallet provider +async function getTokensInWallet(runtime: IAgentRuntime) { + const walletProvider = new WalletProvider( + new Connection("https://api.mainnet-beta.solana.com"), + new PublicKey(runtime.getSetting("WALLET_PUBLIC_KEY")) + ); + const walletInfo = await walletProvider.fetchPortfolioValue(runtime); + const items = walletInfo.items; + return items; +} + +// check if the token symbol is in the wallet +async function checkTokenInWallet(runtime: IAgentRuntime, tokenSymbol: string) { + try { + const items = await getTokensInWallet(runtime); + const token = items.find((item) => item.symbol === tokenSymbol); + + if (token) { + return token.address; + } else { + return null; + } + } catch (error) { + console.error("Error checking token in wallet:", error); + return null; + } +} + // swapToken should took CA, not symbol export const executeSwap: Action = { @@ -193,13 +222,46 @@ export const executeSwap: Action = { // 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.inputTokenCA && response.inputTokenSymbol) { + console.log( + `Attempting to resolve CA for input token symbol: ${response.inputTokenSymbol}` + ); + response.inputTokenCA = await checkTokenInWallet( + runtime, + response.inputTokenSymbol + ); + if (response.inputTokenCA) { + console.log(`Resolved inputTokenCA: ${response.inputTokenCA}`); + } else { + 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.outputTokenCA && response.outputTokenSymbol) { + console.log( + `Attempting to resolve CA for output token symbol: ${response.outputTokenSymbol}` + ); + response.outputTokenCA = await checkTokenInWallet( + runtime, + response.outputTokenSymbol + ); + if (response.outputTokenCA) { + console.log( + `Resolved outputTokenCA: ${response.outputTokenCA}` + ); + } else { + 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) { From 7e85609c07a7c5c6c96a224b455d658f30ae76a8 Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Mon, 4 Nov 2024 16:53:32 +0200 Subject: [PATCH 7/8] fix Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swap.ts | 1 + core/src/providers/wallet.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/actions/swap.ts b/core/src/actions/swap.ts index 66f5c5fbd2c..978c13a046f 100644 --- a/core/src/actions/swap.ts +++ b/core/src/actions/swap.ts @@ -148,6 +148,7 @@ async function getTokensInWallet(runtime: IAgentRuntime) { new Connection("https://api.mainnet-beta.solana.com"), new PublicKey(runtime.getSetting("WALLET_PUBLIC_KEY")) ); + const walletInfo = await walletProvider.fetchPortfolioValue(runtime); const items = walletInfo.items; return items; diff --git a/core/src/providers/wallet.ts b/core/src/providers/wallet.ts index a0a6ff08b89..3c9114c4009 100644 --- a/core/src/providers/wallet.ts +++ b/core/src/providers/wallet.ts @@ -18,6 +18,7 @@ const PROVIDER_CONFIG = { interface Item { name: string; + address: string; symbol: string; decimals: number; balance: string; @@ -48,7 +49,7 @@ interface Prices { ethereum: { usd: string }; } -class WalletProvider { +export class WalletProvider { constructor( private connection: Connection, private walletPublicKey: PublicKey From 0b167441086fe5c09a250fba1685ef522a6b6691 Mon Sep 17 00:00:00 2001 From: MarcoMandar <malicemandar@gmail.com> Date: Tue, 5 Nov 2024 02:51:25 +0200 Subject: [PATCH 8/8] recommender, token performane integration to swap Signed-off-by: MarcoMandar <malicemandar@gmail.com> --- core/src/actions/swap.ts | 75 ++++++++++++++- core/src/adapters/trustScoreDatabase.ts | 111 ++++++++++++++++++++++- core/src/providers/trustScoreProvider.ts | 7 +- 3 files changed, 184 insertions(+), 9 deletions(-) diff --git a/core/src/actions/swap.ts b/core/src/actions/swap.ts index 978c13a046f..6a5a72e3093 100644 --- a/core/src/actions/swap.ts +++ b/core/src/actions/swap.ts @@ -23,6 +23,10 @@ import settings from "../core/settings.ts"; import { bs58 } from "@coral-xyz/anchor/dist/cjs/utils/bytes/index.js"; import BigNumber from "bignumber.js"; import { WalletProvider } from "../providers/wallet.ts"; +import { TrustScoreProvider } from "../providers/trustScoreProvider"; +import { TokenProvider } from "../providers/token"; +import { TrustScoreDatabase } from "../adapters/trustScoreDatabase"; +import { v4 as uuidv4 } from "uuid"; async function swapToken( connection: Connection, @@ -155,7 +159,7 @@ async function getTokensInWallet(runtime: IAgentRuntime) { } // check if the token symbol is in the wallet -async function checkTokenInWallet(runtime: IAgentRuntime, tokenSymbol: string) { +async function getTokenFromWallet(runtime: IAgentRuntime, tokenSymbol: string) { try { const items = await getTokensInWallet(runtime); const token = items.find((item) => item.symbol === tokenSymbol); @@ -212,6 +216,8 @@ export const executeSwap: Action = { }); console.log("Response:", response); + const type = + response.inputTokenSymbol?.toUpperCase() === "SOL" ? "buy" : "sell"; // Add SOL handling logic if (response.inputTokenSymbol?.toUpperCase() === "SOL") { @@ -227,7 +233,7 @@ export const executeSwap: Action = { console.log( `Attempting to resolve CA for input token symbol: ${response.inputTokenSymbol}` ); - response.inputTokenCA = await checkTokenInWallet( + response.inputTokenCA = await getTokenFromWallet( runtime, response.inputTokenSymbol ); @@ -247,7 +253,7 @@ export const executeSwap: Action = { console.log( `Attempting to resolve CA for output token symbol: ${response.outputTokenSymbol}` ); - response.outputTokenCA = await checkTokenInWallet( + response.outputTokenCA = await getTokenFromWallet( runtime, response.outputTokenSymbol ); @@ -387,6 +393,69 @@ export const executeSwap: Action = { ); } + if (type === "buy") { + const tokenProvider = new TokenProvider(response.outputTokenCA); + const module = await import("better-sqlite3"); + const Database = module.default; + const trustScoreDb = new TrustScoreDatabase( + new Database(":memory:") + ); + // add or get recommender + const uuid = uuidv4(); + const recommender = await trustScoreDb.getOrCreateRecommender({ + id: uuid, + address: walletPublicKey.toString(), + solanaPubkey: walletPublicKey.toString(), + }); + + const trustScoreDatabase = new TrustScoreProvider( + tokenProvider, + trustScoreDb + ); + // save the trade + const tradeData = { + buy_amount: response.amount, + is_simulation: false, + }; + await trustScoreDatabase.createTradePerformance( + response.outputTokenCA, + recommender.id, + tradeData + ); + } else if (type === "sell") { + const tokenProvider = new TokenProvider(response.inputTokenCA); + const module = await import("better-sqlite3"); + const Database = module.default; + const trustScoreDb = new TrustScoreDatabase( + new Database(":memory:") + ); + // add or get recommender + const uuid = uuidv4(); + const recommender = await trustScoreDb.getOrCreateRecommender({ + id: uuid, + address: walletPublicKey.toString(), + solanaPubkey: walletPublicKey.toString(), + }); + + const trustScoreDatabase = new TrustScoreProvider( + tokenProvider, + trustScoreDb + ); + // save the trade + const sellDetails = { + sell_amount: response.amount, + sell_recommender_id: recommender.id, + }; + const sellTimeStamp = new Date().getTime().toString(); + await trustScoreDatabase.updateSellDetails( + response.inputTokenCA, + recommender.id, + sellTimeStamp, + sellDetails, + false + ); + } + console.log("Swap completed successfully!"); console.log(`Transaction ID: ${txid}`); diff --git a/core/src/adapters/trustScoreDatabase.ts b/core/src/adapters/trustScoreDatabase.ts index b206fd1923e..5ef6231f5e0 100644 --- a/core/src/adapters/trustScoreDatabase.ts +++ b/core/src/adapters/trustScoreDatabase.ts @@ -172,7 +172,7 @@ export class TrustScoreDatabase { risk_score REAL DEFAULT 0, consistency_score REAL DEFAULT 0, virtual_confidence REAL DEFAULT 0, - last_active_date DATETIME DEFAULT CURRENT_TIMESTAMP + last_active_date DATETIME DEFAULT CURRENT_TIMESTAMP, trust_decay REAL DEFAULT 0, last_updated DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (recommender_id) REFERENCES recommenders(id) ON DELETE CASCADE @@ -349,6 +349,61 @@ export class TrustScoreDatabase { return recommender || null; } + /** + * Retrieves an existing recommender or creates a new one if not found. + * Also initializes metrics for the recommender if they haven't been initialized yet. + * @param recommender Recommender object containing at least one identifier + * @returns Recommender object with all details, or null if failed + */ + getOrCreateRecommender(recommender: Recommender): Recommender | null { + try { + // Begin a transaction + const transaction = this.db.transaction(() => { + // Attempt to retrieve the recommender + const existingRecommender = this.getRecommender( + recommender.address + ); + if (existingRecommender) { + // Recommender exists, ensure metrics are initialized + this.initializeRecommenderMetrics(existingRecommender.id!); + return existingRecommender; + } + + // Recommender does not exist, create a new one + const newRecommenderId = this.addRecommender(recommender); + if (!newRecommenderId) { + throw new Error("Failed to add new recommender."); + } + + // Initialize metrics for the new recommender + const metricsInitialized = + this.initializeRecommenderMetrics(newRecommenderId); + if (!metricsInitialized) { + throw new Error( + "Failed to initialize recommender metrics." + ); + } + + // Retrieve and return the newly created recommender + const newRecommender = this.getRecommender(newRecommenderId); + if (!newRecommender) { + throw new Error( + "Failed to retrieve the newly created recommender." + ); + } + + return newRecommender; + }); + + // Execute the transaction and return the recommender + const recommenderResult = transaction(); + return recommenderResult; + } catch (error) { + console.error("Error in getOrCreateRecommender:", error); + return null; + } + } + /** * Initializes metrics for a recommender if not present. * @param recommenderId Recommender's UUID @@ -548,6 +603,8 @@ export class TrustScoreDatabase { performance.tokenAddress, performance.priceChange24h, performance.volumeChange24h, + performance.trade_24h_change, + performance.liquidity, performance.liquidityChange24h, performance.holderChange24h, // Ensure column name matches schema performance.rugPull ? 1 : 0, @@ -916,7 +973,7 @@ export class TrustScoreDatabase { market_cap_change = ?, sell_liquidity = ?, liquidity_change = ?, - rapidDump = ? + rapidDump = ?, sell_recommender_id = ? WHERE token_address = ? @@ -1016,6 +1073,56 @@ export class TrustScoreDatabase { }; } + /** + * Retrieves the latest trade performance metrics without requiring buyTimeStamp. + * @param tokenAddress Token's address + * @param recommenderId Recommender's UUID + * @param isSimulation Whether the trade is a simulation. If true, retrieves from simulation_trade; otherwise, from trade. + * @returns TradePerformance object or null + */ + getLatestTradePerformance( + tokenAddress: string, + recommenderId: string, + isSimulation: boolean + ): TradePerformance | null { + const tableName = isSimulation ? "simulation_trade" : "trade"; + const sql = ` + SELECT * FROM ${tableName} + WHERE token_address = ? AND recommender_id = ? + ORDER BY buy_timeStamp DESC + LIMIT 1; + `; + const row = this.db.prepare(sql).get(tokenAddress, recommenderId) as + | TradePerformance + | undefined; + if (!row) return null; + + return { + token_address: row.token_address, + recommender_id: row.recommender_id, + buy_price: row.buy_price, + sell_price: row.sell_price, + buy_timeStamp: row.buy_timeStamp, + sell_timeStamp: row.sell_timeStamp, + buy_amount: row.buy_amount, + sell_amount: row.sell_amount, + buy_sol: row.buy_sol, + received_sol: row.received_sol, + buy_value_usd: row.buy_value_usd, + sell_value_usd: row.sell_value_usd, + profit_usd: row.profit_usd, + profit_percent: row.profit_percent, + buy_market_cap: row.buy_market_cap, + sell_market_cap: row.sell_market_cap, + market_cap_change: row.market_cap_change, + buy_liquidity: row.buy_liquidity, + sell_liquidity: row.sell_liquidity, + liquidity_change: row.liquidity_change, + last_updated: row.last_updated, + rapidDump: row.rapidDump, + }; + } + /** * Close the database connection gracefully. */ diff --git a/core/src/providers/trustScoreProvider.ts b/core/src/providers/trustScoreProvider.ts index 1932d9ba587..f941111ac29 100644 --- a/core/src/providers/trustScoreProvider.ts +++ b/core/src/providers/trustScoreProvider.ts @@ -363,8 +363,7 @@ export class TrustScoreProvider { recommenderId: string, sellTimeStamp: string, sellDetails: sellDetails, - isSimulation: boolean, - buyTimeStamp: string + isSimulation: boolean ) { const processedData: ProcessedTokenData = await this.tokenProvider.getProcessedTokenData(); @@ -377,12 +376,12 @@ export class TrustScoreProvider { const sellSol = sellDetails.sell_amount / parseFloat(solPrice); const sell_value_usd = sellDetails.sell_amount * processedData.tradeData.price; - const trade = await this.trustScoreDb.getTradePerformance( + const trade = await this.trustScoreDb.getLatestTradePerformance( tokenAddress, recommenderId, - buyTimeStamp, isSimulation ); + const buyTimeStamp = trade.buy_timeStamp; const marketCap = processedData.dexScreenerData.pairs[0]?.marketCap || 0; const liquidity =