diff --git a/packages/plugin-ordinals/src/actions/runes/price-information.ts b/packages/plugin-ordinals/src/actions/runes/price-information.ts new file mode 100644 index 0000000000..f2ff6416f7 --- /dev/null +++ b/packages/plugin-ordinals/src/actions/runes/price-information.ts @@ -0,0 +1,93 @@ +import { + ActionExample, + HandlerCallback, + IAgentRuntime, + Memory, + State, + type Action, + generateObject, + composeContext, + ModelClass, +} from "@elizaos/core"; +import API from "../../utils/api"; +import { z } from "zod"; +import { runePriceInformationTemplate } from "../../templates"; +import { dollarFormatter, handleError } from "../../utils"; +import BigNumber from "bignumber.js"; + +export const runeSchema = z.object({ + rune: z.string(), +}); + +export default { + name: "GET_RUNE_PRICE", + similes: ["GET_PRICE_RUNE", "RETRIEVE_RUNE_PRICE"], + validate: async () => { + return true; + }, + description: "Retrieves the agents Ordinals wallet's runes portfolio.", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + try { + const context = composeContext({ + state, + template: runePriceInformationTemplate, + }); + + const content: { object: { rune?: string } } = await generateObject( + { + runtime, + context, + schema: runeSchema, + modelClass: ModelClass.LARGE, + } + ); + + const rune = content?.object?.rune; + + if (!rune) { + throw new Error("Unable to find rune name in messages."); + } + + const api = new API(); + + const priceInfo = await api.getRunePrice(rune); + + const currentPrice = priceInfo.pricePerToken; + const totalSupply = priceInfo?.supply?.current; + const marketcap = new BigNumber(currentPrice) + .multipliedBy(new BigNumber(totalSupply)) + .toNumber(); + + callback({ + text: `The price of ${rune} is ${dollarFormatter.format(currentPrice)} per token with a market cap of: ${dollarFormatter.format(marketcap)}`, + }); + + return true; + } catch (error) { + handleError(error, callback); + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "What is the price of GIZMO•IMAGINARY•KITTEN ?", + }, + }, + { + user: "{{user2}}", + content: { + text: "The price of GIZMO•IMAGINARY•KITTEN is:", + action: "GET_RUNE_PRICE", + }, + }, + ], + ] as ActionExample[][], +} as Action; diff --git a/packages/plugin-ordinals/src/actions/runes/transfer.ts b/packages/plugin-ordinals/src/actions/runes/transfer.ts index 3069ab0048..83d99a2cc4 100644 --- a/packages/plugin-ordinals/src/actions/runes/transfer.ts +++ b/packages/plugin-ordinals/src/actions/runes/transfer.ts @@ -92,7 +92,7 @@ export default { // Send 500 UNCOMMON•GOODS to bc1pud2j5tpy5s3c5u6y7e2lqn8tp5208q0mmxjtjqncmzp9wyj5gssswnz8nk // { rune: 'WHICH RUNE', amount: 'WHAT AMOUNT', toAddress: ''} - const api = new API(runtime.getSetting("ORDISCAN_API_KEY")); + const api = new API(); const runeInfo = await api.getRuneInfo(nonSpacedName); elizaLogger.info(JSON.stringify(runeInfo)); diff --git a/packages/plugin-ordinals/src/index.ts b/packages/plugin-ordinals/src/index.ts index 0f58fe5bf8..de0907c2a3 100644 --- a/packages/plugin-ordinals/src/index.ts +++ b/packages/plugin-ordinals/src/index.ts @@ -1,8 +1,8 @@ -import { elizaLogger, Plugin } from "@elizaos/core"; -import { walletProvider } from "./providers/wallet.ts"; +import { Plugin } from "@elizaos/core"; import { WalletProvider } from "./providers/wallet.ts"; import runePortfolio from "./actions/runes/portfolio.ts"; import runeTransfer from "./actions/runes/transfer.ts"; +import runePrice from "./actions/runes/price-information.ts"; import walletAddress from "./actions/wallet/address.ts"; import walletBalance from "./actions/wallet/balance.ts"; import walletUtxos from "./actions/wallet/utxo.ts"; @@ -19,6 +19,7 @@ export const ordinalsPlugin: Plugin = { walletUtxos, txStatus, runePortfolio, + runePrice, ], evaluators: [], providers: [], diff --git a/packages/plugin-ordinals/src/templates.ts b/packages/plugin-ordinals/src/templates.ts index b8e3d0a79f..418b07e7b1 100644 --- a/packages/plugin-ordinals/src/templates.ts +++ b/packages/plugin-ordinals/src/templates.ts @@ -1,3 +1,24 @@ +export const runePriceInformationTemplate = ` +## Recent Messages + +{{recentMessages}} + +Knowledge: + +- A Rune looks similar to: UNCOMMON•GOODS + +Given the most recent message, extract the rune: +- **rune** (string | null): The Rune + +Provide the values in the following JSON format: + +\`\`\`json +{ + "rune": "the rune or null", +} +\`\`\` +`; + export const runeTransferTemplate = ` ## Recent Messages diff --git a/packages/plugin-ordinals/src/types/index.ts b/packages/plugin-ordinals/src/types/index.ts index 334da960df..261cd5b763 100644 --- a/packages/plugin-ordinals/src/types/index.ts +++ b/packages/plugin-ordinals/src/types/index.ts @@ -17,3 +17,40 @@ export interface IBalance { priceChangePercentage24h: number; currentPrice: number; } + +export interface IRuneInfo { + id: string; + name: string; + spaced_name: string; + number: number; + divisibility: number; + symbol: string; + turbo: boolean; + mint_terms: { + amount: string; + cap: string; + height_start: number; + height_end: number; + offset_start: number; + offset_end: number; + }; + supply: { + current: string; + minted: string; + total_mints: string; + mint_percentage: string; + mintable: boolean; + burned: string; + total_burns: string; + premine: string; + }; + location: { + block_hash: string; + block_height: number; + tx_id: string; + tx_index: number; + vout: number; + output: string; + timestamp: number; + }; +} diff --git a/packages/plugin-ordinals/src/utils/api.ts b/packages/plugin-ordinals/src/utils/api.ts index 5092950e13..f97b1ed4f3 100644 --- a/packages/plugin-ordinals/src/utils/api.ts +++ b/packages/plugin-ordinals/src/utils/api.ts @@ -1,7 +1,6 @@ import { elizaLogger } from "@elizaos/core"; -import { IBalance } from "../types"; +import { IBalance, IRuneInfo } from "../types"; -const ORDISCAN_BASE_URL = "https://api.ordiscan.com/v1"; const HIRO_BASE_URL = "https://api.hiro.so"; const fetcher = async (url: string, apiKey?: string) => { @@ -27,20 +26,15 @@ const fetcher = async (url: string, apiKey?: string) => { }; class API { - ordiscanApiKey: string; - - constructor(ordiscanApiKey?: string) { - this.ordiscanApiKey = ordiscanApiKey; - } - async getRunesPortfolio(address: string): Promise { return await fetcher( `https://api-3.xverse.app/v2/address/${address}/rune-balance?includeUnconfirmed=true` ); } - async getRuneInfo(name: string) { - return fetcher(`${HIRO_BASE_URL}/runes/v1/etchings/${name}`); + async getRuneInfo(name: string): Promise { + const nonSpacedName = name?.replaceAll("•", ""); + return await fetcher(`${HIRO_BASE_URL}/runes/v1/etchings/${nonSpacedName}`); } async getRunesUtxos(address: string, runeName: string) { @@ -54,6 +48,22 @@ class API { // `https://api-3.xverse.app/v1/market/address/${address}/rune/${runeName}/utxos` // ); } + + async getRunePrice( + name: string + ): Promise { + const info = await this.getRuneInfo(name); + const priceInfo = await fetcher( + `https://api-3.xverse.app/v1/runes/fiat-rates?currency=USD&runeIds[]=${info?.id}` + ); + + const pricePerToken = + priceInfo?.[ + `${info?.location?.block_height}:${info?.location?.tx_index}` + ].USD; + + return { ...info, pricePerToken }; + } } export default API; diff --git a/packages/plugin-ordinals/src/utils/index.ts b/packages/plugin-ordinals/src/utils/index.ts index 13b770c845..8065830f0a 100644 --- a/packages/plugin-ordinals/src/utils/index.ts +++ b/packages/plugin-ordinals/src/utils/index.ts @@ -82,4 +82,5 @@ export const estimateTransactionSize = ( export const dollarFormatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', + maximumFractionDigits: 8 });