From bd607a6d00a9e5ae11aa7f57de083643f72ffc9f Mon Sep 17 00:00:00 2001 From: Snobbish Bee <125891987+snobbee@users.noreply.github.com> Date: Thu, 6 Feb 2025 03:41:45 +0100 Subject: [PATCH] fix: pnl calc fixed --- packages/client-coinbase/src/index.ts | 293 ++++++++++++++++++-------- 1 file changed, 200 insertions(+), 93 deletions(-) diff --git a/packages/client-coinbase/src/index.ts b/packages/client-coinbase/src/index.ts index d24cda00658..b96cbd07a1f 100644 --- a/packages/client-coinbase/src/index.ts +++ b/packages/client-coinbase/src/index.ts @@ -38,7 +38,7 @@ export class CoinbaseClient implements Client { this.server = express(); this.port = Number(runtime.getSetting("COINBASE_WEBHOOK_PORT")) || 3001; this.wallets = []; - this.initialBalanceETH = 1 + this.initialBalanceETH = 1; } async initialize(): Promise { @@ -60,24 +60,33 @@ export class CoinbaseClient implements Client { // Add CORS middleware to allow external requests this.server.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'POST'); - res.header('Access-Control-Allow-Headers', 'Content-Type'); - if (req.method === 'OPTIONS') { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "POST"); + res.header("Access-Control-Allow-Headers", "Content-Type"); + if (req.method === "OPTIONS") { return res.sendStatus(200); } next(); }); // Add webhook validation middleware - const validateWebhook = (req: express.Request, res: express.Response, next: express.NextFunction) => { + const validateWebhook = ( + req: express.Request, + res: express.Response, + next: express.NextFunction + ) => { const event = req.body as WebhookEvent; - elizaLogger.info('event ', JSON.stringify(event)) - if (!event.event || !event.ticker || !event.timestamp || !event.price) { + elizaLogger.info("event ", JSON.stringify(event)); + if ( + !event.event || + !event.ticker || + !event.timestamp || + !event.price + ) { res.status(400).json({ error: "Invalid webhook payload" }); return; } - if (event.event !== 'buy' && event.event !== 'sell') { + if (event.event !== "buy" && event.event !== "sell") { res.status(400).json({ error: "Invalid event type" }); return; } @@ -85,49 +94,59 @@ export class CoinbaseClient implements Client { }; // Add health check endpoint - this.server.get('/health', (req, res) => { - res.status(200).json({ status: 'ok' }); + this.server.get("/health", (req, res) => { + res.status(200).json({ status: "ok" }); }); this.server.get("/webhook/coinbase/health", (req, res) => { elizaLogger.info("Health check received"); res.status(200).json({ status: "ok" }); }); - + this.server.post("/webhook/coinbase/:agentId", async (req, res) => { elizaLogger.info("Webhook received for agent:", req.params.agentId); const runtime = this.runtime; - + if (!runtime) { res.status(404).json({ error: "Agent not found" }); return; } - + // Validate the webhook payload const event = req.body as WebhookEvent; - if (!event.event || !event.ticker || !event.timestamp || !event.price) { + if ( + !event.event || + !event.ticker || + !event.timestamp || + !event.price + ) { res.status(400).json({ error: "Invalid webhook payload" }); return; } - if (event.event !== 'buy' && event.event !== 'sell') { + if (event.event !== "buy" && event.event !== "sell") { res.status(400).json({ error: "Invalid event type" }); return; } - + try { // Forward the webhook event to the client's handleWebhookEvent method await this.handleWebhookEvent(event); res.status(200).json({ status: "success" }); } catch (error) { - elizaLogger.error("Error processing Coinbase webhook:", error.message); + elizaLogger.error( + "Error processing Coinbase webhook:", + error.message + ); res.status(500).json({ error: "Internal Server Error" }); } }); return new Promise((resolve, reject) => { try { - this.server.listen(this.port, '0.0.0.0', () => { - elizaLogger.info(`Webhook server listening on port ${this.port}`); + this.server.listen(this.port, "0.0.0.0", () => { + elizaLogger.info( + `Webhook server listening on port ${this.port}` + ); resolve(); }); } catch (error) { @@ -145,17 +164,36 @@ export class CoinbaseClient implements Client { this.runtime.getSetting("COINBASE_PRIVATE_KEY") ?? process.env.COINBASE_PRIVATE_KEY, }); - const walletTypes: WalletType[] = ['short_term_trading', 'long_term_trading', 'dry_powder', 'operational_capital']; + const walletTypes: WalletType[] = [ + "short_term_trading", + "long_term_trading", + "dry_powder", + "operational_capital", + ]; const networkId = Coinbase.networks.BaseMainnet; for (const walletType of walletTypes) { - elizaLogger.info('walletType ', walletType); - const wallet = await initializeWallet(this.runtime, networkId, walletType); - elizaLogger.info('Successfully loaded wallet ', wallet.wallet.getId()); + elizaLogger.info("walletType ", walletType); + const wallet = await initializeWallet( + this.runtime, + networkId, + walletType + ); + elizaLogger.info( + "Successfully loaded wallet ", + wallet.wallet.getId() + ); this.wallets.push(wallet); } } - private async generateTweetContent(event: WebhookEvent, amountInCurrency: number, pnl: string, formattedTimestamp: string, state: State, hash: string | null): Promise { + private async generateTweetContent( + event: WebhookEvent, + amountInCurrency: number, + pnl: string, + formattedTimestamp: string, + state: State, + hash: string | null + ): Promise { try { const tradeTweetTemplate = ` # Task @@ -178,17 +216,25 @@ Guidelines: 7. Ensure key details are present: action, amount, ticker, and price Sample buy tweets: -"📈 Added $${amountInCurrency.toFixed(2)} of ${event.ticker} at $${Number(event.price).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" -"🎯 Strategic ${event.ticker} buy: $${amountInCurrency.toFixed(2)} at $${Number(event.price).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" +"📈 Added $${amountInCurrency.toFixed(2)} of ${event.ticker} at $${Number( + event.price + ).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" +"🎯 Strategic ${event.ticker} buy: $${amountInCurrency.toFixed(2)} at $${Number( + event.price + ).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" Sample sell tweets: -"💫 Sold ${event.ticker}: $${amountInCurrency.toFixed(2)} at $${Number(event.price).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" -"📊 Sold $${amountInCurrency.toFixed(2)} of ${event.ticker} at $${Number(event.price).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" +"💫 Sold ${event.ticker}: $${amountInCurrency.toFixed(2)} at $${Number( + event.price + ).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" +"📊 Sold $${amountInCurrency.toFixed(2)} of ${event.ticker} at $${Number( + event.price + ).toFixed(2)}. Overall PNL: ${pnl} ${blockExplorerBaseTxUrl(hash)}" Generate only the tweet text, no commentary or markdown.`; const context = composeContext({ template: tradeTweetTemplate, - state + state, }); const tweetContent = await generateText({ @@ -198,20 +244,24 @@ Generate only the tweet text, no commentary or markdown.`; }); const trimmedContent = tweetContent.trim(); - return trimmedContent.length > 180 ? trimmedContent.substring(0, 177) + "..." : trimmedContent; - + return trimmedContent.length > 180 + ? trimmedContent.substring(0, 177) + "..." + : trimmedContent; } catch (error) { elizaLogger.error("Error generating tweet content:", error); - const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1; - const fallbackTweet = `🚀 ${event.event.toUpperCase()}: $${amount.toFixed(2)} of ${event.ticker} at $${Number(event.price).toFixed(2)}`; + const amount = + Number(this.runtime.getSetting("COINBASE_TRADING_AMOUNT")) ?? 1; + const fallbackTweet = `🚀 ${event.event.toUpperCase()}: $${amount.toFixed( + 2 + )} of ${event.ticker} at $${Number(event.price).toFixed(2)}`; return fallbackTweet; } } private async handleWebhookEvent(event: WebhookEvent) { - // for now just support ETH - if (event.ticker !== 'ETH'&& event.ticker !== 'WETH') { - elizaLogger.info('Unsupported ticker:', event.ticker); + // for now just support ETH + if (event.ticker !== "ETH" && event.ticker !== "WETH") { + elizaLogger.info("Unsupported ticker:", event.ticker); return; } // Set up room and ensure participation @@ -219,49 +269,74 @@ Generate only the tweet text, no commentary or markdown.`; await this.setupRoom(roomId); // Get trading amount from settings - const amount = Number(this.runtime.getSetting('COINBASE_TRADING_AMOUNT')) ?? 1; - elizaLogger.info('amount ', amount); + const amount = + Number(this.runtime.getSetting("COINBASE_TRADING_AMOUNT")) ?? 1; + elizaLogger.info("amount ", amount); // Create and store memory of trade const memory = this.createTradeMemory(event, amount, roomId); - elizaLogger.info('memory ', memory); + elizaLogger.info("memory ", memory); await this.runtime.messageManager.createMemory(memory); - + // Generate state and format timestamp const state = await this.runtime.composeState(memory); const formattedTimestamp = this.getFormattedTimestamp(); - elizaLogger.info('formattedTimestamp ', formattedTimestamp); + elizaLogger.info("formattedTimestamp ", formattedTimestamp); // Execute token swap - const buy = event.event.toUpperCase() === 'BUY'; + const buy = event.event.toUpperCase() === "BUY"; const amountInCurrency = buy ? amount : amount / Number(event.price); - const txHash = await this.executeTokenSwap(event, amountInCurrency, buy); + const txHash = await this.executeTokenSwap( + event, + amountInCurrency, + buy + ); if (txHash == null) { - elizaLogger.error('txHash is null'); + elizaLogger.error("txHash is null"); return; } - elizaLogger.info('txHash ', txHash); + elizaLogger.info("txHash ", txHash); - const pnl = await calculateOverallPNL(this.runtime, this.runtime.getSetting('WALLET_PUBLIC_KEY') as `0x${string}`, 1000) - elizaLogger.info('pnl ', pnl); + const pnl = await calculateOverallPNL( + this.runtime, + this.runtime.getSetting("WALLET_PUBLIC_KEY") as `0x${string}`, + 1000 + ); + elizaLogger.info("pnl ", pnl); // Generate and post tweet - await this.handleTweetPosting(event, amount, pnl, formattedTimestamp, state, txHash); + await this.handleTweetPosting( + event, + amount, + pnl, + formattedTimestamp, + state, + txHash + ); } private async setupRoom(roomId: UUID) { await this.runtime.ensureRoomExists(roomId); - await this.runtime.ensureParticipantInRoom(this.runtime.agentId, roomId); + await this.runtime.ensureParticipantInRoom( + this.runtime.agentId, + roomId + ); } - private createTradeMemory(event: WebhookEvent, amount: number, roomId: UUID): Memory { + private createTradeMemory( + event: WebhookEvent, + amount: number, + roomId: UUID + ): Memory { return { id: stringToUuid(`coinbase-${event.timestamp}`), userId: this.runtime.agentId, agentId: this.runtime.agentId, roomId, content: { - text: `${event.event.toUpperCase()} $${amount} worth of ${event.ticker}`, + text: `${event.event.toUpperCase()} $${amount} worth of ${ + event.ticker + }`, action: "SWAP", source: "coinbase", metadata: { @@ -270,30 +345,34 @@ Generate only the tweet text, no commentary or markdown.`; price: event.price, amount: amount, timestamp: event.timestamp, - walletType: 'short_term_trading', - } + walletType: "short_term_trading", + }, }, - createdAt: Date.now() + createdAt: Date.now(), }; } private getFormattedTimestamp(): string { - return new Intl.DateTimeFormat('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZoneName: 'short' + return new Intl.DateTimeFormat("en-US", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + timeZoneName: "short", }).format(new Date()); } - private async executeTokenSwap(event: WebhookEvent, amount: number, buy: boolean): Promise { + private async executeTokenSwap( + event: WebhookEvent, + amount: number, + buy: boolean + ): Promise { return await tokenSwap( this.runtime, amount, - buy ? 'USDC' : event.ticker, - buy ? event.ticker : 'USDC', - this.runtime.getSetting('WALLET_PUBLIC_KEY'), - this.runtime.getSetting('WALLET_PRIVATE_KEY'), + buy ? "USDC" : event.ticker, + buy ? event.ticker : "USDC", + this.runtime.getSetting("WALLET_PUBLIC_KEY"), + this.runtime.getSetting("WALLET_PRIVATE_KEY"), "base" ); } @@ -317,8 +396,13 @@ Generate only the tweet text, no commentary or markdown.`; ); elizaLogger.info("Generated tweet content:", tweetContent); - if (this.runtime.getSetting('TWITTER_DRY_RUN').toLowerCase() === 'true') { - elizaLogger.info("Dry run mode enabled. Skipping tweet posting."); + if ( + this.runtime.getSetting("TWITTER_DRY_RUN").toLowerCase() === + "true" + ) { + elizaLogger.info( + "Dry run mode enabled. Skipping tweet posting." + ); return; } @@ -357,19 +441,21 @@ Generate only the tweet text, no commentary or markdown.`; async start(): Promise { await this.initialize(); } - } export const CoinbaseClientInterface: Client = { start: async (runtime: IAgentRuntime) => { - elizaLogger.info("Starting Coinbase client with agent ID:", runtime.agentId); + elizaLogger.info( + "Starting Coinbase client with agent ID:", + runtime.agentId + ); const client = new CoinbaseClient(runtime); await client.start(); return client; }, stop: async (runtime: IAgentRuntime) => { try { - elizaLogger.info("Stopping Coinbase client"); + elizaLogger.info("Stopping Coinbase client"); await runtime.clients.coinbase.stop(); } catch (e) { elizaLogger.error("Coinbase client stop error:", e); @@ -377,42 +463,63 @@ export const CoinbaseClientInterface: Client = { }, }; -export const calculateOverallPNL = async (runtime: IAgentRuntime, publicKey: `0x${string}`, initialBalance: number): Promise => { +export const calculateOverallPNL = async ( + runtime: IAgentRuntime, + publicKey: `0x${string}`, + initialBalance: number +): Promise => { + elizaLogger.info(`initialBalance ${initialBalance}`); const client = createWalletClient({ - account: privateKeyToAccount(("0x" + runtime.getSetting("WALLET_PRIVATE_KEY")) as `0x${string}`), + account: privateKeyToAccount( + ("0x" + runtime.getSetting("WALLET_PRIVATE_KEY")) as `0x${string}` + ), chain: base, transport: http(runtime.getSetting("ALCHEMY_HTTP_TRANSPORT_URL")), }).extend(publicActions); const ethBalanceBaseUnits = await client.getBalance({ - address: publicKey - }) - const ethBalance = Number(ethBalanceBaseUnits / BigInt(1e18)) - elizaLogger.info("ethBalance ", ethBalance); - const priceInquiry = await getPriceInquiry(runtime, 'ETH',ethBalance, "USDC", "base"); + address: publicKey, + }); + const ethBalance = Number(ethBalanceBaseUnits) / 1e18; + elizaLogger.info(`ethBalance ${ethBalance}`); + const priceInquiry = await getPriceInquiry( + runtime, + "ETH", + ethBalance, + "USDC", + "base" + ); // get latest quote elizaLogger.info("Getting quote for swap", JSON.stringify(priceInquiry)); const quote = await getQuoteObj(runtime, priceInquiry, publicKey); elizaLogger.info("quote ", JSON.stringify(quote)); - const ethBalanceUSD = Number(quote.buyAmount) - const usdcBalanceBaseUnits = await readContractWrapper(runtime, '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913', "balanceOf", { - account: publicKey - }, "base-mainnet", erc20Abi); - const usdcBalance = Number(usdcBalanceBaseUnits / BigInt(1e6)) - elizaLogger.info("usdcBalance ", usdcBalance); - const pnlUSD = ethBalanceUSD + usdcBalance - initialBalance - elizaLogger.info("pnlUSD ", pnlUSD); - const absoluteValuePNL = Math.abs(pnlUSD) - elizaLogger.info("absoluteValuePNL ", absoluteValuePNL); - const formattedPNL = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', + const ethBalanceUSD = Number(quote.buyAmount) / 1e6; + elizaLogger.info(`ethBalanceUSD ${ethBalanceUSD}`); + const usdcBalanceBaseUnits = await readContractWrapper( + runtime, + "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", + "balanceOf", + { + account: publicKey, + }, + "base-mainnet", + erc20Abi + ); + const usdcBalance = Number(usdcBalanceBaseUnits) / 1e6; + elizaLogger.info(`usdcBalance ${usdcBalance}`); + const pnlUSD = ethBalanceUSD + usdcBalance - initialBalance; + elizaLogger.info(`pnlUSD ${pnlUSD}`); + const absoluteValuePNL = Math.abs(pnlUSD); + elizaLogger.info(`absoluteValuePNL ${absoluteValuePNL}`); + const formattedPNL = new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", minimumFractionDigits: 2, maximumFractionDigits: 2, }).format(absoluteValuePNL); elizaLogger.info("formattedPNL ", formattedPNL); - const formattedPNLUSD = `${pnlUSD < 0 ? '-' : ''}${formattedPNL}` + const formattedPNLUSD = `${pnlUSD < 0 ? "-" : ""}${formattedPNL}`; elizaLogger.info("formattedPNLUSD ", formattedPNLUSD); - return formattedPNLUSD - } + return formattedPNLUSD; +}; export default CoinbaseClientInterface; \ No newline at end of file