diff --git a/characters/podai.character.json b/characters/podai.character.json index 8169a0e3472cc..3bb01562ca921 100644 --- a/characters/podai.character.json +++ b/characters/podai.character.json @@ -16,7 +16,11 @@ "knowledge": [ "knows many top/famous token/coin informcation", "knows the price of the many tokens", - "knows the trend of the web3 tokens" + "knows the trend of the web3 tokens", + "The Arena is a next gen SocialFi app redefining how creators connect, engage, and monetize their content.", + "The Arena is an Avalanche-based social protocol, formerly known as Stars Arena, which was acquired by a new team in November 2023 and rebuilt after being on the verge of collapse.", + "Arena has more than 200,000 registered users, has paid more than $6 million to content creators, and has exceeded $100 million in transaction volume by Q4 2024.", + "Arena Social operates on the Avalanche blockchain, leveraging its high-performance consensus mechanism and low latency to ensure fast and secure operations." ], "messageExamples": [ ], diff --git a/packages/client-direct/package.json b/packages/client-direct/package.json index deb222ef37372..f45561ef4f24f 100644 --- a/packages/client-direct/package.json +++ b/packages/client-direct/package.json @@ -20,10 +20,12 @@ ], "dependencies": { "@elizaos/core": "workspace:*", + "@elizaos/plugin-avalanche": "workspace:*", + "@elizaos/plugin-binance": "workspace:*", + "@elizaos/plugin-data-enrich": "workspace:*", "@elizaos/plugin-image-generation": "workspace:*", "@elizaos/plugin-tee-verifiable-log": "workspace:*", "@elizaos/plugin-tee-log": "workspace:*", - "@elizaos/plugin-data-enrich": "workspace:*", "@elizaos/client-twitter": "workspace:*", "agent-twitter-client": "0.0.18", "@privy-io/server-auth":"1.18.1", @@ -32,9 +34,13 @@ "@types/express": "5.0.0", "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.95.8", - "@elizaos/plugin-binance": "workspace:*", "@binance/connector": "^3.6.0", + "@elysiajs/swagger": "^1.2.0", + "@mysten/sui": "^1.21.1", "solana-agent-kit": "^1.2.0", + "elysia": "^1.2.12", + "ethers": "^6.13.5", + "starknet": "6.18.0", "body-parser": "1.20.3", "cors": "2.8.5", "discord.js": "14.16.3", diff --git a/packages/client-direct/src/routes.ts b/packages/client-direct/src/routes.ts index 9375b4f64238e..235bf5e8a30ee 100644 --- a/packages/client-direct/src/routes.ts +++ b/packages/client-direct/src/routes.ts @@ -34,6 +34,7 @@ import { transferStarknetToken } from "../../plugin-data-enrich/src/starknet"; import { MemoController } from "./memo"; import { requireAuth } from "./auth"; import { CoinAnalysisObj, KEY_BNB_CACHE_STR } from "../../client-twitter/src/sighter"; +import { ArenaAnalysisObj, KEY_ARENA_CACHE_STR } from "../../client-twitter/src/arena"; //import { ethers } from 'ethers'; //import { requireAuth } from "./auth"; @@ -210,6 +211,10 @@ export class Routes { "/:agentId/bnb_query", this.handleBnbQuery.bind(this) ); + app.get( + "/:agentId/arena_query", + this.handleArenaQuery.bind(this) + ); app.post( "/:agentId/twitter_profile_search", this.handleTwitterProfileSearch.bind(this) @@ -630,6 +635,52 @@ export class Routes { }); } } + + async handleArenaQuery(req: express.Request, res: express.Response) { + const kol = typeof req.query.username === 'string' ? req.query.username : ''; + const kolname = kol.trim(); + if (!kolname) { + throw new ApiError(533, "kolname is blank"); + } + console.log("handleArenaQuery, kolname: " + kolname); + const runtime = await this.authUtils.getRuntime(req.params.agentId); + let userId = "blank"; + twEventCenter.emit("MSG_ARENA_QUERY", kolname, userId); + let anaObj: ArenaAnalysisObj = null; + + for (let i = 0; i < 10; i++) { + await this.sleep(1000); + const cached = await runtime.cacheManager.get(KEY_ARENA_CACHE_STR + kolname); + // console.log("handleArenaQuery, cached: " + cached); + if (cached && typeof cached === 'string') { + try { + anaObj = JSON.parse(cached); + if (anaObj) { + if (Date.now() - anaObj.timestamp > 3000) { + continue; + } else { + break; + } + } + } catch (error) { + console.error('JSON parse failed: ', error); + } + } + } + + if (anaObj && anaObj.coin_analysis && anaObj.coin_prediction) { + res.json({ + coin_analysis: anaObj.coin_analysis, + coin_prediction: anaObj.coin_prediction, + }); + } + else { + res.json({ + res: false, + reason: "try again", + }); + } + } async handleTwitterProfileSearch( req: express.Request, @@ -670,9 +721,6 @@ export class Routes { username: item?.username, name: item?.name, avatar: item?.avatar, - //avatar: "https://pbs.twimg.com/profile_images/898967039301349377/bLmMDwtf.jpg", - //avatar: "https://abs.twimg.com/sticky/default_profile_images/default_profile.png", - //avatar: "https://pbs.twimg.com/profile_images/1809130917350494209/Q_WjcqLz.jpg"; }; if (item?.username) { @@ -1198,7 +1246,7 @@ export class Routes { return res.json({ success: true, signature, - data: "ETH reward processed", + data: "ETH eco reward processed", }); case "sui": // Handle SUI transfer diff --git a/packages/client-twitter/package.json b/packages/client-twitter/package.json index e326d19401054..8c179b08bcceb 100644 --- a/packages/client-twitter/package.json +++ b/packages/client-twitter/package.json @@ -22,6 +22,7 @@ "@elizaos/core": "workspace:*", "@elizaos/plugin-data-enrich": "workspace:*", "@elizaos/plugin-binance": "workspace:*", + "@elizaos/plugin-avalanche": "workspace:*", "@binance/connector": "^3.6.0", "agent-twitter-client": "0.0.18", "discord.js": "14.16.3", diff --git a/packages/client-twitter/src/arena.ts b/packages/client-twitter/src/arena.ts new file mode 100644 index 0000000000000..a157823b34202 --- /dev/null +++ b/packages/client-twitter/src/arena.ts @@ -0,0 +1,137 @@ +import { UserManager, ConsensusProvider } from "@elizaos/plugin-data-enrich"; +import { + avalanchePlugin +} from "@elizaos/plugin-avalanche"; +import { + elizaLogger, + generateText, + IAgentRuntime, + ModelClass, +} from "@elizaos/core"; +import { ClientBase } from "./base"; +import { SearchMode } from "agent-twitter-client"; +import { UserResponce } from "../../plugin-avalanche/src/types/index"; + +export const KEY_ARENA_CACHE_STR = "key_arena_res_cache_"; + +export class ArenaAnalysisObj { + public coin_analysis: string; + public coin_prediction: string; + public timestamp: number; + public token: string; + constructor(token: string, analysis: string, prediction: string) { + this.token = token; + this.coin_analysis = analysis; + this.coin_prediction = prediction; + this.timestamp = Date.now(); + } +} + +export class ArenaClient { + client: ClientBase; + runtime: IAgentRuntime; + userManager: UserManager; + + constructor(client: ClientBase, runtime: IAgentRuntime) { + this.client = client; + this.runtime = runtime; + this.userManager = new UserManager(this.runtime.cacheManager); + this.sendingTwitterDebug = false; + } + + intervalId: NodeJS.Timeout; + sendingTwitterDebug: boolean; + + async start() { + console.log("Arena Query start"); + if (!this.client.profile) { + await this.client.init(); + } + } + async sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + async extractBraceContent(input: string): Promise { + const regex = /\{.*\}/; + const match = input.match(regex); + return match ? match[0] : ''; + } + + async arenaQuery(username: string, userId: any) { + // 1. get param. 2 get prompt. 3. get tweet info. 4. get arena info. 5. get ai answer. + + console.log("arenaQuery, in arana. username: " + username); + const promptHeader = "You are a cryptocurrency expert with extensive experience in cryptocurrency trading and frequently active in various cryptocurrency communities. You are now providing an analysis based on the following two social media platforms. The first one is some dynamics of X account, and the second one is the first web3 social media associated with X. I will first provide the updates of X account"; + const promptFooter = " please use 100 - word English texts respectively to analyze the reasons for the current price trend. The response format should be formatted as a JSON block as follows: {\"coin_analysis\": \"{coin_analysis}\"}. No other text should be provided, No need to use markdown syntax, just return JSON directly."; + + const tweetsres = await this.client.fetchSearchTweets( + username, + 20, SearchMode.Latest + ); + const promptTweet = + ` +Here are some tweets/replied: +${[...tweetsres?.tweets] + .filter((tweet) => { + // ignore tweets where any of the thread tweets contain a tweet by the bot + const thread = tweet.thread; + const botTweet = thread.find( + (t) => + t.username === + this.runtime.getSetting("TWITTER_USERNAME") + ); + return !botTweet; + }) + .map( + (tweet) => ` +From: ${tweet.name} (@${tweet.username}) +Text: ${tweet.text}\n +Likes: ${tweet.likes}, Replies: ${tweet.replies}, Retweets: ${tweet.retweets}, + ` + ) + .join("\n")} +`; + + + let promptArena = `There is another web3 social networking site based on X website below, with the same account. It can send content and also capture X's dynamics on the same account. The user associated with this account has corresponding cryptocurrency prices and user information, as shown below`; + const { actions } = avalanchePlugin; + let aranaQueryAction = null; + actions.forEach(action => { + // console.log( "arenaQuery, action.name: " + action.name ); + if(action.name === 'PROFILE_CHECK') { + aranaQueryAction = action; + } + }); + + const arenaOptions: Record = { + kolname: username, + }; + const data = await aranaQueryAction.handler(this.runtime, null, null, arenaOptions, null); + + if(data) { + promptArena += JSON.stringify(data) + } + console.log("arenaQuery, in arenaQuery. promt:\npromptHeader" + + promptHeader + "\n promptTweet " + + promptTweet + "\n promptArena: " + promptArena + "\n promptFooter " + promptFooter); + + let responseStr = await generateText({ + runtime: this.runtime, + context: promptHeader + promptTweet + promptArena + promptFooter, + modelClass: ModelClass.LARGE, + }); + console.log("arenaQuery, in arenaQuery. responseStr: ", responseStr); + + let responseObj = null; + try { + responseObj = JSON.parse(responseStr); + } catch (error) { + responseObj = null; + console.error('JSON parse error: ', error.message); + } + if (responseObj) { + const anaobj = new ArenaAnalysisObj(username, responseObj?.coin_analysis, "empty"); + await this.runtime.cacheManager.set(KEY_ARENA_CACHE_STR + username, JSON.stringify(anaobj)); + } + } +} diff --git a/packages/client-twitter/src/index.ts b/packages/client-twitter/src/index.ts index 14174262830a2..64632ebf42ff0 100644 --- a/packages/client-twitter/src/index.ts +++ b/packages/client-twitter/src/index.ts @@ -7,6 +7,7 @@ import { TwitterSearchClient } from "./search.ts"; import { TwitterSpaceClient } from "./spaces.ts"; import { TwitterWatchClient } from "./watcher.ts"; import { SighterClient, KEY_BNB_CACHE_STR } from "./sighter.ts"; +import { ArenaClient } from "./arena.ts"; import { TwitterFinderClient } from "./finder.ts"; import { EventEmitter } from 'events'; @@ -26,6 +27,7 @@ class TwitterManager { space?: TwitterSpaceClient; watcher: TwitterWatchClient; sighter: SighterClient; + arena: ArenaClient; finder: TwitterFinderClient; constructor(runtime: IAgentRuntime, twitterConfig: TwitterConfig) { @@ -49,6 +51,7 @@ class TwitterManager { this.interaction = new TwitterInteractionClient(this.client, runtime); this.sighter = new SighterClient(this.client, runtime); + this.arena = new ArenaClient(this.client, runtime); // Optional Spaces logic (enabled if TWITTER_SPACES_ENABLE is true) if (twitterConfig.TWITTER_SPACES_ENABLE) { @@ -95,6 +98,7 @@ export const TwitterClientInterface: Client = { await manager.finder.start(); await manager.watcher.start(); + await manager.arena.start(); twEventCenter.on('MSG_RE_TWITTER', (text, userId) => { console.log('MSG_RE_TWITTER userId: ' + userId + " text: " + text); manager.watcher.sendReTweet(text, userId); @@ -103,7 +107,10 @@ export const TwitterClientInterface: Client = { // console.log('MSG_RE_TWITTER userId: ' + userId + " text: " + text); manager.sighter.bnbQuery(coinsymbol, userId); }); - + twEventCenter.on("MSG_ARENA_QUERY", (username, userId) => { + // console.log('MSG_RE_TWITTER userId: ' + userId + " text: " + text); + manager.arena.arenaQuery(username, userId); + }); return manager; }, diff --git a/packages/plugin-avalanche/src/actions/arena.ts b/packages/plugin-avalanche/src/actions/arena.ts new file mode 100644 index 0000000000000..5baef4f41085d --- /dev/null +++ b/packages/plugin-avalanche/src/actions/arena.ts @@ -0,0 +1,73 @@ +import { + type Action, + type ActionExample, + type IAgentRuntime, + type Memory, + type State, + type HandlerCallback, + elizaLogger, + type Content, + } from "@elizaos/core"; + import { UserResponse } from "../types"; + + export interface ProfileContent extends Content { + depositTokenAddress: string; + strategyAddress: string; + amount: string | number; + } + + export default { + name: "PROFILE_CHECK", + similes: ["DEPOSIT_FOR_AVALANCHE", "DEPOSIT_PROFILE"], + validate: async (runtime: IAgentRuntime, _message: Memory) => { + return true; + }, + description: + "get arena socila info.", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + + elizaLogger.log("Starting PROFILE_CHECK handler..."); + const username = _options['kolname'] as string; + try { + const baseUrl = 'https://api.starsarena.com/user/handle'; + const response = await fetch(`${baseUrl}?handle=${username}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + if (!response.ok) { + return null; + } + const data = await response.json(); + const userinfo = data?.user; + if (userinfo) { + return userinfo as UserResponse; + } + } catch (error) { + console.error('Error fetching user info:', error); + } + return null; + }, + examples: [ + [ + { + user: "{{user1}}", + content: { text: "Deposit 1 USDC into the strategy" }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Deposit 10 gmYAK to earn yield" }, + }, + ], + ] as ActionExample[][], + } as Action; diff --git a/packages/plugin-avalanche/src/index.ts b/packages/plugin-avalanche/src/index.ts index 4f6c683da7c40..60995809e6ec5 100644 --- a/packages/plugin-avalanche/src/index.ts +++ b/packages/plugin-avalanche/src/index.ts @@ -3,6 +3,7 @@ import tokenMillCreate from "./actions/tokenMillCreate"; import transfer from "./actions/transfer"; import yakSwap from "./actions/yakSwap"; import yakStrategy from "./actions/yakStrategy"; +import arena from "./actions/arena"; import { tokensProvider } from "./providers/tokens"; import { strategiesProvider } from "./providers/strategies"; import { walletProvider } from "./providers/wallet"; @@ -23,9 +24,9 @@ export const PROVIDER_CONFIG = { export const avalanchePlugin: Plugin = { name: "avalanche", description: "Avalanche Plugin for Eliza", - actions: [transfer, yakSwap, yakStrategy, tokenMillCreate], + actions: [transfer, yakSwap, yakStrategy, tokenMillCreate, arena], evaluators: [], - providers: [tokensProvider, strategiesProvider, walletProvider], + providers: [tokensProvider, strategiesProvider, walletProvider ], }; export default avalanchePlugin; diff --git a/packages/plugin-avalanche/src/types/index.ts b/packages/plugin-avalanche/src/types/index.ts index b1104592eac05..78b6bde276c2d 100644 --- a/packages/plugin-avalanche/src/types/index.ts +++ b/packages/plugin-avalanche/src/types/index.ts @@ -32,4 +32,42 @@ interface TokenMillMarketCreationParameters { args: string; } -export type { YakSwapQuote, TokenMillMarketCreationParameters }; +interface UserResponse { + + id: string; + createdOn: string; + twitterId: string; + twitterHandle: string; + twitterName: string; + twitterPicture: string; + lastLoginTwitterPicture: string; + bannerUrl: string | null; + address: string; + addressBeforeDynamicMigration: string; + dynamicAddress: string; + ethereumAddress: string | null; + solanaAddress: string; + prevAddress: string; + addressConfirmed: boolean; + twitterDescription: string; + signedUp: boolean; + subscriptionCurrency: string; + subscriptionCurrencyAddress: string | null; + subscriptionPrice: string; + keyPrice: string; + lastKeyPrice: string; + threadCount: number; + followerCount: number; + followingsCount: number; + twitterFollowers: number; + subscriptionsEnabled: boolean; + userConfirmed: boolean; + twitterConfirmed: boolean; + flag: number; + ixHandle: string; + handle: string | null; + following: boolean | null; + follower: boolean | null; +} + +export type { YakSwapQuote, TokenMillMarketCreationParameters, UserResponse }; diff --git a/packages/plugin-data-enrich/src/bsc.ts b/packages/plugin-data-enrich/src/bsc.ts new file mode 100644 index 0000000000000..0445a87518d4b --- /dev/null +++ b/packages/plugin-data-enrich/src/bsc.ts @@ -0,0 +1,36 @@ +import { ethers } from 'ethers'; +import { settings } from "@elizaos/core"; + +// BSC mainnet provider URL (you can get this from services like Infura or Alchemy) +const provider = new ethers.providers.JsonRpcProvider("https://bsc-dataseed.binance.org/"); + +// Token contract address and ABI +const tokenAbi = [ + "function transfer(address to, uint256 amount) returns (bool)" +]; + +async function transferBscToken(toAddress: string, amountString: string): Promise { + try { + const contractAddress = settings.BSC_CONTRACT_ADDRESS; + const privateKey = settings.BSC_PRIVATE_KEY; + //const accountAddress = settings.BSC_ACCOUNT_ADDRESS; + const wallet = new ethers.Wallet(privateKey, provider); + + const contract = new ethers.Contract(contractAddress, tokenAbi, wallet); + + // The amount to send, adjust according to your token's decimals + const amount = ethers.utils.parseUnits(amountString, 18); + + console.log("Starting transfer..."); + const tx = await contract.transfer(toAddress, amount); + console.log(`Transaction hash: ${tx.hash}`); + + // Wait for the transaction to be confirmed + await tx.wait(); + console.log("Transfer successful!"); + return tx.hash; + } catch (error) { + console.error("Transfer failed:", error); + throw new Error(`BSC Transaction Error: ${error.message}`); + } +} diff --git a/packages/plugin-data-enrich/src/eth.ts b/packages/plugin-data-enrich/src/eth.ts index 3a9213db388a7..2d54c12526276 100644 --- a/packages/plugin-data-enrich/src/eth.ts +++ b/packages/plugin-data-enrich/src/eth.ts @@ -14,34 +14,34 @@ export async function transferEthToken( ): Promise { try { // BSC mainnet provider URL (you can get this from services like Infura or Alchemy) - let provider = new ethers.providers.JsonRpcProvider(settings.ETH_RPC); + let provider = null; let contractAddress = settings.ETH_CONTRACT_ADDRESS; let privateKey = settings.ETH_PRIVATE_KEY; //const accountAddress = settings.BSC_ACCOUNT_ADDRESS; switch (chainType) { case "eth": - provider = new ethers.providers.JsonRpcProvider(settings.ETH_RPC); + provider = new ethers.JsonRpcProvider(settings.ETH_RPC); contractAddress = settings.ETH_CONTRACT_ADDRESS; privateKey = settings.ETH_PRIVATE_KEY; break; case "bsc": - provider = new ethers.providers.JsonRpcProvider(settings.BSC_RPC); + provider = new ethers.JsonRpcProvider(settings.BSC_RPC); contractAddress = settings.BSC_CONTRACT_ADDRESS; privateKey = settings.BSC_PRIVATE_KEY; break; case "base": - provider = new ethers.providers.JsonRpcProvider(settings.BASE_RPC); + provider = new ethers.JsonRpcProvider(settings.BASE_RPC); contractAddress = settings.BASE_CONTRACT_ADDRESS; privateKey = settings.BASE_PRIVATE_KEY; break; case "mantle": - provider = new ethers.providers.JsonRpcProvider(settings.MANTLE_RPC); + provider = new ethers.JsonRpcProvider(settings.MANTLE_RPC); contractAddress = settings.MANTLE_CONTRACT_ADDRESS; privateKey = settings.MANTLE_PRIVATE_KEY; break; default: - provider = new ethers.providers.JsonRpcProvider(settings.ETH_RPC); + provider = new ethers.JsonRpcProvider(settings.ETH_RPC); contractAddress = settings.ETH_CONTRACT_ADDRESS; privateKey = settings.ETH_PRIVATE_KEY; break; @@ -52,10 +52,13 @@ export async function transferEthToken( const contract = new ethers.Contract(contractAddress, tokenAbi, wallet); // The amount to send, adjust according to your token's decimals - const amount = ethers.utils.parseUnits(amountString, 18); + const amount = ethers.parseUnits(amountString, 18); console.log("Starting transfer..."); - const tx = await contract.transfer(toAddress, amount); + const tx = await contract.transfer(toAddress, amount/*, { + gasLimit: 100000, // Set Gas Limit + gasPrice: ethers.parseUnits("20", "gwei"), //Gas Price + }*/); console.log(`Transaction hash: ${tx.hash}`); // Wait for the transaction to be confirmed diff --git a/packages/plugin-data-enrich/src/infermessage.ts b/packages/plugin-data-enrich/src/infermessage.ts index 33c449e012b76..661d439f8c4f0 100644 --- a/packages/plugin-data-enrich/src/infermessage.ts +++ b/packages/plugin-data-enrich/src/infermessage.ts @@ -56,7 +56,7 @@ export class InferMessageProvider { path.join(InferMessageProvider.cacheKey, key), data, { - expires: Date.now() + 24 * 60 * 60 * 1000, + expires: Date.now() + 24 * 60 * 60 * 1000 * 7, // a week } ); }