├── images ├── logo.jpg └── banner.jpg ├── screenshot ├── mint.png ├── connect.png ├── status.png ├── transfer.png ├── disconnect.png ├── auction-data.png ├── create-pool.png ├── jetton-burn.png ├── jetton-mint.png ├── lending_info.png ├── nft-auction.png ├── nft-transfer.png ├── jetton-get-data.png ├── jetton-transfer.png ├── nft-bid-and-buy.png ├── jetton-wallet-data.png ├── deploy-jetton-minter.png ├── get-colleciton-data.png ├── ston-swap-confirmed.png ├── nft-listing-and-cancel.png ├── nft-ownership-transfer.png ├── jetton-transfer-subaction.png └── ton-jetton-batch-transfer.png ├── src ├── services │ ├── staking │ │ ├── strategies │ │ │ ├── hipo │ │ │ │ ├── sdk │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Constants.ts │ │ │ │ │ ├── Parent.ts │ │ │ │ │ ├── Wallet.ts │ │ │ │ │ ├── Helpers.ts │ │ │ │ │ └── Treasury.ts │ │ │ │ └── README.md │ │ │ ├── hipo.ts │ │ │ └── tonWhales.ts │ │ ├── config │ │ │ └── platformConfig.ts │ │ ├── interfaces │ │ │ ├── pool.ts │ │ │ └── stakingPlatform.ts │ │ └── platformFactory.ts │ └── nft-marketplace │ │ ├── interfaces │ │ └── listings.ts │ │ ├── listingData.ts │ │ └── listingTransactions.ts ├── tests │ ├── simple.test.ts │ ├── queryStonAsset.test.ts │ ├── wallet.test.ts │ ├── evaaRepay.test.ts │ ├── evaaSupply.test.ts │ ├── evaaBorrow.test.ts │ ├── evaaWithdraw.test.ts │ ├── stake.test.ts │ ├── evaaPositions.test.ts │ ├── unstake.test.ts │ ├── fetchPrice.test.ts │ ├── auctionInteraction.test.ts │ ├── getPoolInfo.test.ts │ ├── index.test.ts │ ├── actions │ │ └── transfer.test.ts │ ├── test-utils.ts │ ├── providers │ │ └── tokenProvider.test.ts │ └── swap.test.ts ├── providers │ ├── dexes │ │ ├── index.ts │ │ └── dex.d.ts │ └── ston.ts ├── utils │ ├── formatting.ts │ ├── testnetStonAssets.ts │ ├── modifyMemories.ts │ ├── JettonMinterUtils.ts │ ├── NFTItem.ts │ ├── JettonWallet.ts │ ├── NFTCollection.ts │ └── util.ts ├── cache.ts ├── types.ts ├── enviroment.ts ├── index.ts └── actions │ ├── getPoolInfo.ts │ ├── buyListing.ts │ ├── stake.ts │ ├── loadWallet.ts │ ├── cancelListing.ts │ ├── unstake.ts │ ├── createWallet.ts │ ├── tokenPrice.ts │ └── createListing.ts ├── .prettierrc ├── tsconfig.build.json ├── tsup.config.ts ├── .npmignore ├── .gitignore ├── tsconfig.json ├── LICENSE ├── scripts ├── generate-ton-mnemonic.ts └── debug.sh ├── package.json ├── TUTORIAL.md ├── BUG-BOUNTY.md └── .github └── workflows └── npm-deploy.yml /images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/images/logo.jpg -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/images/banner.jpg -------------------------------------------------------------------------------- /screenshot/mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/mint.png -------------------------------------------------------------------------------- /screenshot/connect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/connect.png -------------------------------------------------------------------------------- /screenshot/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/status.png -------------------------------------------------------------------------------- /screenshot/transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/transfer.png -------------------------------------------------------------------------------- /screenshot/disconnect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/disconnect.png -------------------------------------------------------------------------------- /screenshot/auction-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/auction-data.png -------------------------------------------------------------------------------- /screenshot/create-pool.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/create-pool.png -------------------------------------------------------------------------------- /screenshot/jetton-burn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/jetton-burn.png -------------------------------------------------------------------------------- /screenshot/jetton-mint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/jetton-mint.png -------------------------------------------------------------------------------- /screenshot/lending_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/lending_info.png -------------------------------------------------------------------------------- /screenshot/nft-auction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/nft-auction.png -------------------------------------------------------------------------------- /screenshot/nft-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/nft-transfer.png -------------------------------------------------------------------------------- /screenshot/jetton-get-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/jetton-get-data.png -------------------------------------------------------------------------------- /screenshot/jetton-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/jetton-transfer.png -------------------------------------------------------------------------------- /screenshot/nft-bid-and-buy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/nft-bid-and-buy.png -------------------------------------------------------------------------------- /screenshot/jetton-wallet-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/jetton-wallet-data.png -------------------------------------------------------------------------------- /screenshot/deploy-jetton-minter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/deploy-jetton-minter.png -------------------------------------------------------------------------------- /screenshot/get-colleciton-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/get-colleciton-data.png -------------------------------------------------------------------------------- /screenshot/ston-swap-confirmed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/ston-swap-confirmed.png -------------------------------------------------------------------------------- /screenshot/nft-listing-and-cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/nft-listing-and-cancel.png -------------------------------------------------------------------------------- /screenshot/nft-ownership-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/nft-ownership-transfer.png -------------------------------------------------------------------------------- /screenshot/jetton-transfer-subaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/jetton-transfer-subaction.png -------------------------------------------------------------------------------- /screenshot/ton-jetton-batch-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elizaos-plugins/plugin-ton/HEAD/screenshot/ton-jetton-batch-transfer.png -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Constants"; 2 | 3 | export * from "./Helpers"; 4 | 5 | export * from "./Treasury"; 6 | 7 | export * from "./Parent"; 8 | 9 | export * from "./Wallet"; 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "es5", 4 | "singleQuote": false, 5 | "printWidth": 80, 6 | "tabWidth": 4, 7 | "useTabs": false, 8 | "bracketSpacing": true, 9 | "arrowParens": "always", 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /src/tests/simple.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | 3 | describe("Simple test", () => { 4 | it("should pass", () => { 5 | expect(1 + 1).toBe(2); 6 | }); 7 | 8 | it("should work with async", async () => { 9 | const result = await Promise.resolve(42); 10 | expect(result).toBe(42); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist", 6 | "sourceMap": true, 7 | "inlineSources": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true 10 | }, 11 | "include": ["src/**/*.ts"], 12 | "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] 13 | } 14 | 15 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts'], 5 | outDir: 'dist', 6 | tsconfig: './tsconfig.build.json', // Use build-specific tsconfig 7 | sourcemap: true, 8 | clean: true, 9 | format: ['esm'], // ESM output format 10 | dts: true, 11 | external: ['dotenv', 'fs', 'path', 'https', 'http', '@elizaos/core', 'zod'], 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /src/services/staking/config/platformConfig.ts: -------------------------------------------------------------------------------- 1 | export const PLATFORM_TYPES = ["TON_WHALES", "HIPO"] as const; 2 | export type PlatformType = (typeof PLATFORM_TYPES)[number]; 3 | 4 | export const STAKING_POOL_ADDRESSES: Record = { 5 | TON_WHALES: [ 6 | "kQDV1LTU0sWojmDUV4HulrlYPpxLWSUjM6F3lUurMbwhales", 7 | "kQAHBakDk_E7qLlNQZxJDsqj_ruyAFpqarw85tO-c03fK26F", 8 | ], 9 | HIPO: ["kQAlDMBKCT8WJ4nwdwNRp0lvKMP4vUnHYspFPhEnyR36cg44"], 10 | }; 11 | -------------------------------------------------------------------------------- /src/providers/dexes/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./torchFinance"; 2 | export * from "./dedust"; 3 | export * from "./stonFi"; 4 | export type * from "./dex.d.ts"; 5 | 6 | export enum SupportedMethod { 7 | CREATE_POOL = "CREATE_POOL", 8 | DEPOSIT = "DEPOSIT", 9 | WITHDRAW = "WITHDRAW", 10 | CLAIM_FEE = "CLAIM_FEE", 11 | } 12 | export const SUPPORTED_DEXES = ["TORCH_FINANCE", "STON_FI", "DEDUST"]; 13 | 14 | export const isPoolSupported = (poolName: string) => 15 | SUPPORTED_DEXES.includes(poolName); 16 | -------------------------------------------------------------------------------- /src/utils/formatting.ts: -------------------------------------------------------------------------------- 1 | import { Address, fromNano } from "@ton/ton"; 2 | 3 | export const truncateTONAddress = (address: Address) => { 4 | const addressString = address.toString(); 5 | if (addressString.length <= 12) return addressString; 6 | return `${addressString.slice(0, 6)}...${addressString.slice(-6)}`; 7 | }; 8 | 9 | // Helper function to format numbers with 2 decimal places 10 | export const formatTON = (value: bigint) => { 11 | const num = parseFloat(fromNano(value)); 12 | return num.toFixed(2); 13 | }; 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source files 2 | src/ 3 | *.ts 4 | !dist/**/*.d.ts 5 | 6 | # Config files 7 | tsconfig.json 8 | tsup.config.ts 9 | .prettierrc 10 | .eslintrc* 11 | 12 | # Test files 13 | **/*.test.* 14 | **/*.spec.* 15 | coverage/ 16 | .nyc_output/ 17 | 18 | # Development files 19 | .env* 20 | .vscode/ 21 | .idea/ 22 | .turbo/ 23 | 24 | # Documentation source 25 | docs/ 26 | *.md 27 | !README.md 28 | 29 | # CI/CD 30 | .github/ 31 | .gitlab-ci.yml 32 | .travis.yml 33 | 34 | # Misc 35 | .DS_Store 36 | *.log 37 | *.tmp 38 | node_modules/ 39 | bun.lockb -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build outputs 5 | dist/ 6 | *.tsbuildinfo 7 | 8 | # Environment files 9 | .env 10 | .env.local 11 | .env.*.local 12 | 13 | # IDE 14 | .vscode/ 15 | .idea/ 16 | *.swp 17 | *.swo 18 | *~ 19 | 20 | # OS 21 | .DS_Store 22 | Thumbs.db 23 | 24 | # Logs 25 | *.log 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # Testing 31 | coverage/ 32 | .nyc_output/ 33 | 34 | # Temporary files 35 | *.tmp 36 | *.temp 37 | .cache/ 38 | .turbo/ 39 | 40 | # Package manager files 41 | yarn.lock 42 | package-lock.json 43 | pnpm-lock.yaml 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src", 6 | "baseUrl": "./", 7 | "lib": ["ESNext"], 8 | "target": "ESNext", 9 | "module": "Preserve", 10 | "moduleResolution": "Bundler", 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "allowImportingTsExtensions": true, 14 | "declaration": true, 15 | "emitDeclarationOnly": true, 16 | "resolveJsonModule": true, 17 | "moduleDetection": "force", 18 | "allowArbitraryExtensions": true, 19 | "types": ["bun"] 20 | }, 21 | "include": ["src/**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /src/services/staking/interfaces/pool.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "@ton/ton"; 2 | 3 | export type PoolMemberList = PoolMemberData[]; 4 | 5 | export interface PoolMemberData { 6 | address: Address; 7 | profit_per_coin: bigint; 8 | balance: bigint; 9 | pending_withdraw: bigint; 10 | pending_withdraw_all: boolean; 11 | pending_deposit: bigint; 12 | member_withdraw: bigint; 13 | } 14 | 15 | export interface PoolInfo { 16 | address: Address; 17 | min_stake: bigint; 18 | deposit_fee: bigint; 19 | withdraw_fee: bigint; 20 | balance: bigint; 21 | pending_deposits: bigint; 22 | pending_withdraws: bigint; 23 | } 24 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/sdk/Constants.ts: -------------------------------------------------------------------------------- 1 | import { Address } from "@ton/ton"; 2 | 3 | export const treasuryAddresses = new Map([ 4 | [ 5 | "mainnet", 6 | Address.parse("EQCLyZHP4Xe8fpchQz76O-_RmUhaVc_9BAoGyJrwJrcbz2eZ"), 7 | ], 8 | [ 9 | "testnet", 10 | Address.parse("kQAlDMBKCT8WJ4nwdwNRp0lvKMP4vUnHYspFPhEnyR36cg44"), 11 | ], 12 | ]); 13 | 14 | export const opDepositCoins = 0x3d3761a6; 15 | export const opUnstakeTokens = 0x595f07bc; 16 | 17 | export const feeStake = 100000000n; 18 | export const feeUnstake = 100000000n; 19 | 20 | export const minimumTonBalanceReserve = 200000000n; 21 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/sdk/Parent.ts: -------------------------------------------------------------------------------- 1 | import { Address, Contract, ContractProvider, TupleBuilder } from "@ton/ton"; 2 | 3 | export class Parent implements Contract { 4 | constructor(readonly address: Address) {} 5 | 6 | static createFromAddress(address: Address) { 7 | return new Parent(address); 8 | } 9 | 10 | async getWalletAddress( 11 | provider: ContractProvider, 12 | owner: Address 13 | ): Promise
{ 14 | const tb = new TupleBuilder(); 15 | tb.writeAddress(owner); 16 | const { stack } = await provider.get("get_wallet_address", tb.build()); 17 | return stack.readAddress(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import NodeCache from "node-cache"; 2 | 3 | // Create a singleton cache instance 4 | const cache = new NodeCache({ 5 | stdTTL: 600, // 10 minutes default TTL 6 | checkperiod: 120, // Check for expired keys every 2 minutes 7 | }); 8 | 9 | export const cacheManager = { 10 | get: (key: string): T | undefined => { 11 | return cache.get(key); 12 | }, 13 | 14 | set: (key: string, value: T, ttl?: number): boolean => { 15 | return cache.set(key, value, ttl ?? 600); 16 | }, 17 | 18 | delete: (key: string): number => { 19 | return cache.del(key); 20 | }, 21 | 22 | has: (key: string): boolean => { 23 | return cache.has(key); 24 | }, 25 | 26 | clear: (): void => { 27 | cache.flushAll(); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /src/services/staking/interfaces/stakingPlatform.ts: -------------------------------------------------------------------------------- 1 | import { Address, MessageRelaxed, TonClient } from "@ton/ton"; 2 | import { WalletProvider } from "../../../providers/wallet"; 3 | import { PoolInfo } from "./pool"; 4 | 5 | export interface StakingPlatform { 6 | readonly tonClient: TonClient; 7 | readonly walletProvider: WalletProvider; 8 | getStakedTon(walletAddress: Address, poolAddress: Address): Promise; 9 | getPendingWithdrawal( 10 | walletAddress: Address, 11 | poolAddress: Address 12 | ): Promise; 13 | getPoolInfo(poolAddress: Address): Promise; 14 | createStakeMessage( 15 | poolAddress: Address, 16 | amount: number 17 | ): Promise; 18 | createUnstakeMessage( 19 | poolAddress: Address, 20 | amount: number 21 | ): Promise; 22 | } 23 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/README.md: -------------------------------------------------------------------------------- 1 | # Local Hipo SDK Code 2 | 3 | This folder contains code copied from [hipo-finance/sdk](https://github.com/hipo-finance/sdk) due to module resolution conflicts between our build configuration (`moduleResolution: "Bundler"`) and the SDK's (`moduleResolution: "Node10"`). 4 | 5 | ## Attribution 6 | 7 | All code in this directory is from the [Hipo Finance SDK](https://github.com/hipo-finance/sdk), consisting of 5 core files for staking functionality. Original license applies. 8 | 9 | [Include Hipo's license here] 10 | 11 | ## Why Copy? 12 | 13 | Module resolution conflicts created circular dependency issues that were simpler to resolve by copying these small files rather than implementing complex workarounds. 14 | 15 | ## Files 16 | 17 | - [List the 5 files and their basic purposes] 18 | 19 | ## Updates 20 | 21 | Check original SDK for any updates to these files. 22 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/sdk/Wallet.ts: -------------------------------------------------------------------------------- 1 | import { Address, Contract, ContractProvider, Dictionary } from "@ton/ton"; 2 | 3 | export interface WalletState { 4 | tokens: bigint; 5 | staking: Dictionary; 6 | unstaking: bigint; 7 | } 8 | 9 | export class Wallet implements Contract { 10 | constructor(readonly address: Address) {} 11 | 12 | static createFromAddress(address: Address) { 13 | return new Wallet(address); 14 | } 15 | 16 | async getWalletState(provider: ContractProvider): Promise { 17 | const { stack } = await provider.get("get_wallet_state", []); 18 | return { 19 | tokens: stack.readBigNumber(), 20 | staking: Dictionary.loadDirect( 21 | Dictionary.Keys.BigUint(32), 22 | Dictionary.Values.BigVarUint(4), 23 | stack.readCellOpt() 24 | ), 25 | unstaking: stack.readBigNumber(), 26 | }; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/dexes/dex.d.ts: -------------------------------------------------------------------------------- 1 | import { JettonMaster } from "@ton/ton"; 2 | import { SupportedMethod } from "."; 3 | 4 | export type Token = { 5 | address: string; 6 | name: string; 7 | }; 8 | 9 | export interface DEX { 10 | supportMethods: readonly SupportedMethod[]; 11 | createPool?: (jettons: JettonMaster[]) => {}; 12 | // LP tokens should be issued 13 | deposit?: ( 14 | jettonDeposits: JettonDeposit[], 15 | tonAmount: number, 16 | params?: {} 17 | ) => {}; 18 | // LP tokens should be burned 19 | withdraw?: ( 20 | jettonWithdrawals: JettonWithdrawal[], 21 | isTon: boolean, 22 | amount: number, 23 | params?: {} 24 | ) => {}; 25 | claimFee?: (params: { isTon: boolean; jettons: JettonMaster[] }) => {}; 26 | } 27 | 28 | export type JettonDeposit = { 29 | jetton: JettonMaster; 30 | amount: number; 31 | }; 32 | 33 | export type JettonWithdrawal = JettonDeposit; 34 | 35 | export type TransactionHash = string; 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Eliza Contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface DexScreenerResponse { 2 | schemaVersion: string; 3 | pairs: { 4 | chainId: string; 5 | dexId: string; 6 | url: string; 7 | pairAddress: string; 8 | baseToken: { 9 | address: string; 10 | name: string; 11 | symbol: string; 12 | }; 13 | quoteToken: { 14 | address: string; 15 | name: string; 16 | symbol: string; 17 | }; 18 | priceNative: string; 19 | priceUsd: string; 20 | priceChange: { 21 | h1: number; 22 | h6: number; 23 | h24: number; 24 | }; 25 | }[]; 26 | } 27 | 28 | export interface TonApiRateResponse { 29 | rates: { 30 | [key: string]: { 31 | prices: { 32 | USD: number; 33 | }; 34 | diff_24h: { 35 | USD: string; 36 | }; 37 | diff_7d: { 38 | USD: string; 39 | }; 40 | diff_30d: { 41 | USD: string; 42 | }; 43 | }; 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/tests/queryStonAsset.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeAll } from "bun:test"; 2 | import { jest } from "bun:test"; 3 | import type { IAgentRuntime } from "@elizaos/core"; 4 | import { initStonProvider, type StonProvider } from "../providers/ston"; 5 | 6 | //const TON_RPC_URL = "https://testnet.toncenter.com/api/v2/jsonRPC"; 7 | const TON_RPC_URL = "https://toncenter.com/api/v2/jsonRPC"; 8 | 9 | describe("Query Ston Asset Action", () => { 10 | let stonProvider: StonProvider; 11 | let mockedRuntime: IAgentRuntime; 12 | 13 | beforeAll(async () => { 14 | mockedRuntime = { 15 | getSetting: jest.fn().mockImplementation((key) => { 16 | if (key == "TON_RPC_URL") return TON_RPC_URL; 17 | return undefined; 18 | }), 19 | } as unknown as IAgentRuntime; 20 | stonProvider = await initStonProvider(mockedRuntime); 21 | }); 22 | 23 | it("should successfully query TON asset and return asset information", async () => { 24 | const asset = await stonProvider.getAsset("TON"); 25 | expect(asset.kind).toBe("Ton"); 26 | }); 27 | 28 | it("should fail to query a non existent asset", async () => { 29 | await expect(stonProvider.getAsset("NonExistentAsset")).rejects.toThrow( 30 | "Asset NonExistentAsset not supported" 31 | ); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /scripts/generate-ton-mnemonic.ts: -------------------------------------------------------------------------------- 1 | // pnpm install @ton/ton @ton/crypto 2 | import { mnemonicNew, KeyPair, mnemonicToPrivateKey } from "@ton/crypto"; 3 | import { WalletContractV4 } from "@ton/ton"; 4 | 5 | async function start() { 6 | const amount = 2; 7 | const password = ""; 8 | 9 | const keyPairs = await Promise.all( 10 | Array.from({ length: amount }).map(async () => { 11 | const mnemonics: string[] = await mnemonicNew(24, password); 12 | const pair: KeyPair = await mnemonicToPrivateKey( 13 | mnemonics, 14 | password 15 | ); 16 | const wallet = WalletContractV4.create({ 17 | workchain: 0, 18 | publicKey: pair.publicKey, 19 | }); 20 | const formattedAddress = wallet.address.toString({ 21 | bounceable: false, 22 | urlSafe: true, 23 | }); 24 | 25 | return { 26 | mnemonic: mnemonics.join(" "), 27 | address: formattedAddress, 28 | version: "v4", 29 | publicKeyHash: pair.publicKey.toString("hex"), 30 | }; 31 | }) 32 | ); 33 | 34 | console.log(keyPairs); 35 | console.log(new Date().toISOString()); 36 | } 37 | 38 | start() 39 | .then(() => console.log("Done")) 40 | .catch((err) => console.error(err)); 41 | -------------------------------------------------------------------------------- /src/tests/wallet.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import loadWalletAction from "../actions/loadWallet"; 3 | 4 | describe("Load Wallet Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(loadWalletAction.name).toBe("RECOVER_TON_WALLET"); 7 | expect(loadWalletAction.description).toBe( 8 | "Loads an existing TON wallet from an encrypted backup file using the provided password." 9 | ); 10 | expect(loadWalletAction.similes).toContain("IMPORT_TON_WALLET"); 11 | expect(loadWalletAction.similes).toContain("RECOVER_WALLET"); 12 | }); 13 | 14 | it("should have validate function", () => { 15 | expect(typeof loadWalletAction.validate).toBe("function"); 16 | }); 17 | 18 | it("should have handler function", () => { 19 | expect(typeof loadWalletAction.handler).toBe("function"); 20 | }); 21 | 22 | it("should have examples", () => { 23 | expect(Array.isArray(loadWalletAction.examples)).toBe(true); 24 | expect(loadWalletAction.examples.length).toBeGreaterThan(0); 25 | 26 | // Check first example structure 27 | const firstExample = loadWalletAction.examples[0]; 28 | expect(Array.isArray(firstExample)).toBe(true); 29 | expect(firstExample.length).toBeGreaterThan(0); 30 | expect(firstExample[0]).toHaveProperty("user"); 31 | expect(firstExample[0]).toHaveProperty("content"); 32 | }); 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /src/utils/testnetStonAssets.ts: -------------------------------------------------------------------------------- 1 | // STON API doesn't support testnet assets, so we need to use a list of assets 2 | export const testnetAssets = [ 3 | { 4 | symbol: "TON", 5 | blacklisted: false, 6 | community: false, 7 | defaultSymbol: true, 8 | deprecated: false, 9 | decimals: 9, 10 | displayName: "TON", 11 | kind: "Ton", 12 | contractAddress: 13 | "EQC-0000000000000000000000000000000000000000000000000000000000000000", 14 | popularityIndex: 10.0, 15 | tags: [ 16 | "asset:default_symbol", 17 | "asset:liquidity:very_high", 18 | "asset:essential", 19 | "high_liquidity", 20 | "default_symbol", 21 | "asset:popular", 22 | ], 23 | dexPriceUsd: "1.0", 24 | }, 25 | { 26 | symbol: "TestRED", 27 | kind: "Jetton", 28 | contractAddress: "kQDLvsZol3juZyOAVG8tWsJntOxeEZWEaWCbbSjYakQpuYN5", 29 | blacklisted: false, 30 | community: false, 31 | defaultSymbol: true, 32 | deprecated: false, 33 | decimals: 9, 34 | displayName: "TestRED", 35 | popularityIndex: 10.0, 36 | tags: [ 37 | "asset:default_symbol", 38 | "asset:liquidity:very_high", 39 | "high_liquidity", 40 | "default_symbol", 41 | "asset:popular", 42 | ], 43 | dexPriceUsd: "1.0", 44 | }, 45 | ]; 46 | -------------------------------------------------------------------------------- /src/tests/evaaRepay.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from "bun:test"; 2 | import evaaRepayAction from "../actions/evaaRepay"; 3 | 4 | describe("EVAA Repay Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(evaaRepayAction.name).toBe("EVAA_REPAY"); 7 | expect(evaaRepayAction.description).toBe( 8 | "Repay all repayed TON tokens to the EVAA lending protocol" 9 | ); 10 | expect(evaaRepayAction.similes).toContain("REPAY_TON"); 11 | expect(evaaRepayAction.similes).toContain("REPAY_ALL_TON"); 12 | expect(evaaRepayAction.similes).toContain("REPAY_FULL_TON"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof evaaRepayAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof evaaRepayAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(evaaRepayAction.examples)).toBe(true); 25 | expect(evaaRepayAction.examples?.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = evaaRepayAction.examples?.[0]; 29 | if (firstExample) { 30 | expect(Array.isArray(firstExample)).toBe(true); 31 | expect(firstExample.length).toBeGreaterThan(0); 32 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 33 | expect(firstExample[0]).toHaveProperty("content"); 34 | } 35 | }); 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /src/tests/evaaSupply.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import evaaSupplyAction from "../actions/evaaSupply"; 3 | 4 | describe("EVAA Supply Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(evaaSupplyAction.name).toBe("EVAA_SUPPLY"); 7 | expect(evaaSupplyAction.description).toBe( 8 | "Supply/lend TON, USDT and USDC tokens to the EVAA lending protocol" 9 | ); 10 | expect(evaaSupplyAction.similes).toContain("LEND"); 11 | expect(evaaSupplyAction.similes).toContain("LEND_TON"); 12 | expect(evaaSupplyAction.similes).toContain("SUPPLY_TON"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof evaaSupplyAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof evaaSupplyAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(evaaSupplyAction.examples)).toBe(true); 25 | expect(evaaSupplyAction.examples?.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = evaaSupplyAction.examples?.[0]; 29 | if (firstExample) { 30 | expect(Array.isArray(firstExample)).toBe(true); 31 | expect(firstExample.length).toBeGreaterThan(0); 32 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 33 | expect(firstExample[0]).toHaveProperty("content"); 34 | } 35 | }); 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /src/tests/evaaBorrow.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeEach } from "bun:test"; 2 | import evaaBorrowAction from "../actions/evaaBorrow"; 3 | 4 | describe("EVAA Borrow Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(evaaBorrowAction.name).toBe("EVAA_BORROW"); 7 | expect(evaaBorrowAction.description).toBe( 8 | "Borrow TON, USDT and USDC tokens from the EVAA lending protocol" 9 | ); 10 | expect(evaaBorrowAction.similes).toContain("BORROW_TON"); 11 | expect(evaaBorrowAction.similes).toContain("BORROW_USDT"); 12 | expect(evaaBorrowAction.similes).toContain("BORROW_USDC"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof evaaBorrowAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof evaaBorrowAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(evaaBorrowAction.examples)).toBe(true); 25 | expect(evaaBorrowAction.examples?.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = evaaBorrowAction.examples?.[0]; 29 | if (firstExample) { 30 | expect(Array.isArray(firstExample)).toBe(true); 31 | expect(firstExample.length).toBeGreaterThan(0); 32 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 33 | expect(firstExample[0]).toHaveProperty("content"); 34 | } 35 | }); 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /src/tests/evaaWithdraw.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import evaaWithdrawAction from "../actions/evaaWithdraw"; 3 | 4 | describe("EVAA Withdraw Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(evaaWithdrawAction.name).toBe("EVAA_WITHDRAW"); 7 | expect(evaaWithdrawAction.description).toBe( 8 | "Withdraw TON tokens from the EVAA lending protocol" 9 | ); 10 | expect(evaaWithdrawAction.similes).toContain("WITHDRAW_TON"); 11 | expect(evaaWithdrawAction.similes).toContain("REMOVE_TON"); 12 | expect(evaaWithdrawAction.similes).toContain("REDEEM_TON"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof evaaWithdrawAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof evaaWithdrawAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(evaaWithdrawAction.examples)).toBe(true); 25 | expect(evaaWithdrawAction.examples?.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = evaaWithdrawAction.examples?.[0]; 29 | if (firstExample) { 30 | expect(Array.isArray(firstExample)).toBe(true); 31 | expect(firstExample.length).toBeGreaterThan(0); 32 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 33 | expect(firstExample[0]).toHaveProperty("content"); 34 | } 35 | }); 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /src/tests/stake.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import stakeAction from "../actions/stake"; 3 | 4 | describe("Stake Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(stakeAction.name).toBe("DEPOSIT_TON"); 7 | expect(stakeAction.description).toBe( 8 | "Deposit TON tokens in a specified pool." 9 | ); 10 | expect(stakeAction.similes).toContain("STAKE_TOKENS"); 11 | expect(stakeAction.similes).toContain("DEPOSIT_TON"); 12 | expect(stakeAction.similes).toContain("DEPOSIT_TOKEN"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof stakeAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof stakeAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(stakeAction.examples)).toBe(true); 25 | expect(stakeAction.examples.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = stakeAction.examples[0]; 29 | expect(Array.isArray(firstExample)).toBe(true); 30 | expect(firstExample.length).toBeGreaterThan(0); 31 | expect(firstExample[0]).toHaveProperty("user"); 32 | expect(firstExample[0]).toHaveProperty("content"); 33 | }); 34 | 35 | it("should have correct template format", () => { 36 | const template = stakeAction.template; 37 | expect(template).toContain("{{recentMessages}}"); 38 | expect(template).toContain(""); 39 | expect(template).toContain(""); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/evaaPositions.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import evaaPositionsAction from "../actions/evaaPositions"; 3 | 4 | describe("EVAA Positions Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(evaaPositionsAction.name).toBe("EVAA_POSITIONS"); 7 | expect(evaaPositionsAction.description).toBe( 8 | "Calculates and displays accrued interest and health factors for borrowed positions" 9 | ); 10 | expect(evaaPositionsAction.similes).toContain("BORROW_POSITIONS"); 11 | expect(evaaPositionsAction.similes).toContain("GET_BORROW_POSITIONS"); 12 | expect(evaaPositionsAction.similes).toContain( 13 | "VIEW_BORROWED_POSITIONS" 14 | ); 15 | }); 16 | 17 | it("should have validate function", () => { 18 | expect(typeof evaaPositionsAction.validate).toBe("function"); 19 | }); 20 | 21 | it("should have handler function", () => { 22 | expect(typeof evaaPositionsAction.handler).toBe("function"); 23 | }); 24 | 25 | it("should have examples", () => { 26 | expect(Array.isArray(evaaPositionsAction.examples)).toBe(true); 27 | expect(evaaPositionsAction.examples?.length).toBeGreaterThan(0); 28 | 29 | // Check first example structure 30 | const firstExample = evaaPositionsAction.examples?.[0]; 31 | if (firstExample) { 32 | expect(Array.isArray(firstExample)).toBe(true); 33 | expect(firstExample.length).toBeGreaterThan(0); 34 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 35 | expect(firstExample[0]).toHaveProperty("content"); 36 | } 37 | }); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /src/tests/unstake.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import unstakeAction from "../actions/unstake"; 3 | 4 | describe("Unstake Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(unstakeAction.name).toBe("WITHDRAW_TON"); 7 | expect(unstakeAction.description).toBe( 8 | "Withdraw TON tokens from a specified pool." 9 | ); 10 | expect(unstakeAction.similes).toContain("UNSTAKE_TOKENS"); 11 | expect(unstakeAction.similes).toContain("WITHDRAW_TON"); 12 | expect(unstakeAction.similes).toContain("TON_UNSTAKE"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof unstakeAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof unstakeAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(unstakeAction.examples)).toBe(true); 25 | expect(unstakeAction.examples.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = unstakeAction.examples[0]; 29 | expect(Array.isArray(firstExample)).toBe(true); 30 | expect(firstExample.length).toBeGreaterThan(0); 31 | expect(firstExample[0]).toHaveProperty("user"); 32 | expect(firstExample[0]).toHaveProperty("content"); 33 | }); 34 | 35 | it("should have correct template format", () => { 36 | const template = unstakeAction.template; 37 | expect(template).toContain("{{recentMessages}}"); 38 | expect(template).toContain(""); 39 | expect(template).toContain(""); 40 | }); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /src/tests/fetchPrice.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import tokenPriceAction from "../actions/tokenPrice"; 3 | 4 | describe("Token Price Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(tokenPriceAction.name).toBe("GET_TOKEN_PRICE_TON"); 7 | expect(tokenPriceAction.description).toBe( 8 | "Fetches and returns token price information on TON blockchain" 9 | ); 10 | expect(tokenPriceAction.similes).toContain("FETCH_TOKEN_PRICE_TON"); 11 | expect(tokenPriceAction.similes).toContain("CHECK_TOKEN_PRICE_TON"); 12 | expect(tokenPriceAction.similes).toContain("TOKEN_PRICE_TON"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof tokenPriceAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof tokenPriceAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(tokenPriceAction.examples)).toBe(true); 25 | expect(tokenPriceAction.examples.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = tokenPriceAction.examples[0]; 29 | expect(Array.isArray(firstExample)).toBe(true); 30 | expect(firstExample.length).toBeGreaterThan(0); 31 | expect(firstExample[0]).toHaveProperty("name"); 32 | expect(firstExample[0]).toHaveProperty("content"); 33 | }); 34 | 35 | it("should have correct template format", () => { 36 | const template = tokenPriceAction.template; 37 | expect(template).toContain("{{recentMessages}}"); 38 | expect(template).toContain("```json"); 39 | }); 40 | }); 41 | 42 | -------------------------------------------------------------------------------- /src/tests/auctionInteraction.test.ts: -------------------------------------------------------------------------------- 1 | 2 | import { describe, it, expect } from "bun:test"; 3 | import auctionInteractionAction from "../actions/auctionInteraction"; 4 | 5 | describe("Auction Interaction Action", () => { 6 | it("should have correct metadata", () => { 7 | expect(auctionInteractionAction.name).toBe("INTERACT_AUCTION"); 8 | expect(auctionInteractionAction.description).toBe( 9 | "Interacts with an auction contract. Supports actions: getSaleData, bid, stop, and cancel." 10 | ); 11 | expect(auctionInteractionAction.similes).toContain("AUCTION_INTERACT"); 12 | expect(auctionInteractionAction.similes).toContain("AUCTION_ACTION"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof auctionInteractionAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof auctionInteractionAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(auctionInteractionAction.examples)).toBe(true); 25 | expect(auctionInteractionAction.examples.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = auctionInteractionAction.examples[0]; 29 | expect(Array.isArray(firstExample)).toBe(true); 30 | expect(firstExample.length).toBeGreaterThan(0); 31 | expect(firstExample[0]).toHaveProperty("user"); 32 | expect(firstExample[0]).toHaveProperty("content"); 33 | }); 34 | 35 | it("should have correct template format", () => { 36 | const template = auctionInteractionAction.template; 37 | expect(template).toContain("{{recentMessages}}"); 38 | expect(template).toContain(""); 39 | expect(template).toContain(""); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/getPoolInfo.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import getPoolInfoAction from "../actions/getPoolInfo"; 3 | 4 | describe("Get Pool Info Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(getPoolInfoAction.name).toBe("GET_POOL_INFO"); 7 | expect(getPoolInfoAction.description).toBe( 8 | "Fetch detailed global staking pool information. Only perform if user is asking for a specific Pool Info, and NOT your stake." 9 | ); 10 | expect(getPoolInfoAction.similes).toContain("FETCH_POOL_INFO"); 11 | expect(getPoolInfoAction.similes).toContain("POOL_DATA"); 12 | expect(getPoolInfoAction.similes).toContain("GET_STAKING_INFO"); 13 | }); 14 | 15 | it("should have validate function", () => { 16 | expect(typeof getPoolInfoAction.validate).toBe("function"); 17 | }); 18 | 19 | it("should have handler function", () => { 20 | expect(typeof getPoolInfoAction.handler).toBe("function"); 21 | }); 22 | 23 | it("should have examples", () => { 24 | expect(Array.isArray(getPoolInfoAction.examples)).toBe(true); 25 | expect(getPoolInfoAction.examples.length).toBeGreaterThan(0); 26 | 27 | // Check first example structure 28 | const firstExample = getPoolInfoAction.examples[0]; 29 | expect(Array.isArray(firstExample)).toBe(true); 30 | expect(firstExample.length).toBeGreaterThan(0); 31 | expect(firstExample[0]).toHaveProperty("user"); 32 | expect(firstExample[0]).toHaveProperty("content"); 33 | }); 34 | 35 | it("should have correct template format", () => { 36 | const template = getPoolInfoAction.template; 37 | expect(template).toContain("{{recentMessages}}"); 38 | expect(template).toContain(""); 39 | expect(template).toContain(""); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import tonPlugin from "../index"; 3 | 4 | describe("TON Plugin", () => { 5 | it("should export a valid plugin object", () => { 6 | expect(tonPlugin).toBeDefined(); 7 | expect(tonPlugin.name).toBe("ton"); 8 | expect(tonPlugin.description).toBe("Ton Plugin for Eliza"); 9 | }); 10 | 11 | it("should export actions array", () => { 12 | expect(tonPlugin.actions).toBeDefined(); 13 | if (tonPlugin.actions) { 14 | expect(Array.isArray(tonPlugin.actions)).toBe(true); 15 | expect(tonPlugin.actions.length).toBeGreaterThan(0); 16 | } 17 | }); 18 | 19 | it("should not have services in v1", () => { 20 | // Services are removed in v1 migration 21 | expect(tonPlugin.services).toBeUndefined(); 22 | }); 23 | 24 | it("should export providers array", () => { 25 | expect(tonPlugin.providers).toBeDefined(); 26 | if (tonPlugin.providers) { 27 | expect(Array.isArray(tonPlugin.providers)).toBe(true); 28 | expect(tonPlugin.providers.length).toBeGreaterThan(0); 29 | } 30 | }); 31 | 32 | it("should have all required action names", () => { 33 | const actionNames = 34 | tonPlugin.actions?.map((action: any) => action.name) || []; 35 | const expectedActions = [ 36 | "SEND_TON_TOKEN", 37 | "CREATE_TON_WALLET", 38 | "EVAA_BORROW", 39 | "EVAA_SUPPLY", 40 | "EVAA_WITHDRAW", 41 | "EVAA_REPAY", 42 | "EVAA_POSITIONS", 43 | "SWAP_TOKEN_STON", 44 | "QUERY_STON_ASSET", 45 | "TRANSFER_NFT", 46 | "MINT_NFT", 47 | "UPDATE_NFT_METADATA", 48 | "MANAGE_LIQUIDITY_POOLS", 49 | "INTERACT_JETTON", 50 | ]; 51 | 52 | for (const expectedAction of expectedActions) { 53 | expect(actionNames).toContain(expectedAction); 54 | } 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/sdk/Helpers.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell } from "@ton/ton"; 2 | import { 3 | feeStake, 4 | feeUnstake, 5 | minimumTonBalanceReserve, 6 | opDepositCoins, 7 | opUnstakeTokens, 8 | } from "./Constants"; 9 | 10 | export function maxAmountToStake(tonBalance: bigint): bigint { 11 | tonBalance -= minimumTonBalanceReserve; 12 | return tonBalance > 0n ? tonBalance : 0n; 13 | } 14 | 15 | interface TonConnectMessage { 16 | address: string; 17 | amount: string; 18 | stateInit: string | undefined; 19 | payload: string | undefined; 20 | } 21 | 22 | export function createDepositMessage( 23 | treasury: Address, 24 | amountInNano: bigint, 25 | queryId = 0n, 26 | referrer?: Address 27 | ): TonConnectMessage { 28 | const address = treasury.toString(); 29 | const amount = (amountInNano + feeStake).toString(); 30 | const stateInit = undefined; 31 | const payload = beginCell() 32 | .storeUint(opDepositCoins, 32) 33 | .storeUint(queryId, 64) 34 | .storeAddress(null) 35 | .storeCoins(amountInNano) 36 | .storeCoins(1n) 37 | .storeAddress(referrer) 38 | .endCell() 39 | .toBoc() 40 | .toString("base64"); 41 | return { 42 | address, 43 | amount, 44 | stateInit, 45 | payload, 46 | }; 47 | } 48 | 49 | export function createUnstakeMessage( 50 | wallet: Address, 51 | amountInNano: bigint, 52 | queryId = 0n 53 | ): TonConnectMessage { 54 | const address = wallet.toString(); 55 | const amount = feeUnstake.toString(); 56 | const stateInit = undefined; 57 | const payload = beginCell() 58 | .storeUint(opUnstakeTokens, 32) 59 | .storeUint(queryId, 64) 60 | .storeCoins(amountInNano) 61 | .storeAddress(undefined) 62 | .storeMaybeRef(beginCell().storeUint(0, 4).storeCoins(1n)) 63 | .endCell() 64 | .toBoc() 65 | .toString("base64"); 66 | return { 67 | address, 68 | amount, 69 | stateInit, 70 | payload, 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/enviroment.ts: -------------------------------------------------------------------------------- 1 | import type { IAgentRuntime } from "@elizaos/core"; 2 | import { z } from "zod"; 3 | 4 | export const CONFIG_KEYS = { 5 | TON_PRIVATE_KEY: "TON_PRIVATE_KEY", 6 | TON_RPC_URL: "TON_RPC_URL", 7 | TON_RPC_API_KEY: "TON_RPC_API_KEY", 8 | TON_EXPLORER_URL: "TON_EXPLORER_URL", 9 | TON_MANIFEST_URL: "TON_MANIFEST_URL", 10 | TON_BRIDGE_URL: "TON_BRIDGE_URL", 11 | }; 12 | 13 | export const envSchema = z.object({ 14 | TON_PRIVATE_KEY: z.string().min(1, "Ton private key is required"), 15 | TON_RPC_URL: z.string(), 16 | TON_RPC_API_KEY: z.string(), 17 | TON_EXPLORER_URL: z.string(), 18 | TON_MANIFEST_URL: z.string(), 19 | TON_BRIDGE_URL: z.string(), 20 | }); 21 | 22 | export type EnvConfig = z.infer; 23 | 24 | export async function validateEnvConfig( 25 | runtime: IAgentRuntime 26 | ): Promise { 27 | try { 28 | const config = { 29 | TON_PRIVATE_KEY: 30 | runtime.getSetting(CONFIG_KEYS.TON_PRIVATE_KEY) || 31 | process.env.TON_PRIVATE_KEY, 32 | TON_RPC_URL: 33 | runtime.getSetting(CONFIG_KEYS.TON_RPC_URL) || 34 | process.env.TON_RPC_URL, 35 | TON_RPC_API_KEY: 36 | runtime.getSetting(CONFIG_KEYS.TON_RPC_API_KEY) || 37 | process.env.TON_RPC_API_KEY, 38 | TON_EXPLORER_URL: 39 | runtime.getSetting(CONFIG_KEYS.TON_EXPLORER_URL) || 40 | process.env.TON_EXPLORER_URL, 41 | TON_MANIFEST_URL: 42 | runtime.getSetting(CONFIG_KEYS.TON_MANIFEST_URL) || 43 | process.env.TON_MANIFEST_URL, 44 | TON_BRIDGE_URL: 45 | runtime.getSetting(CONFIG_KEYS.TON_BRIDGE_URL) || 46 | process.env.TON_BRIDGE_URL, 47 | }; 48 | 49 | return envSchema.parse(config); 50 | } catch (error) { 51 | if (error instanceof z.ZodError) { 52 | const errorMessages = error.errors 53 | .map((err) => `${err.path.join(".")}: ${err.message}`) 54 | .join("\n"); 55 | throw new Error( 56 | `Ton configuration validation failed:\n${errorMessages}` 57 | ); 58 | } 59 | throw error; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/tests/actions/transfer.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect, beforeAll } from "bun:test"; 2 | import transferAction from "../../actions/transfer"; 3 | 4 | describe("Transfer Action", () => { 5 | it("should have correct metadata", () => { 6 | expect(transferAction.name).toBe("SEND_TON_TOKEN"); 7 | expect(transferAction.description).toBe( 8 | "Call this action to send TON tokens to another wallet address. Supports sending any amount of TON to any valid TON wallet address. Transaction will be signed and broadcast to the TON blockchain." 9 | ); 10 | expect(transferAction.similes).toContain("SEND_TON"); 11 | expect(transferAction.similes).toContain("SEND_TON_TOKENS"); 12 | }); 13 | 14 | it("should validate transfer parameters", async () => { 15 | const runtime = { 16 | character: { name: "test", settings: {} }, 17 | messageManager: { 18 | createMemory: async (memory: any) => memory, 19 | }, 20 | getSetting: (key: string) => 21 | key === "TON_EXPLORER_URL" ? "https://tonviewer.com/" : null, 22 | }; 23 | 24 | const message = { 25 | userId: "user123", 26 | agentId: "agent123", 27 | roomId: "room123", 28 | content: { 29 | text: "transfer 1 TON to EQRecipient123", 30 | recipient: "EQRecipient123", 31 | amount: "1", 32 | }, 33 | }; 34 | 35 | const result = await transferAction.validate(runtime as any); 36 | expect(result).toBe(true); 37 | }); 38 | 39 | it("should format transfer examples correctly", () => { 40 | expect(transferAction.examples).toBeDefined(); 41 | expect(transferAction.examples.length).toBeGreaterThan(0); 42 | 43 | // Examples are arrays of messages 44 | const firstExampleSet = transferAction.examples[0]; 45 | expect(Array.isArray(firstExampleSet)).toBe(true); 46 | expect(firstExampleSet.length).toBeGreaterThan(0); 47 | 48 | const firstMessage = firstExampleSet[0]; 49 | expect(firstMessage).toHaveProperty("user"); 50 | expect(firstMessage).toHaveProperty("content"); 51 | expect(firstMessage.content).toHaveProperty("text"); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@elizaos/plugin-ton", 3 | "version": "1.4.4", 4 | "type": "module", 5 | "main": "dist/index.js", 6 | "module": "dist/index.js", 7 | "types": "dist/index.d.ts", 8 | "exports": { 9 | "./package.json": "./package.json", 10 | ".": { 11 | "import": { 12 | "@elizaos/source": "./src/index.ts", 13 | "types": "./dist/index.d.ts", 14 | "default": "./dist/index.js" 15 | } 16 | } 17 | }, 18 | "files": [ 19 | "dist" 20 | ], 21 | "dependencies": { 22 | "@dedust/sdk": "^0.8.7", 23 | "@elizaos/core": "latest", 24 | "@evaafi/sdk": "^0.9.2-a", 25 | "@pinata/sdk": "^2.1.0", 26 | "@ston-fi/api": "^0.19.0", 27 | "@ston-fi/sdk": "^2.2.2", 28 | "@ton/crypto": "3.3.0", 29 | "@ton/ton": "^15.1.0", 30 | "@tonconnect/sdk": "^3.0.7", 31 | "@torch-finance/core": "^1.3.2", 32 | "@torch-finance/dex-contract-wrapper": "^0.2.9", 33 | "@torch-finance/sdk": "^1.2.2", 34 | "@torch-finance/simulator": "^0.4.0", 35 | "bignumber.js": "9.1.2", 36 | "node-cache": "5.1.2", 37 | "qrcode": "^1.5.4" 38 | }, 39 | "devDependencies": { 40 | "@biomejs/biome": "1.5.3", 41 | "@elizaos/cli": "^1.6.3", 42 | "@types/bun": "latest", 43 | "bun": "^1.2.15", 44 | "prettier": "^3.0.0", 45 | "tsup": "8.3.5", 46 | "typescript": "^5.0.0" 47 | }, 48 | "scripts": { 49 | "build": "tsup", 50 | "dev": "tsup --watch", 51 | "lint": "prettier --write ./src", 52 | "clean": "rm -rf dist .turbo node_modules .turbo-tsconfig.json tsconfig.tsbuildinfo", 53 | "format": "prettier --write ./src", 54 | "format:check": "prettier --check ./src", 55 | "test": "bun test", 56 | "test:watch": "bun test --watch", 57 | "test:coverage": "bun test --coverage" 58 | }, 59 | "peerDependencies": { 60 | "whatwg-url": "7.1.0" 61 | }, 62 | "publishConfig": { 63 | "access": "public" 64 | }, 65 | "agentConfig": { 66 | "pluginType": "elizaos:client:1.4.2", 67 | "pluginParameters": { 68 | "TON_PRIVATE_KEY": { 69 | "type": "string", 70 | "description": "Ton private key is required", 71 | "required": true, 72 | "sensitive": true 73 | }, 74 | "TON_RPC_URL": { 75 | "type": "string", 76 | "description": "TON network RPC endpoint URL", 77 | "required": true, 78 | "sensitive": true 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/utils/modifyMemories.ts: -------------------------------------------------------------------------------- 1 | import { type IAgentRuntime, type Memory, type State } from "@elizaos/core"; 2 | 3 | export async function replaceLastMemory( 4 | runtime: IAgentRuntime, 5 | state: State, 6 | template: string 7 | ): Promise { 8 | const memory = state.recentMessagesData[0]; 9 | 10 | // In v1, we can't easily delete memories, so we'll skip this step 11 | // await runtime.removeMemory(memory.id, "messages"); 12 | 13 | const prompt = template 14 | .replace("{{agentName}}", runtime.character.name) 15 | .replace("{{recentMessages}}", state.recentMessages || ""); 16 | 17 | // Simple response generation - in v1 we'll just use the prompt as response 18 | const response = { text: prompt }; 19 | 20 | const newMemory = { 21 | id: crypto.randomUUID() as `${string}-${string}-${string}-${string}-${string}`, 22 | userId: (memory as any).userId || "default", 23 | entityId: memory.entityId || undefined, 24 | agentId: memory.agentId, 25 | roomId: memory.roomId, 26 | content: { 27 | ...memory.content, 28 | text: response.text, 29 | }, 30 | createdAt: Date.now(), 31 | } as Memory; 32 | 33 | await runtime.createMemory(newMemory, "messages"); 34 | 35 | return newMemory; 36 | } 37 | 38 | export async function addMemory( 39 | runtime: IAgentRuntime, 40 | state: State, 41 | memory: Memory, 42 | template: string 43 | ): Promise { 44 | const prompt = template 45 | .replace("{{agentName}}", runtime.character.name) 46 | .replace("{{recentMessages}}", state.recentMessages || ""); 47 | 48 | // Simple response generation - in v1 we'll just use the prompt as response 49 | const response = { text: prompt }; 50 | 51 | const newMemory = { 52 | id: crypto.randomUUID() as `${string}-${string}-${string}-${string}-${string}`, 53 | userId: (memory as any).userId || "default", 54 | entityId: memory.entityId || undefined, 55 | agentId: memory.agentId, 56 | roomId: memory.roomId, 57 | content: { 58 | text: response.text, 59 | inReplyTo: memory.content.inReplyTo, 60 | action: memory.content.action, 61 | source: memory.content.source, 62 | }, 63 | createdAt: Date.now(), 64 | } as Memory; 65 | 66 | await runtime.createMemory(newMemory, "messages"); 67 | 68 | return newMemory; 69 | } 70 | -------------------------------------------------------------------------------- /src/tests/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { IAgentRuntime, Memory, State } from "@elizaos/core"; 2 | import { KeyPair } from "@ton/crypto"; 3 | 4 | export function createMockRuntime( 5 | overrides?: Partial 6 | ): IAgentRuntime { 7 | return { 8 | character: { name: "test-agent", settings: {} }, 9 | getSetting: (key: string) => { 10 | if (key === "TON_EXPLORER_URL") 11 | return "https://testnet.tonviewer.com/"; 12 | if (key === "TON_RPC_URL") 13 | return "https://testnet.toncenter.com/api/v2/jsonRPC"; 14 | return null; 15 | }, 16 | messageManager: { 17 | createMemory: async (memory: Memory) => memory, 18 | updateMemory: async (memory: Memory) => memory, 19 | getMemories: async () => [], 20 | }, 21 | stateManager: { 22 | getState: async () => ({ messages: [] }), 23 | updateState: async (state: State) => state, 24 | }, 25 | useModel: async (modelType: any, options: any) => { 26 | return options.prompt || "Mock response"; 27 | }, 28 | ...overrides, 29 | } as any; 30 | } 31 | 32 | export function createMockWalletProvider(keypair?: KeyPair) { 33 | return { 34 | wallet: { 35 | address: { 36 | toString: () => "EQTest123456789", 37 | toRawString: () => "0:test123456789", 38 | }, 39 | createTransfer: () => ({}), 40 | getSeqno: async () => 1, 41 | send: async () => {}, 42 | }, 43 | getWalletClient: () => ({ 44 | open: (contract: any) => contract, 45 | sendTransaction: async () => ({ hash: "mockTxHash123" }), 46 | getContractState: async () => ({ 47 | lastTransaction: { hash: "mockTxHash123" }, 48 | }), 49 | }), 50 | keypair: keypair || { 51 | secretKey: Buffer.from("mock-secret-key"), 52 | publicKey: Buffer.from("mock-public-key"), 53 | }, 54 | }; 55 | } 56 | 57 | export function createMockMemory(overrides?: Partial): Memory { 58 | return { 59 | id: "test-memory-id", 60 | userId: "test-user", 61 | agentId: "test-agent", 62 | roomId: "test-room", 63 | content: { 64 | text: "Test message", 65 | ...overrides?.content, 66 | }, 67 | ...overrides, 68 | } as Memory; 69 | } 70 | -------------------------------------------------------------------------------- /src/services/staking/platformFactory.ts: -------------------------------------------------------------------------------- 1 | import { elizaLogger } from "@elizaos/core"; 2 | import { Address } from "@ton/ton"; 3 | 4 | import { StakingPlatform } from "./interfaces/stakingPlatform.ts"; 5 | 6 | import { 7 | PLATFORM_TYPES, 8 | STAKING_POOL_ADDRESSES, 9 | PlatformType, 10 | } from "./config/platformConfig.ts"; 11 | 12 | function isPlatformType(type: string): type is PlatformType { 13 | return PLATFORM_TYPES.includes(type as PlatformType); 14 | } 15 | 16 | type StakingPoolAddresses = { 17 | [K in PlatformType]: Address[]; 18 | }; 19 | 20 | export class PlatformFactory { 21 | private static strategies = new Map(); 22 | private static addresses: StakingPoolAddresses; 23 | 24 | // initliazer block 25 | static { 26 | this.addresses = Object.fromEntries( 27 | Object.entries(STAKING_POOL_ADDRESSES).map(([type, addrs]) => [ 28 | type, 29 | addrs.map((addr) => Address.parse(addr)), 30 | ]) 31 | ) as StakingPoolAddresses; 32 | } 33 | 34 | static register(type: PlatformType, strategy: StakingPlatform): void { 35 | this.strategies.set(type, strategy); 36 | } 37 | 38 | static getStrategy(address: Address): StakingPlatform | null { 39 | const type = this.getPlatformType(address); 40 | if (!type) { 41 | elizaLogger.info(`Unknown platform address: ${address}`); 42 | return null; 43 | } 44 | 45 | const strategy = this.strategies.get(type); 46 | if (!strategy) { 47 | elizaLogger.warn(`No strategy implemented for platform: ${type}`); 48 | return null; 49 | } 50 | 51 | elizaLogger.debug(`Found strategy for platform: ${type}`); 52 | return strategy; 53 | } 54 | 55 | static getAllStrategies(): StakingPlatform[] { 56 | return Array.from(this.strategies.values()); 57 | } 58 | 59 | private static getPlatformType(address: Address): PlatformType | null { 60 | const entry = Object.entries(this.addresses).find(([_, addresses]) => 61 | addresses.some((addr) => addr.equals(address)) 62 | ); 63 | 64 | if (!entry) return null; 65 | 66 | const [type] = entry; 67 | return isPlatformType(type) ? type : null; 68 | } 69 | 70 | static getAllAddresses(): Address[] { 71 | return Object.values(this.addresses).flat(); 72 | } 73 | 74 | static getAddressesByType(type: PlatformType): Address[] { 75 | return this.addresses[type] || []; 76 | } 77 | 78 | static getAvailablePlatformTypes(): PlatformType[] { 79 | return [...PLATFORM_TYPES]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/utils/JettonMinterUtils.ts: -------------------------------------------------------------------------------- 1 | import { beginCell, Cell, Dictionary } from "@ton/ton"; 2 | 3 | export type JettonMetaDataKeys = 4 | | "name" 5 | | "description" 6 | | "image" 7 | | "symbol" 8 | | "decimals" 9 | | "uri" 10 | | "social" 11 | | "website"; 12 | 13 | /** 14 | * Build on-chain metadata for a Jetton 15 | * @param data Object containing metadata key-value pairs 16 | * @returns Cell containing the metadata 17 | */ 18 | export function buildJettonOnchainMetadata(data: { 19 | [s in JettonMetaDataKeys]?: string | undefined; 20 | }): Cell { 21 | const dict = Dictionary.empty( 22 | Dictionary.Keys.Buffer(32), 23 | Dictionary.Values.Cell() 24 | ); 25 | 26 | Object.entries(data).forEach(([k, v]) => { 27 | if (v) { 28 | dict.set( 29 | Buffer.from(k), 30 | beginCell().storeUint(0, 8).storeStringTail(v).endCell() 31 | ); 32 | } 33 | }); 34 | 35 | return beginCell().storeUint(0, 8).storeDict(dict).endCell(); 36 | } 37 | 38 | /** 39 | * Build off-chain metadata for a Jetton 40 | * @param uri URI pointing to the metadata 41 | * @returns Cell containing the metadata URI 42 | */ 43 | export function buildJettonOffChainMetadata(uri: string): Cell { 44 | return beginCell() 45 | .storeUint(0x01, 8) // off-chain marker 46 | .storeStringTail(uri) 47 | .endCell(); 48 | } 49 | 50 | /** 51 | * Parse token metadata from a cell 52 | * @param cell Cell containing the metadata 53 | * @returns Object with parsed metadata 54 | */ 55 | export function parseTokenMetadataCell(cell: Cell): Record { 56 | const slice = cell.beginParse(); 57 | const type = slice.loadUint(8); 58 | 59 | // Handle on-chain metadata 60 | if (type === 0) { 61 | const dict = slice.loadDict( 62 | Dictionary.Keys.Buffer(32), 63 | Dictionary.Values.Cell() 64 | ); 65 | const metadata: Record = {}; 66 | 67 | for (const [key, value] of dict) { 68 | const keyString = key.toString(); 69 | const valueSlice = value.beginParse(); 70 | valueSlice.loadUint(8); // Skip prefix 71 | metadata[keyString] = valueSlice.loadStringTail(); 72 | } 73 | 74 | return metadata; 75 | } 76 | 77 | // Handle off-chain metadata 78 | if (type === 1) { 79 | return { 80 | uri: slice.loadStringTail(), 81 | }; 82 | } 83 | 84 | return {}; 85 | } 86 | 87 | /** 88 | * Create a Jetton content cell from metadata 89 | * @param metadata Object containing metadata key-value pairs 90 | * @returns Cell containing the content 91 | */ 92 | export function createJettonContent(metadata: Record): Cell { 93 | return buildJettonOnchainMetadata(metadata); 94 | } 95 | -------------------------------------------------------------------------------- /scripts/debug.sh: -------------------------------------------------------------------------------- 1 | # system: debian11 2 | # required: jq, tmux, pnpm 3 | 4 | # you can uncomment above if using openai provider 5 | OPENAI_API_KEY="$OPENAI_API_KEY" 6 | # if no apikey provided, the endpoint may be not working 7 | TON_RPC_API_KEY="$TON_RPC_API_KEY" 8 | 9 | apt install -y jq tmux 10 | 11 | grep 'const importedPlugin = await import("@elizaos/plugin-ton");' agent/src/index.ts 12 | if [ $? -eq 0 ]; then 13 | echo "already patched" 14 | else 15 | # insert tonplugin as default plugin for the default character 16 | if [ -z "$OPENAI_API_KEY" ]; then 17 | sed -i '/let characters = \[defaultCharacter\];/a \ 18 | const importedPlugin = await import("@elizaos/plugin-ton");\ 19 | defaultCharacter.plugins = [importedPlugin.default];\ 20 | // defaultCharacter.modelProvider = ModelProviderName.OPENAI;' agent/src/index.ts 21 | else 22 | sed -i '/let characters = \[defaultCharacter\];/a \ 23 | const importedPlugin = await import("@elizaos/plugin-ton");\ 24 | defaultCharacter.plugins = [importedPlugin.default];\ 25 | defaultCharacter.modelProvider = ModelProviderName.OPENAI;' agent/src/index.ts 26 | fi 27 | fi 28 | 29 | # check the content 30 | grep -C 4 'let characters = ' agent/src/index.ts 31 | 32 | # Note: Previously resolved https://github.com/elizaOS/eliza/issues/1965 - no longer needed 33 | 34 | pnpm install --no-frozen-lockfile 35 | 36 | # generate ton private key if you do not have one 37 | # pnpm --dir packages/plugin-ton mnemonic 38 | 39 | pnpm build 40 | 41 | TON_RPC_URL="https://testnet.toncenter.com/api/v2/jsonRPC" 42 | TON_PRIVATE_KEY="demise portion caught unit slot patient pumpkin second faint surround vote awkward afraid turtle extra donate core auction share arrest spend maid say chuckle" 43 | 44 | # stop the server if it is running 45 | tmux kill-session -t client && tmux kill-session -t agent || true 46 | 47 | # start client 48 | tmux new -s client -d \ 49 | "export TON_RPC_URL='$TON_RPC_URL' && export TON_PRIVATE_KEY='$TON_PRIVATE_KEY' && \ 50 | export OPENAI_API_KEY='$OPENAI_API_KEY' && export TON_RPC_API_KEY=$TON_RPC_API_KEY && pnpm --dir client dev -- --host" 51 | # start agent using the default character 52 | tmux new -s agent -d \ 53 | "export TON_RPC_URL='$TON_RPC_URL' && export TON_PRIVATE_KEY='$TON_PRIVATE_KEY' && \ 54 | export OPENAI_API_KEY='$OPENAI_API_KEY' && export TON_RPC_API_KEY='$TON_RPC_API_KEY' && echo '$TON_PRIVATE_KEY' && env && pnpm --dir agent dev --" 55 | 56 | # check the status 57 | tmux ls 58 | 59 | echo ' 60 | - you can check balance and transfer on https://testnet.tonviewer.com/kQDT62Zxkrlj-NG9cODSAfRzuNYrSbrtVVAnjHfK7lvs4Rp1 61 | - you should get testcoin from https://t.me/testgiver_ton_bot, the address is: `UQDT62Zxkrlj-NG9cODSAfRzuNYrSbrtVVAnjHfK7lvs4fw6` 62 | - open the browser, go to http://localhost:5173/ and select one chat to start 63 | - send `hello`` and wait for it have been initialized 64 | - send `Send 0.3 TON tokens to EQCGScrZe1xbyWqWDvdI6mzP-GAcAWFv6ZXuaJOuSqemxku4` to test the transfer 65 | - `tmux kill-session -t client && tmux kill-session -t agent` to stop the server 66 | - `tmux a -t client` to watch the client logs, `tmux a -t agent` to watch the agent logs 67 | ' 68 | -------------------------------------------------------------------------------- /src/utils/NFTItem.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Cell, 5 | internal, 6 | SendMode, 7 | TonClient, 8 | } from "@ton/ton"; 9 | import { MintParams, NFTCollection } from "./NFTCollection"; 10 | import { WalletProvider } from "../providers/wallet"; 11 | 12 | export async function getAddressByIndex( 13 | client: TonClient, 14 | collectionAddress: Address, 15 | itemIndex: number 16 | ): Promise
{ 17 | const response = await client.runMethod( 18 | collectionAddress, 19 | "get_nft_address_by_index", 20 | [{ type: "int", value: BigInt(itemIndex) }] 21 | ); 22 | return response.stack.readAddress(); 23 | } 24 | 25 | export async function getNftOwner( 26 | walletProvider: WalletProvider, 27 | nftAddress: string 28 | ): Promise
{ 29 | try { 30 | const client = walletProvider.getWalletClient(); 31 | const result = await client.runMethod( 32 | Address.parse(nftAddress), 33 | "get_nft_data" 34 | ); 35 | 36 | result.stack.skip(3); 37 | const owner = result.stack.readAddress() as Address; 38 | 39 | // Create a clean operational address 40 | const rawString = owner.toRawString(); 41 | const operationalAddress = Address.parseRaw(rawString); 42 | 43 | return operationalAddress; 44 | } catch (error) { 45 | throw new Error( 46 | `Failed to get NFT owner: ${error instanceof Error ? error.message : String(error)}` 47 | ); 48 | } 49 | } 50 | 51 | export class NftItem { 52 | private readonly collectionAddress: Address; 53 | 54 | constructor(collection: string) { 55 | this.collectionAddress = Address.parse(collection); 56 | } 57 | 58 | public createMintBody(params: MintParams): Cell { 59 | const body = beginCell(); 60 | body.storeUint(1, 32); 61 | body.storeUint(params.queryId || 0, 64); 62 | body.storeUint(params.itemIndex, 64); 63 | body.storeCoins(params.amount); 64 | const nftItemContent = beginCell(); 65 | nftItemContent.storeAddress(params.itemOwnerAddress); 66 | const uriContent = beginCell(); 67 | uriContent.storeBuffer(Buffer.from(params.commonContentUrl)); 68 | nftItemContent.storeRef(uriContent.endCell()); 69 | body.storeRef(nftItemContent.endCell()); 70 | return body.endCell(); 71 | } 72 | 73 | public async deploy( 74 | walletProvider: WalletProvider, 75 | params: MintParams 76 | ): Promise { 77 | const walletClient = walletProvider.getWalletClient(); 78 | const contract = walletClient.open(walletProvider.wallet); 79 | const seqno = await contract.getSeqno(); 80 | await contract.sendTransfer({ 81 | seqno, 82 | secretKey: walletProvider.keypair.secretKey, 83 | messages: [ 84 | internal({ 85 | value: "0.05", 86 | to: this.collectionAddress, 87 | body: this.createMintBody(params), 88 | }), 89 | ], 90 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 91 | }); 92 | return seqno; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/tests/providers/tokenProvider.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import { tonTokenPriceProvider } from "../../providers/tokenProvider"; 3 | 4 | describe("Token Price Provider", () => { 5 | it("should have correct name and description", () => { 6 | expect(tonTokenPriceProvider.name).toBe("tonTokenPriceProvider"); 7 | expect(tonTokenPriceProvider.description).toBe( 8 | "Provides real-time TON token and pair price information" 9 | ); 10 | }); 11 | 12 | it("should extract token from text correctly", () => { 13 | const testCases = [ 14 | { input: "What is the price of TON?", expected: "TON" }, 15 | { input: "Check the value of NOT token", expected: "NOT" }, 16 | { input: "What is the price of DOGS?", expected: "DOGS" }, 17 | ]; 18 | 19 | for (const testCase of testCases) { 20 | // Use private method via any cast for testing 21 | const result = (tonTokenPriceProvider as any).extractToken( 22 | testCase.input 23 | ); 24 | expect(result).toBe(testCase.expected); 25 | } 26 | }); 27 | 28 | it("should extract pair from text correctly", () => { 29 | const testCases = [ 30 | { input: "What is the price of TON/USDT?", expected: "TON/USDT" }, 31 | { input: "Check the value of NOT/TON pair", expected: "NOT/TON" }, 32 | ]; 33 | 34 | for (const testCase of testCases) { 35 | // Use private method via any cast for testing 36 | const result = (tonTokenPriceProvider as any).extractPair( 37 | testCase.input 38 | ); 39 | expect(result).toBe(testCase.expected); 40 | } 41 | }); 42 | 43 | it("should normalize token names correctly", () => { 44 | const testCases = [ 45 | { input: "notcoin", expected: "NOT" }, 46 | { input: "toncoin", expected: "TON" }, 47 | { input: "dedust", expected: "DDST" }, 48 | { input: "random", expected: "RANDOM" }, 49 | ]; 50 | 51 | for (const testCase of testCases) { 52 | // Use private method via any cast for testing 53 | const result = (tonTokenPriceProvider as any).normalizeToken( 54 | testCase.input 55 | ); 56 | expect(result).toBe(testCase.expected); 57 | } 58 | }); 59 | 60 | it("should format token price data correctly", () => { 61 | const mockData = { 62 | rates: { 63 | EQTest123: { 64 | prices: { USD: 2.123456 }, 65 | diff_24h: { USD: "+5.23" }, 66 | diff_7d: { USD: "-2.10" }, 67 | diff_30d: { USD: "+15.55" }, 68 | }, 69 | }, 70 | }; 71 | 72 | const result = tonTokenPriceProvider.formatTokenPriceData( 73 | "TON", 74 | "EQTest123", 75 | mockData 76 | ); 77 | expect(result).toContain("Current price: $2.123456 USD"); 78 | expect(result).toContain("24h change: +5.23"); 79 | expect(result).toContain("7d change: -2.10"); 80 | expect(result).toContain("30d change: +15.55"); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/services/nft-marketplace/interfaces/listings.ts: -------------------------------------------------------------------------------- 1 | import { Address, TupleReader } from "@ton/ton"; 2 | 3 | interface BaseListingData { 4 | listingAddress: Address; 5 | isAuction: boolean; 6 | } 7 | 8 | export interface FixedPriceListingData extends BaseListingData { 9 | isAuction: false; 10 | owner: Address; 11 | fullPrice: bigint; 12 | } 13 | 14 | export interface AuctionListingData extends BaseListingData { 15 | isAuction: true; 16 | owner: Address; 17 | fullPrice: bigint; 18 | minBid: bigint; 19 | lastBid: bigint; 20 | maxBid: bigint; 21 | endTime: number; 22 | } 23 | 24 | export type ListingData = FixedPriceListingData | AuctionListingData; 25 | 26 | export interface FixedPriceData { 27 | magic: number; 28 | isComplete: boolean; 29 | createdAt: number; 30 | marketplace: Address; 31 | nft: Address; 32 | owner: Address; 33 | fullPrice: bigint; 34 | marketFeeAddress: Address; 35 | marketFee: bigint; 36 | royaltyAddress: Address; 37 | royaltyAmount: bigint; 38 | } 39 | 40 | export interface AuctionData { 41 | magic: number; 42 | end: boolean; 43 | endTime: number; 44 | marketplace: Address; 45 | nft: Address; 46 | owner: Address; 47 | lastBid: bigint; 48 | lastMember: Address | null; 49 | minStep: number; 50 | marketFeeAddress: Address; 51 | mpFeeFactor: number; 52 | mpFeeBase: number; 53 | royaltyAddress: Address; 54 | royaltyFeeFactor: number; 55 | royaltyFeeBase: number; 56 | maxBid: bigint; 57 | minBid: bigint; 58 | createdAt: number; 59 | lastBidAt: number; 60 | isCanceled: boolean; 61 | } 62 | 63 | export function parseFixedPriceDataFromStack( 64 | stack: TupleReader 65 | ): FixedPriceData { 66 | return { 67 | magic: stack.readNumber(), 68 | isComplete: stack.readBoolean(), 69 | createdAt: stack.readNumber(), 70 | marketplace: stack.readAddress(), 71 | nft: stack.readAddress(), 72 | owner: stack.readAddress(), 73 | fullPrice: stack.readBigNumber(), 74 | marketFeeAddress: stack.readAddress(), 75 | marketFee: stack.readBigNumber(), 76 | royaltyAddress: stack.readAddress(), 77 | royaltyAmount: stack.readBigNumber(), 78 | }; 79 | } 80 | 81 | export function parseAuctionDataFromStack(stack: TupleReader): AuctionData { 82 | return { 83 | magic: stack.readNumber(), 84 | end: stack.readBoolean(), 85 | endTime: stack.readNumber(), 86 | marketplace: stack.readAddress(), 87 | nft: stack.readAddress(), 88 | owner: stack.readAddress(), 89 | lastBid: stack.readBigNumber(), 90 | lastMember: stack.readAddressOpt(), 91 | minStep: stack.readNumber(), 92 | marketFeeAddress: stack.readAddress(), 93 | mpFeeFactor: stack.readNumber(), 94 | mpFeeBase: stack.readNumber(), 95 | royaltyAddress: stack.readAddress(), 96 | royaltyFeeFactor: stack.readNumber(), 97 | royaltyFeeBase: stack.readNumber(), 98 | maxBid: stack.readBigNumber(), 99 | minBid: stack.readBigNumber(), 100 | createdAt: stack.readNumber(), 101 | lastBidAt: stack.readNumber(), 102 | isCanceled: stack.readBoolean(), 103 | }; 104 | } 105 | -------------------------------------------------------------------------------- /TUTORIAL.md: -------------------------------------------------------------------------------- 1 | ✈️ How to Use ElizaOS with plugin-ton 🚀 2 | 3 | 🌟 Getting Started with ElizaOS and TON Plugin 4 | 5 | 🔍 What is ElizaOS with plugin-ton? 6 | ElizaOS is an AI platform now integrated with the TON blockchain, enabling new possibilities for interacting with cryptocurrency and smart contracts through artificial intelligence. 7 | 8 | 📱 Installation and Setup 9 | 10 | 1. 📥 Install ElizaOS from source: https://github.com/elizaOS/eliza 11 | 2. 🔗 Setup ElizaOS here: https://eliza.how/docs/0.25.9/intro 12 | 3. 🧩 Enable the TON Plugin in your ElizaOS: npx elizaos plugins add @elizaos-plugins/ton-plugin 13 | 4. 🔄 Connect your TON wallet to start using blockchain features: https://github.com/elizaos-plugins/plugin-ton/blob/main/screenshot/connect.png 14 | 15 | ▎💼 Key Features 16 | 17 | 1. 🪙 TON Wallet Integration 18 | - Manage your TON assets directly through ElizaOS 19 | - Check balances and transaction history 20 | - Send and receive TON coins 21 | 22 | 2. 📝 Smart Contract Interaction 23 | - Deploy and interact with TON smart contracts 24 | - Execute contract methods via natural language commands 25 | 26 | 3. 🔍 Blockchain Data Access 27 | - Query TON blockchain data 28 | - Get real-time information about transactions and accounts 29 | 30 | 🛠️ Using TON Plugin Commands 31 | 32 | 💰 Wallet Operations 33 | 34 | - 👛 Check wallet status: 35 | - Ask: "What's my Wallet status?" 36 | - Example: https://github.com/elizaos-plugins/plugin-ton/blob/main/screenshot/status.png 37 | 38 | - 💸 Send TON: 39 | - Say: "Send 1.5 TON to EQA..." 40 | - Example: https://github.com/elizaos-plugins/plugin-ton/blob/main/screenshot/transfer.png 41 | 42 | 📊 Blockchain Queries 43 | 44 | - 🔍 Account Information: 45 | - Ask: "Show information about this TON address: EQA..." 46 | - Example: https://github.com/elizaos-plugins/plugin-ton/blob/main/screenshot/status.png 47 | 48 | - 📜 Transaction History: 49 | - Request: "Show my recent TON transactions" 50 | 51 | 📄 Smart Contract Interaction 52 | 53 | - 🚀 Deploy Contract: 54 | - Say: "Deploy my Jetton Minter in TON blockchain" 55 | - Example: https://github.com/elizaos-plugins/plugin-ton/blob/main/screenshot/deploy-jetton-minter.png 56 | 57 | - 🔄 Call Contract Method: 58 | - Request: "Call the 'transfer' method on my contract" 59 | - Example: https://github.com/elizaos-plugins/plugin-ton/blob/main/screenshot/get-colleciton-data.png 60 | 61 | ▎⚠️ Security Best Practices 62 | 63 | 1. 🔐 Never share your seed phrase or private keys 64 | 2. 🛡️ Verify addresses before sending any funds 65 | 3. 🧐 Double-check transactions before confirming them 66 | 4. 🔒 Use secure connections when accessing your wallet 67 | 68 | ▎🆘 Troubleshooting 69 | 70 | 1. 🔄 Plugin Not Working? 71 | - Ensure ElizaOS is updated to the latest version 72 | - Reinstall the TON plugin 73 | - Check your internet connection 74 | 75 | 2. 🚫 Transaction Failed? 76 | - Verify you have sufficient TON for the transaction and fees 77 | - Check that the recipient address is correct 78 | - Try again with a smaller amount first 79 | 80 | ▎📚 Resources and Support 81 | 82 | - 📖 Documentation: https://github.com/elizaos-plugins/plugin-ton 83 | - 💬 Community Support: Join the TON AI Narrative community 84 | - 🛠️ Technical Help: Submit issues on the GitHub repository 85 | 86 | ▎🚀 Join the AI + TON Revolution! 87 | 88 | Together we're building a future where artificial intelligence and blockchain work in harmony, creating new possibilities for everyone! ✨🌐 89 | -------------------------------------------------------------------------------- /BUG-BOUNTY.md: -------------------------------------------------------------------------------- 1 | # 🛠️ TON Plugin Bug Bounty Program 2 | 3 | The **TON Plugin Bug Bounty Program** is an open invitation for developers and security researchers to contribute to the stability and reliability of the AI ecosystem within TON. Your expertise and keen eye for detail can help us refine and strengthen the TON Plugin, ensuring it serves developers worldwide with top-tier functionality. 4 | 5 | ## 🌍 Why Participate? 6 | 7 | By joining this bug bounty, you're not just finding issues—you’re shaping the future of AI-powered applications in the **TON ecosystem**. Your contributions will: 8 | 9 | ✅ Improve the **security and reliability** of AI-driven integrations. 10 | ✅ Enhance the **developer experience**, making it easier for others to build on TON. 11 | ✅ Help **accelerate AI innovation** in a decentralized environment. 12 | ✅ Gain recognition for your expertise within a rapidly growing Web3 and AI community. 13 | 14 | --- 15 | 16 | ## 📝 How to Participate 17 | 18 | 1️⃣ **Find a Bug** – Identify any issue in the **TON Plugin** that affects functionality, stability, or security. 19 | 20 | 2️⃣ **Document It Clearly** – Provide a detailed description using the **Bug Report Template** below. 21 | 22 | 3️⃣ **Submit an Issue on GitHub** – Open a GitHub Issue in the repository and label it as **"bug report"**. 23 | 24 | 4️⃣ **Wait for Validation** – Our team will **verify and register** the bug with an additional label **"registered"** once it is confirmed. 25 | 26 | 5️⃣ **Fix the Bug (Developers Only)** – If you're a developer who contributed to the related functionality, you will be assigned to fix the issue. 27 | 28 | --- 29 | 30 | ## 🏆 Recognition for the Best Contributions 31 | 32 | To acknowledge outstanding contributions, the **top 3 most impactful bug reports** will receive a special prize. 33 | 34 | Your efforts directly contribute to building a more robust AI infrastructure in the TON ecosystem. Thank you for helping us push the boundaries of innovation! 🚀 35 | 36 | --- 37 | 38 | ## 📋 Bug Report Template 39 | 40 | When submitting a bug report, please use the following template to ensure clarity and reproducibility: 41 | 42 | **Title:** 43 | A concise title summarizing the issue. 44 | 45 | **Description:** 46 | Provide a detailed explanation of the bug. What is happening, and what should happen instead? 47 | 48 | **Steps to Reproduce:** 49 | 50 | 1. Step 1 – Describe the initial condition 51 | 2. Step 2 – Outline actions taken to trigger the bug 52 | 3. Step 3 – Expected vs. actual behavior 53 | 54 | **Environment:** 55 | 56 | - Operating System: [e.g., Windows, macOS, Linux] 57 | - Browser: [Chrome, Safari etc] 58 | - Package manager: [npm, yarn eth] 59 | - AI Provider: [OpenAI, Claude etc] 60 | 61 | **Screenshots & Logs:** 62 | Attach relevant screenshots or error logs to provide more context. 63 | 64 | **Related Pull Request (if applicable):** 65 | Provide a link to the PR related to this bug: [PR #]() 66 | 67 | **Possible Fix (if known):** 68 | If you have an idea of how this could be fixed, provide a brief suggestion. 69 | 70 | --- 71 | 72 | ### ⚠️ Bug Submission Requirements 73 | 74 | ✔️ **Be precise:** Clearly describe the issue with all necessary details. 75 | ✔️ **Use the template:** This helps streamline the validation process. 76 | ✔️ **Attach proof:** Screenshots, logs, and reproducible test cases increase credibility. 77 | ✔️ **Stay professional:** Keep communication constructive and focused on improvements. 78 | 79 | --- 80 | 81 | ### Submission Deadline 82 | 83 | 28th March 2025 84 | 85 | 🔗 **Submit your bug reports here:** [GitHub Issues](https://github.com/elizaos-plugins/plugin-ton/issues) 86 | 87 | 📢 **Join us in shaping the future of AI & Web3 on TON!** 🚀✨ 88 | -------------------------------------------------------------------------------- /src/utils/JettonWallet.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, Contract, toNano, internal } from "@ton/ton"; 2 | import { WalletProvider } from "../providers/wallet"; 3 | import { waitSeqnoContract } from "./util"; 4 | 5 | export const OP_CODES = { 6 | TRANSFER: 0x0f8a7ea5, 7 | BURN: 0x595f07bc, 8 | } as const; 9 | 10 | export class JettonWallet implements Contract { 11 | constructor( 12 | readonly address: Address, 13 | readonly init?: { code: Cell; data: Cell } 14 | ) {} 15 | 16 | static createFromAddress(address: Address) { 17 | return new JettonWallet(address); 18 | } 19 | 20 | /** 21 | * Helper method to send a transaction and wait for it to complete 22 | * @param walletProvider The wallet provider 23 | * @param to Destination address 24 | * @param value Amount of TON to send 25 | * @param body Message body 26 | */ 27 | private async sendTransaction( 28 | walletProvider: WalletProvider, 29 | to: Address, 30 | value: string | bigint, 31 | body: Cell 32 | ) { 33 | const provider = walletProvider.getWalletClient(); 34 | const contract = provider.open(walletProvider.wallet); 35 | const seqno = await contract.getSeqno(); 36 | 37 | await contract.sendTransfer({ 38 | seqno: seqno, 39 | secretKey: walletProvider.keypair.secretKey, 40 | messages: [ 41 | internal({ 42 | value, 43 | to, 44 | body, 45 | }), 46 | ], 47 | }); 48 | 49 | await waitSeqnoContract(seqno, contract); 50 | } 51 | 52 | static transferMessage( 53 | to: Address, 54 | amount: bigint, 55 | responseAddress: Address | null = null, 56 | forwardAmount: bigint = 0n, 57 | forwardPayload: Cell | null = null, 58 | query_id: number | bigint = 0 59 | ) { 60 | return beginCell() 61 | .storeUint(OP_CODES.TRANSFER, 32) 62 | .storeUint(query_id, 64) 63 | .storeCoins(amount) 64 | .storeAddress(to) 65 | .storeAddress(responseAddress) 66 | .storeBit(false) // null custom_payload 67 | .storeCoins(forwardAmount) 68 | .storeMaybeRef(forwardPayload) 69 | .endCell(); 70 | } 71 | 72 | async sendTransfer( 73 | walletProvider: WalletProvider, 74 | to: Address, 75 | amount: bigint, 76 | responseAddress: Address | null = null, 77 | forwardAmount: bigint = 0n, 78 | forwardPayload: Cell | null = null 79 | ) { 80 | await this.sendTransaction( 81 | walletProvider, 82 | this.address, 83 | toNano(0.05) + forwardAmount, 84 | JettonWallet.transferMessage( 85 | to, 86 | amount, 87 | responseAddress, 88 | forwardAmount, 89 | forwardPayload 90 | ) 91 | ); 92 | } 93 | 94 | async getWalletData(walletProvider: WalletProvider) { 95 | const client = walletProvider.getWalletClient(); 96 | const result = await client 97 | .provider(this.address) 98 | .get("get_wallet_data", []); 99 | const balance = result.stack.readBigNumber(); 100 | const owner = result.stack.readAddress(); 101 | const jettonMaster = result.stack.readAddress(); 102 | const walletCode = result.stack.readCell(); 103 | 104 | return { 105 | balance, 106 | owner, 107 | jettonMaster, 108 | walletCode, 109 | }; 110 | } 111 | 112 | async getBalance(provider: WalletProvider) { 113 | const data = await this.getWalletData(provider); 114 | return data.balance; 115 | } 116 | 117 | async getOwner(provider: WalletProvider) { 118 | const data = await this.getWalletData(provider); 119 | return data.owner; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Action, Plugin } from "@elizaos/core"; 2 | import transferAction from "./actions/transfer.ts"; 3 | import createWalletAction from "./actions/createWallet.ts"; 4 | import loadWalletAction from "./actions/loadWallet.ts"; 5 | // import borrowAction from "./actions/evaaBorrow"; 6 | // import supplyAction from "./actions/evaaSupply"; 7 | // import withdrawAction from "./actions/evaaWithdraw"; 8 | // import repayAction from "./actions/evaaRepay"; 9 | // import positionsAction from "./actions/evaaPositions"; 10 | import stakeAction from "./actions/stake.ts"; 11 | import unstakeAction from "./actions/unstake.ts"; 12 | import getPoolInfoAction from "./actions/getPoolInfo.ts"; 13 | import batchTransferAction from "./actions/batchTransfer.ts"; 14 | import auctionAction from "./actions/auctionInteraction.ts"; 15 | import createListingAction from "./actions/createListing.ts"; 16 | import buyListingAction from "./actions/buyListing.ts"; 17 | import createAuctionAction from "./actions/createAuction.ts"; 18 | import bidListingAction from "./actions/bidListing.ts"; 19 | import cancelListingAction from "./actions/cancelListing.ts"; 20 | import { WalletProvider, nativeWalletProvider } from "./providers/wallet.ts"; 21 | import transferNFTAction from "./actions/transferNFT.ts"; 22 | import mintNFTAction from "./actions/mintNFT.ts"; 23 | import getCollectionDataAction from "./actions/getCollectionData.ts"; 24 | import updateNFTMetadataAction from "./actions/updateNFTMetadata.ts"; 25 | import tokenPriceAction from "./actions/tokenPrice.ts"; 26 | import { tonTokenPriceProvider } from "./providers/tokenProvider.ts"; 27 | import jettonInteractionAction from "./actions/jettonInteraction.ts"; 28 | export { tokenPriceAction as GetTokenPrice }; 29 | import { StakingProvider, nativeStakingProvider } from "./providers/staking.ts"; 30 | import { swapStonAction } from "./actions/swapSton.ts"; 31 | import queryStonAssetAction from "./actions/queryStonAsset.ts"; 32 | import dexAction from "./actions/dex.ts"; 33 | 34 | export { 35 | WalletProvider, 36 | transferAction as TransferTonToken, 37 | createWalletAction as CreateTonWallet, 38 | loadWalletAction as LoadTonWallet, 39 | }; 40 | export { 41 | StakingProvider, 42 | stakeAction as StakeTonToken, 43 | unstakeAction as UnstakeTonToken, 44 | getPoolInfoAction as GetPoolInfoTonToken, 45 | }; 46 | import { tonConnectProvider } from "./providers/tonConnect.ts"; 47 | import { 48 | connectAction, 49 | disconnectAction, 50 | showConnectionStatusAction, 51 | } from "./actions/tonConnect.ts"; 52 | import tonConnectTransactionAction from "./actions/tonConnectTransaction.ts"; 53 | export { batchTransferAction as BatchTransferTokens }; 54 | export { auctionAction as AuctionInteractionActionTon }; 55 | 56 | export { 57 | getCollectionDataAction as GetCollectionData, 58 | updateNFTMetadataAction as UpdateNFTMetadata, 59 | mintNFTAction as MintNFT, 60 | transferNFTAction as TransferNFT, 61 | }; 62 | export { dexAction as DexAction }; 63 | export { jettonInteractionAction as JettonInteractionActionTon }; 64 | export const tonPlugin: Plugin = { 65 | name: "ton", 66 | description: "Ton Plugin for Eliza", 67 | actions: [ 68 | transferAction, 69 | createWalletAction, 70 | loadWalletAction, 71 | stakeAction, 72 | unstakeAction, 73 | // borrowAction, 74 | // supplyAction, 75 | // withdrawAction, 76 | // repayAction, 77 | // positionsAction, 78 | getPoolInfoAction, 79 | batchTransferAction, 80 | connectAction, 81 | disconnectAction, 82 | showConnectionStatusAction, 83 | tonConnectTransactionAction, 84 | tokenPriceAction, 85 | swapStonAction, 86 | queryStonAssetAction, 87 | createListingAction as Action, 88 | createAuctionAction as Action, 89 | bidListingAction as Action, 90 | buyListingAction as Action, 91 | cancelListingAction as Action, 92 | auctionAction as Action, 93 | transferNFTAction as Action, 94 | mintNFTAction as Action, 95 | updateNFTMetadataAction as Action, 96 | getCollectionDataAction as Action, 97 | dexAction as Action, 98 | jettonInteractionAction as Action, 99 | ], 100 | evaluators: [], 101 | providers: [ 102 | nativeWalletProvider, 103 | nativeStakingProvider, 104 | tonConnectProvider, 105 | tonTokenPriceProvider, 106 | ], 107 | }; 108 | 109 | export default tonPlugin; 110 | -------------------------------------------------------------------------------- /.github/workflows/npm-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | branches: 6 | - 1.x 7 | workflow_dispatch: 8 | 9 | jobs: 10 | verify_version: 11 | runs-on: ubuntu-latest 12 | outputs: 13 | should_publish: ${{ steps.check.outputs.should_publish }} 14 | version: ${{ steps.check.outputs.version }} 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Check if package.json version changed 22 | id: check 23 | run: | 24 | echo "Current branch: ${{ github.ref }}" 25 | 26 | # Get current version 27 | CURRENT_VERSION=$(jq -r .version package.json) 28 | echo "Current version: $CURRENT_VERSION" 29 | 30 | # Get previous commit hash 31 | git rev-parse HEAD~1 || git rev-parse HEAD 32 | PREV_COMMIT=$(git rev-parse HEAD~1 2>/dev/null || git rev-parse HEAD) 33 | 34 | # Check if package.json changed 35 | if git diff --name-only HEAD~1 HEAD | grep "package.json"; then 36 | echo "Package.json was changed in this commit" 37 | 38 | # Get previous version if possible 39 | if git show "$PREV_COMMIT:package.json" 2>/dev/null; then 40 | PREV_VERSION=$(git show "$PREV_COMMIT:package.json" | jq -r .version) 41 | echo "Previous version: $PREV_VERSION" 42 | 43 | if [ "$CURRENT_VERSION" != "$PREV_VERSION" ]; then 44 | echo "Version changed from $PREV_VERSION to $CURRENT_VERSION" 45 | echo "should_publish=true" >> $GITHUB_OUTPUT 46 | else 47 | echo "Version unchanged" 48 | echo "should_publish=false" >> $GITHUB_OUTPUT 49 | fi 50 | else 51 | echo "First commit with package.json, will publish" 52 | echo "should_publish=true" >> $GITHUB_OUTPUT 53 | fi 54 | else 55 | echo "Package.json not changed in this commit" 56 | echo "should_publish=false" >> $GITHUB_OUTPUT 57 | fi 58 | 59 | echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT 60 | 61 | publish: 62 | needs: verify_version 63 | if: needs.verify_version.outputs.should_publish == 'true' 64 | runs-on: ubuntu-latest 65 | permissions: 66 | contents: write 67 | steps: 68 | - name: Checkout repository 69 | uses: actions/checkout@v4 70 | with: 71 | fetch-depth: 0 72 | 73 | - name: Create Git tag 74 | run: | 75 | git config user.name "github-actions[bot]" 76 | git config user.email "github-actions[bot]@users.noreply.github.com" 77 | git tag -a "v${{ needs.verify_version.outputs.version }}" -m "Release v${{ needs.verify_version.outputs.version }}" 78 | git push origin "v${{ needs.verify_version.outputs.version }}" 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | - name: Setup Bun 83 | uses: oven-sh/setup-bun@v2 84 | 85 | - name: Install dependencies 86 | run: bun install 87 | 88 | - name: Build package 89 | run: bun run build 90 | 91 | - name: Publish to npm 92 | run: bun publish 93 | env: 94 | NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} 95 | 96 | create_release: 97 | needs: [verify_version, publish] 98 | if: needs.verify_version.outputs.should_publish == 'true' 99 | runs-on: ubuntu-latest 100 | permissions: 101 | contents: write 102 | steps: 103 | - name: Create GitHub Release 104 | uses: actions/create-release@v1 105 | env: 106 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 107 | with: 108 | tag_name: "v${{ needs.verify_version.outputs.version }}" 109 | release_name: "v${{ needs.verify_version.outputs.version }}" 110 | body: "Release v${{ needs.verify_version.outputs.version }}" 111 | draft: false 112 | prerelease: false 113 | -------------------------------------------------------------------------------- /src/utils/NFTCollection.ts: -------------------------------------------------------------------------------- 1 | import { 2 | beginCell, 3 | Address, 4 | Cell, 5 | internal, 6 | contractAddress, 7 | SendMode, 8 | StateInit, 9 | } from "@ton/ton"; 10 | import { encodeOffChainContent } from "./util"; 11 | import { WalletProvider } from "../providers/wallet"; 12 | export type CollectionData = { 13 | ownerAddress: Address; 14 | royaltyPercent: number; 15 | royaltyAddress: Address; 16 | nextItemIndex: number; 17 | collectionContentUrl: string; 18 | commonContentUrl: string; 19 | }; 20 | 21 | export type MintParams = { 22 | queryId: number | null; 23 | itemOwnerAddress: Address; 24 | itemIndex: number; 25 | amount: bigint; 26 | commonContentUrl: string; 27 | }; 28 | 29 | export class NFTCollection { 30 | private collectionData: CollectionData; 31 | 32 | constructor(collectionData: CollectionData) { 33 | this.collectionData = collectionData; 34 | } 35 | 36 | private createCodeCell(): Cell { 37 | const NftCollectionCodeBoc = 38 | "te6cckECFAEAAh8AART/APSkE/S88sgLAQIBYgkCAgEgBAMAJbyC32omh9IGmf6mpqGC3oahgsQCASAIBQIBIAcGAC209H2omh9IGmf6mpqGAovgngCOAD4AsAAvtdr9qJofSBpn+pqahg2IOhph+mH/SAYQAEO4tdMe1E0PpA0z/U1NQwECRfBNDUMdQw0HHIywcBzxbMyYAgLNDwoCASAMCwA9Ra8ARwIfAFd4AYyMsFWM8WUAT6AhPLaxLMzMlx+wCAIBIA4NABs+QB0yMsCEsoHy//J0IAAtAHIyz/4KM8WyXAgyMsBE/QA9ADLAMmAE59EGOASK3wAOhpgYC42Eit8H0gGADpj+mf9qJofSBpn+pqahhBCDSenKgpQF1HFBuvgoDoQQhUZYBWuEAIZGWCqALnixJ9AQpltQnlj+WfgOeLZMAgfYBwGyi544L5cMiS4ADxgRLgAXGBEuAB8YEYGYHgAkExIREAA8jhXU1DAQNEEwyFAFzxYTyz/MzMzJ7VTgXwSED/LwACwyNAH6QDBBRMhQBc8WE8s/zMzMye1UAKY1cAPUMI43gED0lm+lII4pBqQggQD6vpPywY/egQGTIaBTJbvy9AL6ANQwIlRLMPAGI7qTAqQC3gSSbCHis+YwMlBEQxPIUAXPFhPLP8zMzMntVABgNQLTP1MTu/LhklMTugH6ANQwKBA0WfAGjhIBpENDyFAFzxYTyz/MzMzJ7VSSXwXiN0CayQ=="; 39 | return Cell.fromBase64(NftCollectionCodeBoc); 40 | } 41 | 42 | private createDataCell(): Cell { 43 | const data = this.collectionData; 44 | const dataCell = beginCell(); 45 | 46 | dataCell.storeAddress(data.ownerAddress); 47 | dataCell.storeUint(data.nextItemIndex, 64); 48 | const contentCell = beginCell(); 49 | 50 | const collectionContent = encodeOffChainContent( 51 | data.collectionContentUrl 52 | ); 53 | 54 | const commonContent = beginCell(); 55 | commonContent.storeBuffer(Buffer.from(data.commonContentUrl)); 56 | 57 | contentCell.storeRef(collectionContent); 58 | contentCell.storeRef(commonContent.asCell()); 59 | dataCell.storeRef(contentCell); 60 | const NftItemCodeCell = Cell.fromBase64( 61 | "te6cckECDQEAAdAAART/APSkE/S88sgLAQIBYgMCAAmhH5/gBQICzgcEAgEgBgUAHQDyMs/WM8WAc8WzMntVIAA7O1E0NM/+kAg10nCAJp/AfpA1DAQJBAj4DBwWW1tgAgEgCQgAET6RDBwuvLhTYALXDIhxwCSXwPg0NMDAXGwkl8D4PpA+kAx+gAxcdch+gAx+gAw8AIEs44UMGwiNFIyxwXy4ZUB+kDUMBAj8APgBtMf0z+CEF/MPRRSMLqOhzIQN14yQBPgMDQ0NTWCEC/LJqISuuMCXwSED/LwgCwoAcnCCEIt3FzUFyMv/UATPFhAkgEBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7AAH2UTXHBfLhkfpAIfAB+kDSADH6AIIK+vCAG6EhlFMVoKHeItcLAcMAIJIGoZE24iDC//LhkiGOPoIQBRONkchQCc8WUAvPFnEkSRRURqBwgBDIywVQB88WUAX6AhXLahLLH8s/Im6zlFjPFwGRMuIByQH7ABBHlBAqN1viDACCAo41JvABghDVMnbbEDdEAG1xcIAQyMsFUAfPFlAF+gIVy2oSyx/LPyJus5RYzxcBkTLiAckB+wCTMDI04lUC8ANqhGIu" 62 | ); 63 | dataCell.storeRef(NftItemCodeCell); 64 | const royaltyBase = 1000; 65 | const royaltyFactor = Math.floor(data.royaltyPercent * royaltyBase); 66 | const royaltyCell = beginCell(); 67 | royaltyCell.storeUint(royaltyFactor, 16); 68 | royaltyCell.storeUint(royaltyBase, 16); 69 | royaltyCell.storeAddress(data.royaltyAddress); 70 | dataCell.storeRef(royaltyCell); 71 | 72 | return dataCell.endCell(); 73 | } 74 | 75 | public get stateInit(): StateInit { 76 | const code = this.createCodeCell(); 77 | const data = this.createDataCell(); 78 | 79 | return { code, data }; 80 | } 81 | 82 | public get address(): Address { 83 | return contractAddress(0, this.stateInit); 84 | } 85 | 86 | public async deploy(walletProvider: WalletProvider) { 87 | const walletClient = walletProvider.getWalletClient(); 88 | const contract = walletClient.open(walletProvider.wallet); 89 | const seqno = await contract.getSeqno(); 90 | await contract.sendTransfer({ 91 | seqno, 92 | secretKey: walletProvider.keypair.secretKey, 93 | messages: [ 94 | internal({ 95 | value: "0.05", 96 | to: this.address, 97 | init: this.stateInit, 98 | }), 99 | ], 100 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 101 | }); 102 | return seqno; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/tests/swap.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from "bun:test"; 2 | import { 3 | swapStonAction, 4 | finishSwapStonAction, 5 | getPendingStonSwapDetailsAction, 6 | } from "../actions/swapSton"; 7 | 8 | describe("Swap Ston Actions", () => { 9 | describe("Swap Token Ston Action", () => { 10 | it("should have correct metadata", () => { 11 | expect(swapStonAction.name).toBe("SWAP_TOKEN_STON"); 12 | expect(swapStonAction.description).toContain( 13 | "Start a swap of tokens in TON blockchain through STON.fi DEX" 14 | ); 15 | expect(swapStonAction.similes).toContain("SWAP_TOKENS_STON"); 16 | }); 17 | 18 | it("should have validate function", () => { 19 | expect(typeof swapStonAction.validate).toBe("function"); 20 | }); 21 | 22 | it("should have handler function", () => { 23 | expect(typeof swapStonAction.handler).toBe("function"); 24 | }); 25 | 26 | it("should have examples", () => { 27 | expect(Array.isArray(swapStonAction.examples)).toBe(true); 28 | expect(swapStonAction.examples?.length).toBeGreaterThan(0); 29 | 30 | // Check first example structure 31 | const firstExample = swapStonAction.examples?.[0]; 32 | if (firstExample) { 33 | expect(Array.isArray(firstExample)).toBe(true); 34 | expect(firstExample.length).toBeGreaterThan(0); 35 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 36 | expect(firstExample[0]).toHaveProperty("content"); 37 | } 38 | }); 39 | }); 40 | 41 | describe("Finish Swap Token Ston Action", () => { 42 | it("should have correct metadata", () => { 43 | expect(finishSwapStonAction.name).toBe("FINISH_SWAP_TOKEN_STON"); 44 | expect(finishSwapStonAction.description).toContain( 45 | "Finish a pending swap of tokens in TON blockchain through STON.fi DEX" 46 | ); 47 | expect(finishSwapStonAction.similes).toContain( 48 | "FINISH_SWAP_TOKENS_STON" 49 | ); 50 | }); 51 | 52 | it("should have validate function", () => { 53 | expect(typeof finishSwapStonAction.validate).toBe("function"); 54 | }); 55 | 56 | it("should have handler function", () => { 57 | expect(typeof finishSwapStonAction.handler).toBe("function"); 58 | }); 59 | 60 | it("should have examples", () => { 61 | expect(Array.isArray(finishSwapStonAction.examples)).toBe(true); 62 | expect(finishSwapStonAction.examples?.length).toBeGreaterThan(0); 63 | 64 | // Check first example structure 65 | const firstExample = finishSwapStonAction.examples?.[0]; 66 | if (firstExample) { 67 | expect(Array.isArray(firstExample)).toBe(true); 68 | expect(firstExample.length).toBeGreaterThan(0); 69 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 70 | expect(firstExample[0]).toHaveProperty("content"); 71 | } 72 | }); 73 | }); 74 | 75 | describe("Get Pending Ston Swap Details Action", () => { 76 | it("should have correct metadata", () => { 77 | expect(getPendingStonSwapDetailsAction.name).toBe( 78 | "GET_PENDING_STON_SWAP_DETAILS" 79 | ); 80 | expect(getPendingStonSwapDetailsAction.description).toContain( 81 | "Get the details of the pending swap of tokens in TON blockchain through STON.fi DEX" 82 | ); 83 | }); 84 | 85 | it("should have validate function", () => { 86 | expect(typeof getPendingStonSwapDetailsAction.validate).toBe( 87 | "function" 88 | ); 89 | }); 90 | 91 | it("should have handler function", () => { 92 | expect(typeof getPendingStonSwapDetailsAction.handler).toBe( 93 | "function" 94 | ); 95 | }); 96 | 97 | it("should have examples", () => { 98 | expect( 99 | Array.isArray(getPendingStonSwapDetailsAction.examples) 100 | ).toBe(true); 101 | expect( 102 | getPendingStonSwapDetailsAction.examples?.length 103 | ).toBeGreaterThan(0); 104 | 105 | // Check first example structure 106 | const firstExample = getPendingStonSwapDetailsAction.examples?.[0]; 107 | if (firstExample) { 108 | expect(Array.isArray(firstExample)).toBe(true); 109 | expect(firstExample.length).toBeGreaterThan(0); 110 | // expect(firstExample[0]).toHaveProperty("user"); // Removed in v1 111 | expect(firstExample[0]).toHaveProperty("content"); 112 | } 113 | }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/actions/getPoolInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | composePromptFromState, 4 | parseKeyValueXml, 5 | type Content, 6 | type HandlerCallback, 7 | ModelType, 8 | type IAgentRuntime, 9 | type Memory, 10 | type State, 11 | } from "@elizaos/core"; 12 | import { initStakingProvider, IStakingProvider } from "../providers/staking"; 13 | 14 | export interface PoolInfoContent extends Content { 15 | poolId: string; 16 | } 17 | 18 | function isPoolInfoContent(content: Content): content is PoolInfoContent { 19 | return typeof content.poolId === "string"; 20 | } 21 | 22 | const getPoolInfoTemplate = `Extract the pool information from the conversation. 23 | 24 | {{recentMessages}} 25 | 26 | Extract the pool identifier (TON address) for which to fetch staking pool information. 27 | 28 | Respond with the extracted values in this XML format: 29 | 30 | TON_ADDRESS_HERE 31 | `; 32 | 33 | export class GetPoolInfoAction { 34 | constructor(private stakingProvider: IStakingProvider) {} 35 | 36 | async getPoolInfo(params: PoolInfoContent): Promise { 37 | elizaLogger.log(`Fetching pool info for pool (${params.poolId})`); 38 | try { 39 | // Call the staking provider's getPoolInfo method. 40 | const poolInfo = await this.stakingProvider.getFormattedPoolInfo( 41 | params.poolId 42 | ); 43 | return poolInfo; 44 | } catch (error) { 45 | const errorMessage = 46 | error instanceof Error ? error.message : String(error); 47 | throw new Error(`Fetching pool info failed: ${errorMessage}`); 48 | } 49 | } 50 | } 51 | 52 | const buildPoolInfoDetails = async ( 53 | runtime: IAgentRuntime, 54 | message: Memory, 55 | state: State 56 | ): Promise => { 57 | if (!state) { 58 | state = (await runtime.composeState(message)) as State; 59 | } 60 | 61 | const poolInfoContext = composePromptFromState({ 62 | state, 63 | template: getPoolInfoTemplate, 64 | }); 65 | 66 | const response = await runtime.useModel(ModelType.TEXT_SMALL, { 67 | prompt: poolInfoContext, 68 | }); 69 | 70 | const parsedResponse = parseKeyValueXml(response); 71 | 72 | return { 73 | poolId: parsedResponse?.poolId || "", 74 | } as PoolInfoContent; 75 | }; 76 | 77 | export default { 78 | name: "GET_POOL_INFO", 79 | similes: ["FETCH_POOL_INFO", "POOL_DATA", "GET_STAKING_INFO"], 80 | description: 81 | "Fetch detailed global staking pool information. Only perform if user is asking for a specific Pool Info, and NOT your stake.", 82 | handler: async ( 83 | runtime: IAgentRuntime, 84 | message: Memory, 85 | state?: State, 86 | options?: any, 87 | callback?: HandlerCallback 88 | ): Promise => { 89 | elizaLogger.log("Starting GET_POOL_INFO handler..."); 90 | const poolInfoDetails = await buildPoolInfoDetails( 91 | runtime, 92 | message, 93 | state || (await runtime.composeState(message)) 94 | ); 95 | 96 | if (!isPoolInfoContent(poolInfoDetails)) { 97 | elizaLogger.error("Invalid content for GET_POOL_INFO action."); 98 | if (callback) { 99 | callback({ 100 | text: "Invalid pool info details provided.", 101 | content: { error: "Invalid pool info content" }, 102 | }); 103 | } 104 | return; 105 | } 106 | 107 | try { 108 | const stakingProvider = await initStakingProvider(runtime); 109 | const action = new GetPoolInfoAction(stakingProvider); 110 | const poolInfo = await action.getPoolInfo(poolInfoDetails); 111 | 112 | if (callback) { 113 | callback({ 114 | text: `Successfully fetched pool info: \n${poolInfo}`, 115 | content: poolInfo, 116 | }); 117 | } 118 | return; 119 | } catch (error) { 120 | elizaLogger.error("Error fetching pool info:"); 121 | const errorMessage = 122 | error instanceof Error ? error.message : String(error); 123 | if (callback) { 124 | callback({ 125 | text: `Error fetching pool info: ${errorMessage}`, 126 | content: { error: errorMessage }, 127 | }); 128 | } 129 | return; 130 | } 131 | }, 132 | template: getPoolInfoTemplate, 133 | validate: async (runtime: IAgentRuntime) => true, 134 | examples: [ 135 | [ 136 | { 137 | user: "{{user1}}", 138 | name: "{{user1}}", 139 | content: { 140 | text: "Get info for pool pool123", 141 | action: "GET_POOL_INFO", 142 | }, 143 | }, 144 | { 145 | user: "assistant", 146 | name: "{{agent}}", 147 | content: { 148 | text: "Fetching pool info...", 149 | action: "GET_POOL_INFO", 150 | }, 151 | }, 152 | { 153 | user: "assistant", 154 | name: "{{agent}}", 155 | content: { 156 | text: 'Fetched pool info for pool pool123: { "totalStaked": 1000, "rewardRate": 0.05, ...}', 157 | }, 158 | }, 159 | ], 160 | ], 161 | }; 162 | -------------------------------------------------------------------------------- /src/services/nft-marketplace/listingData.ts: -------------------------------------------------------------------------------- 1 | import { Address, TupleReader } from "@ton/ton"; 2 | import { WalletProvider } from "../../providers/wallet"; 3 | import { getNftOwner } from "../../utils/NFTItem"; 4 | import { 5 | ListingData, 6 | FixedPriceListingData, 7 | AuctionListingData, 8 | FixedPriceData, 9 | AuctionData, 10 | parseFixedPriceDataFromStack, 11 | parseAuctionDataFromStack 12 | } from "./interfaces/listings.ts"; 13 | 14 | export function isAuction(stack: TupleReader): boolean { 15 | return stack.remaining === 20; 16 | } 17 | 18 | export async function getListingData(walletProvider: WalletProvider, nftAddress: string): Promise { 19 | try { 20 | const listingAddress = await getNftOwner(walletProvider, nftAddress); 21 | const client = walletProvider.getWalletClient(); 22 | const result = await client.runMethod(listingAddress, "get_sale_data"); 23 | 24 | if (!isAuction(result.stack)) { 25 | return parseFixedPriceData(listingAddress, result.stack); 26 | } else { 27 | return parseAuctionData(listingAddress, result.stack); 28 | } 29 | } catch (error) { 30 | throw new Error("Failed to get listing data"); 31 | } 32 | } 33 | 34 | function parseFixedPriceData(listingAddress: Address, stack: TupleReader): FixedPriceListingData { 35 | const fullData = parseFixedPriceDataFromStack(stack); 36 | 37 | // Return only what's needed for the simplified interface 38 | return { 39 | listingAddress, 40 | owner: fullData.owner, 41 | fullPrice: fullData.fullPrice, 42 | isAuction: false 43 | }; 44 | } 45 | 46 | function parseAuctionData(listingAddress: Address, stack: TupleReader): AuctionListingData { 47 | const fullData = parseAuctionDataFromStack(stack); 48 | 49 | // Return only what's needed for the simplified interface 50 | return { 51 | listingAddress, 52 | owner: fullData.owner, 53 | fullPrice: fullData.maxBid, // Max bid serves as the "buy now" price 54 | minBid: fullData.minBid, 55 | lastBid: fullData.lastBid, 56 | maxBid: fullData.maxBid, 57 | endTime: fullData.endTime, 58 | isAuction: true 59 | }; 60 | } 61 | 62 | export async function getFixedPriceData(walletProvider: WalletProvider, nftAddress: string): Promise { 63 | try { 64 | const listingAddress = await getNftOwner(walletProvider, nftAddress); 65 | const client = walletProvider.getWalletClient(); 66 | const result = await client.runMethod(listingAddress, "get_sale_data"); 67 | 68 | if (isAuction(result.stack)) { 69 | throw new Error("Not a fixed price listing"); 70 | } 71 | 72 | const data = parseFixedPriceDataFromStack(result.stack); 73 | 74 | // Return with listingAddress attached 75 | return { 76 | ...data, 77 | listingAddress 78 | }; 79 | } catch (error) { 80 | throw new Error("Failed to get fixed price data"); 81 | } 82 | } 83 | 84 | export async function getAuctionData(walletProvider: WalletProvider, nftAddress: string): Promise { 85 | try { 86 | const listingAddress = await getNftOwner(walletProvider, nftAddress); 87 | const client = walletProvider.getWalletClient(); 88 | const result = await client.runMethod(listingAddress, "get_sale_data"); 89 | 90 | if (!isAuction(result.stack)) { 91 | throw new Error("Not an auction listing"); 92 | } 93 | 94 | const data = parseAuctionDataFromStack(result.stack); 95 | 96 | // Return with listingAddress attached 97 | return { 98 | ...data, 99 | listingAddress 100 | }; 101 | } catch (error) { 102 | throw new Error("Failed to get auction data"); 103 | } 104 | } 105 | 106 | export async function getBuyPrice(walletProvider: WalletProvider, nftAddress: string): Promise { 107 | const listingData = await getListingData(walletProvider, nftAddress); 108 | return listingData.fullPrice; 109 | } 110 | 111 | export async function getMinBid(walletProvider: WalletProvider, nftAddress: string): Promise { 112 | const listingData = await getListingData(walletProvider, nftAddress); 113 | if (!listingData.isAuction) { 114 | throw new Error("Not an auction listing"); 115 | } 116 | return listingData.minBid; 117 | } 118 | 119 | export async function getLastBid(walletProvider: WalletProvider, nftAddress: string): Promise { 120 | const listingData = await getListingData(walletProvider, nftAddress); 121 | if (!listingData.isAuction) { 122 | throw new Error("Not an auction listing"); 123 | } 124 | return listingData.lastBid; 125 | } 126 | 127 | export async function isAuctionEnded(walletProvider: WalletProvider, nftAddress: string): Promise { 128 | const listingData = await getListingData(walletProvider, nftAddress); 129 | if (!listingData.isAuction) { 130 | throw new Error("Not an auction listing"); 131 | } 132 | 133 | const now = Math.floor(Date.now() / 1000); 134 | return now > listingData.endTime; 135 | } 136 | 137 | export async function getNextValidBidAmount(walletProvider: WalletProvider, nftAddress: string): Promise { 138 | const listingData = await getListingData(walletProvider, nftAddress); 139 | if (!listingData.isAuction) { 140 | throw new Error("Not an auction listing"); 141 | } 142 | 143 | if (listingData.lastBid === BigInt(0)) { 144 | return listingData.minBid; 145 | } 146 | 147 | // Get complete auction data to access minStep 148 | const auctionData = await getAuctionData(walletProvider, nftAddress); 149 | const minIncrement = (listingData.lastBid * BigInt(auctionData.minStep)) / BigInt(100); 150 | return listingData.lastBid + minIncrement; 151 | } 152 | 153 | -------------------------------------------------------------------------------- /src/services/nft-marketplace/listingTransactions.ts: -------------------------------------------------------------------------------- 1 | import { beginCell, internal, SendMode, toNano } from "@ton/ton"; 2 | import { 3 | getBuyPrice, 4 | getListingData, 5 | getMinBid, 6 | getNextValidBidAmount, 7 | isAuction, 8 | isAuctionEnded, 9 | } from "./listingData"; 10 | import { waitSeqnoContract } from "../../utils/util"; 11 | import { WalletProvider } from "../../providers/wallet"; 12 | 13 | export async function buyListing( 14 | walletProvider: WalletProvider, 15 | nftAddress: string 16 | ): Promise { 17 | try { 18 | const { listingAddress } = await getListingData( 19 | walletProvider, 20 | nftAddress 21 | ); 22 | const fullPrice = await getBuyPrice(walletProvider, nftAddress); 23 | 24 | // Calculate amount to send (price + gas) 25 | const gasAmount = toNano("1"); // 1 TON for gas 26 | const amountToSend = fullPrice + gasAmount; 27 | 28 | // Send the transaction to buy 29 | const client = walletProvider.getWalletClient(); 30 | const contract = client.open(walletProvider.wallet); 31 | 32 | const seqno = await contract.getSeqno(); 33 | const transferMessage = internal({ 34 | to: listingAddress, 35 | value: amountToSend, 36 | bounce: true, 37 | body: "", // Empty body for default buy operation 38 | }); 39 | 40 | await contract.sendTransfer({ 41 | seqno, 42 | secretKey: walletProvider.keypair.secretKey, 43 | messages: [transferMessage], 44 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 45 | }); 46 | 47 | await waitSeqnoContract(seqno, contract); 48 | 49 | return { 50 | nftAddress, 51 | listingAddress: listingAddress.toString(), 52 | price: fullPrice.toString(), 53 | message: "Buy transaction sent successfully", 54 | }; 55 | } catch (error) { 56 | throw new Error( 57 | `Failed to buy NFT: ${error instanceof Error ? error.message : String(error)}` 58 | ); 59 | } 60 | } 61 | 62 | export async function cancelListing( 63 | walletProvider: WalletProvider, 64 | nftAddress: string 65 | ): Promise { 66 | try { 67 | const listingData = await getListingData(walletProvider, nftAddress); 68 | 69 | // Opcode for cancellation 70 | const opcode = listingData.isAuction ? 1 : 3; // 1 for auction, 3 for fixed price 71 | 72 | const msgBody = beginCell() 73 | .storeUint(opcode, 32) 74 | .storeUint(0, 64) 75 | .endCell(); // queryId = 0 76 | const gasAmount = toNano("0.2"); // 0.2 TON for cancellation gas 77 | 78 | // Send the transaction to cancel 79 | const client = walletProvider.getWalletClient(); 80 | const contract = client.open(walletProvider.wallet); 81 | 82 | const seqno = await contract.getSeqno(); 83 | const transferMessage = internal({ 84 | to: listingData.listingAddress, 85 | value: gasAmount, 86 | bounce: true, 87 | body: msgBody, 88 | }); 89 | 90 | await contract.sendTransfer({ 91 | seqno, 92 | secretKey: walletProvider.keypair.secretKey, 93 | messages: [transferMessage], 94 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 95 | }); 96 | 97 | await waitSeqnoContract(seqno, contract); 98 | 99 | return { 100 | nftAddress, 101 | listingAddress: listingData.listingAddress.toString(), 102 | message: "Cancel listing transaction sent successfully", 103 | }; 104 | } catch (error) { 105 | throw new Error( 106 | `Failed to cancel NFT listing: ${error instanceof Error ? error.message : String(error)}` 107 | ); 108 | } 109 | } 110 | 111 | export async function bidOnAuction( 112 | walletProvider: WalletProvider, 113 | nftAddress: string, 114 | bidAmount: bigint 115 | ): Promise { 116 | try { 117 | const listingData = await getListingData(walletProvider, nftAddress); 118 | 119 | if (!listingData.isAuction) { 120 | throw new Error( 121 | "Cannot bid on a fixed-price listing. Use buyListing instead." 122 | ); 123 | } 124 | 125 | // Check if auction has ended 126 | const auctionEnded = await isAuctionEnded(walletProvider, nftAddress); 127 | if (auctionEnded) { 128 | throw new Error("Auction has already ended."); 129 | } 130 | 131 | // If no bidAmount provided, get the next valid bid amount 132 | const bid = bidAmount; 133 | 134 | // Check if bid is valid 135 | const minBid = await getMinBid(walletProvider, nftAddress); 136 | if (bid < minBid) { 137 | throw new Error( 138 | `Bid too low. Minimum bid is ${minBid.toString()}.` 139 | ); 140 | } 141 | 142 | // Gas amount for the transaction 143 | const gasAmount = toNano("0.1"); 144 | const amountToSend = bid + gasAmount; 145 | 146 | // Send the bid transaction 147 | const client = walletProvider.getWalletClient(); 148 | const contract = client.open(walletProvider.wallet); 149 | 150 | const seqno = await contract.getSeqno(); 151 | 152 | const transferMessage = internal({ 153 | to: listingData.listingAddress, 154 | value: amountToSend, 155 | bounce: true, 156 | body: "", 157 | }); 158 | 159 | await contract.sendTransfer({ 160 | seqno, 161 | secretKey: walletProvider.keypair.secretKey, 162 | messages: [transferMessage], 163 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 164 | }); 165 | 166 | await waitSeqnoContract(seqno, contract); 167 | 168 | return { 169 | nftAddress, 170 | listingAddress: listingData.listingAddress.toString(), 171 | bidAmount: bid.toString(), 172 | message: "Bid placed successfully", 173 | }; 174 | } catch (error) { 175 | throw new Error( 176 | `Failed to place bid: ${error instanceof Error ? error.message : String(error)}` 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Cell, 5 | fromNano, 6 | MessageRelaxed, 7 | OpenedContract, 8 | toNano, 9 | TonClient, 10 | TupleReader, 11 | } from "@ton/ton"; 12 | import { StakingPlatform } from "../interfaces/stakingPlatform.ts"; 13 | import { internal } from "@ton/ton"; 14 | import { WalletProvider } from "../../../providers/wallet.ts"; 15 | import { elizaLogger } from "@elizaos/core"; 16 | 17 | import { 18 | Treasury, 19 | Wallet, 20 | Parent, 21 | TreasuryConfig, 22 | feeStake, 23 | feeUnstake, 24 | } from "./hipo/sdk/index.ts"; 25 | import { PoolInfo } from "../interfaces/pool.ts"; 26 | 27 | async function getTreasuryState( 28 | tonClient: TonClient, 29 | treasuryAddress: Address 30 | ): Promise { 31 | const treasuryInstance = Treasury; 32 | const treasury = tonClient.open( 33 | treasuryInstance.createFromAddress(treasuryAddress) 34 | ); 35 | return treasury.getTreasuryState(); 36 | } 37 | 38 | async function getHipoWallet( 39 | tonClient: TonClient, 40 | address: Address, 41 | treasuryAddress: Address 42 | ): Promise> { 43 | const treasuryState = await getTreasuryState(tonClient, treasuryAddress); 44 | 45 | if (!treasuryState.parent) throw new Error("No parent in treasury state"); 46 | const parent = tonClient.open( 47 | Parent.createFromAddress(treasuryState.parent) 48 | ); 49 | 50 | const walletAddress = await parent.getWalletAddress(address); 51 | 52 | // Get wallet contract 53 | const hipoWalletInstance = Wallet; 54 | const hipoWallet = tonClient.open( 55 | hipoWalletInstance.createFromAddress(walletAddress) 56 | ); 57 | 58 | return hipoWallet; 59 | } 60 | 61 | async function getExchangeRate( 62 | tonClient: TonClient, 63 | treasuryAddress: Address 64 | ): Promise { 65 | const treasuryState = await getTreasuryState(tonClient, treasuryAddress); 66 | return Number(treasuryState.totalTokens) / Number(treasuryState.totalCoins); 67 | } 68 | 69 | function calculateJettonsToTon(jettons: bigint, rate: number): bigint { 70 | console.info(jettons); 71 | return !rate || !jettons 72 | ? BigInt(0) 73 | : BigInt(toNano(Number(fromNano(jettons)) * (1 / rate))); 74 | } 75 | 76 | export class HipoStrategy implements StakingPlatform { 77 | constructor( 78 | readonly tonClient: TonClient, 79 | readonly walletProvider: WalletProvider 80 | ) {} 81 | 82 | async getPendingWithdrawal( 83 | address: Address, 84 | poolAddress: Address 85 | ): Promise { 86 | const hipoWallet = await getHipoWallet( 87 | this.tonClient, 88 | address, 89 | poolAddress 90 | ); 91 | const walletState = await hipoWallet.getWalletState(); 92 | 93 | const rate = await getExchangeRate(this.tonClient, poolAddress); 94 | return calculateJettonsToTon(walletState.unstaking, rate); 95 | } 96 | 97 | async getStakedTon( 98 | address: Address, 99 | poolAddress: Address 100 | ): Promise { 101 | const hipoWallet = await getHipoWallet( 102 | this.tonClient, 103 | address, 104 | poolAddress 105 | ); 106 | const walletState = await hipoWallet.getWalletState(); 107 | 108 | const rate = await getExchangeRate(this.tonClient, poolAddress); 109 | return calculateJettonsToTon(walletState.tokens, rate); 110 | } 111 | 112 | async getPoolInfo(poolAddress: Address): Promise { 113 | try { 114 | const result = await getTreasuryState(this.tonClient, poolAddress); 115 | const rate = await getExchangeRate(this.tonClient, poolAddress); 116 | return { 117 | address: poolAddress, 118 | min_stake: BigInt(0), 119 | deposit_fee: feeStake, 120 | withdraw_fee: feeUnstake, 121 | balance: calculateJettonsToTon(result.totalTokens, rate), 122 | pending_deposits: calculateJettonsToTon( 123 | result.totalStaking, 124 | rate 125 | ), 126 | pending_withdraws: calculateJettonsToTon( 127 | result.totalUnstaking, 128 | rate 129 | ), 130 | }; 131 | } catch (error) { 132 | console.error("Error fetching Hipo pool info:", error); 133 | throw error; 134 | } 135 | } 136 | 137 | async createStakeMessage( 138 | poolAddress: Address, 139 | amount: number 140 | ): Promise { 141 | const payload = beginCell() 142 | .storeUint(0x3d3761a6, 32) 143 | .storeUint(0n, 64) 144 | .storeAddress(null) 145 | .storeCoins(toNano(amount)) 146 | .storeCoins(1n) 147 | .storeAddress(null) 148 | .endCell(); 149 | 150 | const intMessage = internal({ 151 | to: poolAddress, 152 | value: toNano(amount) + 100000000n, 153 | body: payload, 154 | bounce: true, 155 | init: null, 156 | }); 157 | 158 | return intMessage; 159 | } 160 | 161 | async createUnstakeMessage( 162 | poolAddress: Address, 163 | amount: number 164 | ): Promise { 165 | const rate = await getExchangeRate(this.tonClient, poolAddress); 166 | 167 | const jettonAmount = amount * rate; 168 | 169 | const payload = beginCell() 170 | .storeUint(0x595f07bc, 32) 171 | .storeUint(0n, 64) 172 | .storeCoins(toNano(jettonAmount)) 173 | .storeAddress(undefined) 174 | .storeMaybeRef(beginCell().storeUint(0, 4).storeCoins(1n)) 175 | .endCell(); 176 | 177 | const hipoWallet = await getHipoWallet( 178 | this.tonClient, 179 | Address.parse(this.walletProvider.getAddress()), 180 | poolAddress 181 | ); 182 | 183 | const intMessage = internal({ 184 | to: hipoWallet.address, 185 | value: 100000000n, 186 | body: payload, 187 | bounce: true, 188 | init: null, 189 | }); 190 | 191 | return intMessage; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/utils/util.ts: -------------------------------------------------------------------------------- 1 | import { Address, beginCell, Cell, internal, SendMode } from "@ton/ton"; 2 | import pinataSDK from "@pinata/sdk"; 3 | 4 | import { readdirSync } from "fs"; 5 | import { writeFile, readFile } from "fs/promises"; 6 | import path from "path"; 7 | import { WalletProvider } from "../providers/wallet"; 8 | // import { MintParams } from "./NFTCollection"; 9 | export const sleep = async (ms: number) => { 10 | await new Promise((resolve) => setTimeout(resolve, ms)); 11 | }; 12 | 13 | export const base64ToHex = (base64: string) => { 14 | return Buffer.from(base64, "base64").toString("hex"); 15 | }; 16 | 17 | export function bufferToChunks(buff: Buffer, chunkSize: number) { 18 | const chunks: Buffer[] = []; 19 | while (buff.byteLength > 0) { 20 | chunks.push(buff.subarray(0, chunkSize)); 21 | buff = buff.subarray(chunkSize); 22 | } 23 | return chunks; 24 | } 25 | 26 | export function makeSnakeCell(data: Buffer): Cell { 27 | const chunks = bufferToChunks(data, 127); 28 | 29 | if (chunks.length === 0) { 30 | return beginCell().endCell(); 31 | } 32 | 33 | if (chunks.length === 1) { 34 | return beginCell().storeBuffer(chunks[0]).endCell(); 35 | } 36 | 37 | let curCell = beginCell(); 38 | 39 | for (let i = chunks.length - 1; i >= 0; i--) { 40 | const chunk = chunks[i]; 41 | 42 | curCell.storeBuffer(chunk); 43 | 44 | if (i - 1 >= 0) { 45 | const nextCell = beginCell(); 46 | nextCell.storeRef(curCell); 47 | curCell = nextCell; 48 | } 49 | } 50 | 51 | return curCell.endCell(); 52 | } 53 | 54 | export function encodeOffChainContent(content: string) { 55 | let data = Buffer.from(content); 56 | const offChainPrefix = Buffer.from([0x01]); 57 | data = Buffer.concat([offChainPrefix, data]); 58 | return makeSnakeCell(data); 59 | } 60 | 61 | export async function waitSeqno(seqno: number, wallet: any) { 62 | for (let attempt = 0; attempt < 10; attempt++) { 63 | await sleep(2000); 64 | const seqnoAfter = await wallet.contract.getSeqno(); 65 | if (seqnoAfter == seqno + 1) break; 66 | } 67 | } 68 | 69 | export async function uploadFolderToIPFS(folderPath: string): Promise { 70 | const pinata = new pinataSDK({ 71 | pinataApiKey: process.env.PINATA_API_KEY, 72 | pinataSecretApiKey: process.env.PINATA_API_SECRET, 73 | }); 74 | 75 | const response = await pinata.pinFromFS(folderPath); 76 | return response.IpfsHash; 77 | } 78 | 79 | export async function updateMetadataFiles( 80 | metadataFolderPath: string, 81 | imagesIpfsHash: string 82 | ): Promise { 83 | const files = readdirSync(metadataFolderPath); 84 | 85 | files.forEach(async (filename, index) => { 86 | const filePath = path.join(metadataFolderPath, filename); 87 | const file = await readFile(filePath); 88 | 89 | const metadata = JSON.parse(file.toString()); 90 | metadata.image = 91 | index != files.length - 1 92 | ? `ipfs://${imagesIpfsHash}/${index}.jpg` 93 | : `ipfs://${imagesIpfsHash}/logo.jpg`; 94 | 95 | await writeFile(filePath, JSON.stringify(metadata)); 96 | }); 97 | } 98 | 99 | export async function uploadJSONToIPFS(json: any): Promise { 100 | const pinata = new pinataSDK({ 101 | pinataApiKey: process.env.PINATA_API_KEY, 102 | pinataSecretApiKey: process.env.PINATA_API_SECRET, 103 | }); 104 | 105 | const response = await pinata.pinJSONToIPFS(json); 106 | return response.IpfsHash; 107 | } 108 | 109 | export function formatCurrency(amount: string, digits: number): string { 110 | try { 111 | return parseFloat(amount).toFixed(digits).toString(); 112 | } catch (e) { 113 | return "0"; 114 | } 115 | } 116 | 117 | export async function topUpBalance( 118 | walletProvider: WalletProvider, 119 | nftAmount: number, 120 | collectionAddress: string 121 | ): Promise { 122 | const feeAmount = 0.026; // approximate value of fees for 1 transaction in our case 123 | const walletClient = walletProvider.getWalletClient(); 124 | const contract = walletClient.open(walletProvider.wallet); 125 | const seqno = await contract.getSeqno(); 126 | const amount = nftAmount * feeAmount; 127 | 128 | await contract.sendTransfer({ 129 | seqno, 130 | secretKey: walletProvider.keypair.secretKey, 131 | messages: [ 132 | internal({ 133 | value: amount.toString(), 134 | to: collectionAddress, 135 | bounce: false, 136 | }), 137 | ], 138 | sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, 139 | }); 140 | 141 | return seqno; 142 | } 143 | 144 | export async function waitSeqnoContract(seqno: number, contract: any) { 145 | for (let attempt = 0; attempt < 10; attempt++) { 146 | await sleep(2000); 147 | console.log("Transaction sent, still waiting for confirmation..."); 148 | 149 | const seqnoAfter: number = await contract.getSeqno(); 150 | if (seqnoAfter == seqno + 1) break; 151 | } 152 | } 153 | 154 | export function sanitizeTonAddress( 155 | input: string, 156 | bounceable?: boolean, 157 | testOnly?: boolean 158 | ): string | null { 159 | try { 160 | // Parse the input into a normalized address 161 | const address = Address.parse(input); 162 | 163 | // Convert to the desired format based on the provided flags 164 | const sanitizedAddress = address.toString({ 165 | bounceable: bounceable ?? false, 166 | testOnly: testOnly ?? false, 167 | }); 168 | 169 | return sanitizedAddress; 170 | } catch (error) { 171 | console.error( 172 | "Invalid TON address:", 173 | error instanceof Error ? error.message : String(error) 174 | ); 175 | return null; // Return null if the address is invalid 176 | } 177 | } 178 | 179 | /** 180 | * Converts an input (string or number) to a BigInt. 181 | * 182 | * The input may contain underscore separators (e.g. "50_000") which are removed. 183 | * The returned value is a BigInt (e.g. 50_000n). 184 | * 185 | * @param input - The input string or number. 186 | * @returns The corresponding BigInt. 187 | */ 188 | export function convertToBigInt(input: string | number): bigint { 189 | // If the input is a string, remove underscores; otherwise, just convert the number. 190 | const cleanedInput = 191 | typeof input === "string" ? input.replace(/_/g, "") : input; 192 | return BigInt(cleanedInput); 193 | } 194 | -------------------------------------------------------------------------------- /src/actions/buyListing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | composePromptFromState, 4 | parseKeyValueXml, 5 | ModelType, 6 | type IAgentRuntime, 7 | type Memory, 8 | type State, 9 | type HandlerCallback, 10 | Content, 11 | } from "@elizaos/core"; 12 | import { Address, internal, SendMode, toNano } from "@ton/ton"; 13 | import { z } from "zod"; 14 | import { initWalletProvider, WalletProvider } from "../providers/wallet"; 15 | import { waitSeqnoContract } from "../utils/util"; 16 | import { 17 | getBuyPrice, 18 | getListingData, 19 | } from "../services/nft-marketplace/listingData"; 20 | import { buyListing } from "../services/nft-marketplace/listingTransactions"; 21 | 22 | /** 23 | * Schema for buy listing input. 24 | * Only requires: 25 | * - nftAddress: The NFT contract address. 26 | */ 27 | const buyListingSchema = z 28 | .object({ 29 | nftAddress: z.string().nonempty("NFT address is required"), 30 | }) 31 | .refine((data) => data.nftAddress, { 32 | message: "NFT address is required", 33 | path: ["nftAddress"], 34 | }); 35 | 36 | export interface BuyListingContent extends Content { 37 | nftAddress: string; 38 | } 39 | 40 | function isBuyListingContent(content: Content): content is BuyListingContent { 41 | return typeof content.nftAddress === "string"; 42 | } 43 | 44 | const buyListingTemplate = ` 45 | Analyze the conversation and extract the following information about the requested NFT purchase: 46 | - NFT address to buy 47 | 48 | Format the response as key-value pairs in XML format. 49 | 50 | 51 | {{recentMessages}} 52 | 53 | 54 | Extract the NFT purchase details from the conversation above. 55 | Respond with the following format: 56 | 57 | nft_address_to_buy 58 | `; 59 | 60 | /** 61 | * Helper function to build buy listing parameters. 62 | */ 63 | const buildBuyListingData = async ( 64 | runtime: IAgentRuntime, 65 | message: Memory, 66 | state?: State 67 | ): Promise => { 68 | // Initialize or update state 69 | let currentState = state; 70 | if (!currentState) { 71 | currentState = (await runtime.composeState(message)) as State; 72 | } else { 73 | currentState = await runtime.composeState(message, ["RECENT_MESSAGES"]); 74 | } 75 | 76 | const prompt = composePromptFromState({ 77 | state: currentState, 78 | template: buyListingTemplate, 79 | }); 80 | 81 | const result = await runtime.useModel(ModelType.TEXT_SMALL, { 82 | prompt, 83 | }); 84 | 85 | const parsedContent = parseKeyValueXml( 86 | typeof result === "string" ? result : (result as any).value || "" 87 | ); 88 | 89 | const buyContent: BuyListingContent = { 90 | nftAddress: parsedContent?.nftAddress || "", 91 | text: "", // Required by Content interface 92 | }; 93 | 94 | // Validate with schema 95 | const validatedContent = buyListingSchema.parse(buyContent); 96 | return validatedContent as BuyListingContent; 97 | }; 98 | 99 | /** 100 | * BuyListingAction encapsulates the logic to buy an NFT listing. 101 | */ 102 | export class BuyListingAction { 103 | private walletProvider: WalletProvider; 104 | constructor(walletProvider: WalletProvider) { 105 | this.walletProvider = walletProvider; 106 | } 107 | 108 | /** 109 | * Buys an NFT listing 110 | */ 111 | async buy(nftAddress: string): Promise { 112 | try { 113 | elizaLogger.log(`Starting purchase of NFT: ${nftAddress}`); 114 | 115 | const receipt = await buyListing(this.walletProvider, nftAddress); 116 | 117 | return receipt; 118 | } catch (error) { 119 | elizaLogger.error(`Error buying NFT ${nftAddress}: ${error}`); 120 | throw new Error( 121 | `Failed to buy NFT: ${error instanceof Error ? error.message : String(error)}` 122 | ); 123 | } 124 | } 125 | } 126 | 127 | export default { 128 | name: "BUY_LISTING", 129 | similes: ["NFT_BUY", "PURCHASE_NFT", "BUY_NFT"], 130 | description: 131 | "Buys a listed NFT by sending the required payment to the listing contract.", 132 | handler: async ( 133 | runtime: IAgentRuntime, 134 | message: Memory, 135 | state?: State, 136 | _options?: any, 137 | callback?: HandlerCallback 138 | ): Promise => { 139 | elizaLogger.log("Starting BUY_LISTING handler..."); 140 | const params = await buildBuyListingData( 141 | runtime, 142 | message, 143 | state || (await runtime.composeState(message)) 144 | ); 145 | 146 | if (!isBuyListingContent(params)) { 147 | if (callback) { 148 | callback({ 149 | text: "Unable to process buy listing request. Invalid content provided.", 150 | content: { error: "Invalid buy listing content" }, 151 | }); 152 | } 153 | return; 154 | } 155 | 156 | try { 157 | const walletProvider = await initWalletProvider(runtime); 158 | const buyListingAction = new BuyListingAction(walletProvider); 159 | 160 | const result = await buyListingAction.buy(params.nftAddress); 161 | 162 | if (callback) { 163 | callback({ 164 | text: JSON.stringify(result, null, 2), 165 | content: result, 166 | }); 167 | } 168 | } catch (error) { 169 | elizaLogger.error("Error in BUY_LISTING handler:"); 170 | if (callback) { 171 | callback({ 172 | text: `Error in BUY_LISTING: ${error instanceof Error ? error.message : String(error)}`, 173 | content: { 174 | error: 175 | error instanceof Error 176 | ? error.message 177 | : String(error), 178 | }, 179 | }); 180 | } 181 | } 182 | return; 183 | }, 184 | template: buyListingTemplate, 185 | // eslint-disable-next-line 186 | validate: async (_runtime: IAgentRuntime) => { 187 | return true; 188 | }, 189 | examples: [ 190 | [ 191 | { 192 | user: "{{user1}}", 193 | name: "{{user1}}", 194 | content: { 195 | nftAddress: "EQNftAddressExample", 196 | action: "BUY_LISTING", 197 | }, 198 | }, 199 | { 200 | user: "assistant", 201 | name: "{{agent}}", 202 | content: { 203 | text: "Buy transaction sent successfully", 204 | }, 205 | }, 206 | ], 207 | ], 208 | }; 209 | -------------------------------------------------------------------------------- /src/actions/stake.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | composePromptFromState, 4 | type Content, 5 | type HandlerCallback, 6 | ModelType, 7 | parseKeyValueXml, 8 | type IAgentRuntime, 9 | type Memory, 10 | type State, 11 | } from "@elizaos/core"; 12 | import { 13 | IStakingProvider, 14 | StakingProvider, 15 | initStakingProvider, 16 | } from "../providers/staking"; 17 | import { initWalletProvider } from "../providers/wallet"; 18 | 19 | export interface StakeContent extends Content { 20 | poolId: string; 21 | amount: string | number; 22 | } 23 | 24 | function isStakeContent(content: Content): content is StakeContent { 25 | return ( 26 | typeof content.poolId === "string" && 27 | (typeof content.amount === "string" || 28 | typeof content.amount === "number") 29 | ); 30 | } 31 | 32 | const stakeTemplate = `Extract the staking details from the recent messages. 33 | 34 | Given the recent messages, extract the following information for staking TON: 35 | - Pool identifier (poolId) 36 | - Amount to stake 37 | 38 | Respond with the values in the following XML format: 39 | 40 | extracted pool id 41 | extracted amount 42 | 43 | 44 | {{recentMessages}} 45 | 46 | Extract the staking details and respond with values in the XML format above.`; 47 | 48 | /** 49 | * Modified StakeAction class that uses the nativeStakingProvider which 50 | * internally leverages the current wallet provider to construct and send 51 | * on-chain transactions. 52 | */ 53 | export class StakeAction { 54 | constructor(private stakingProvider: IStakingProvider) {} 55 | 56 | async stake(params: StakeContent): Promise { 57 | elizaLogger.log( 58 | `Staking: ${params.amount} TON in pool (${params.poolId}) using wallet provider` 59 | ); 60 | try { 61 | return await this.stakingProvider.stake( 62 | params.poolId, 63 | Number(params.amount) 64 | ); 65 | } catch (error) { 66 | throw new Error( 67 | `Staking failed: ${error instanceof Error ? error.message : String(error)}` 68 | ); 69 | } 70 | } 71 | } 72 | 73 | const buildStakeDetails = async ( 74 | runtime: IAgentRuntime, 75 | message: Memory, 76 | state: State 77 | ): Promise => { 78 | // Initialize or update state 79 | if (!state) { 80 | state = (await runtime.composeState(message)) as State; 81 | } 82 | 83 | // Compose prompt from state 84 | const prompt = composePromptFromState({ 85 | state, 86 | template: stakeTemplate, 87 | }); 88 | 89 | // Generate response using the small model 90 | const response = await runtime.useModel(ModelType.TEXT_SMALL, { 91 | prompt, 92 | }); 93 | 94 | // Parse the XML response 95 | const parsedResponse = parseKeyValueXml(response); 96 | 97 | const stakeContent: StakeContent = { 98 | poolId: parsedResponse?.poolId || "", 99 | amount: parsedResponse?.amount || "0", 100 | text: "", // Required by Content interface 101 | }; 102 | 103 | return stakeContent; 104 | }; 105 | 106 | export default { 107 | name: "DEPOSIT_TON", 108 | similes: ["STAKE_TOKENS", "DEPOSIT_TON", "DEPOSIT_TOKEN"], 109 | description: "Deposit TON tokens in a specified pool.", 110 | handler: async ( 111 | runtime: IAgentRuntime, 112 | message: Memory, 113 | state?: State, 114 | options?: any, 115 | callback?: HandlerCallback 116 | ): Promise => { 117 | elizaLogger.log("Starting DEPOSIT_TON handler..."); 118 | const stakeDetails = await buildStakeDetails( 119 | runtime, 120 | message, 121 | state || (await runtime.composeState(message)) 122 | ); 123 | 124 | if (!isStakeContent(stakeDetails)) { 125 | elizaLogger.error("Invalid content for DEPOSIT_TON action."); 126 | if (callback) { 127 | callback({ 128 | text: "Invalid staking details provided.", 129 | content: { error: "Invalid staking content" }, 130 | }); 131 | } 132 | return; 133 | } 134 | 135 | try { 136 | const walletProvider = await initWalletProvider(runtime); 137 | const stakingProvider = await initStakingProvider(runtime); 138 | // Instantiate StakeAction with the native staking provider. 139 | const action = new StakeAction(stakingProvider); 140 | const txHash = await action.stake(stakeDetails); 141 | 142 | if (callback) { 143 | callback({ 144 | text: `Successfully staked ${stakeDetails.amount} TON in pool ${stakeDetails.poolId}. Transaction: ${txHash}`, 145 | content: { 146 | success: true, 147 | hash: txHash, 148 | amount: stakeDetails.amount, 149 | poolId: stakeDetails.poolId, 150 | }, 151 | }); 152 | } 153 | return; 154 | } catch (error) { 155 | elizaLogger.error("Error during staking:"); 156 | if (callback) { 157 | callback({ 158 | text: `Error staking TON: ${error instanceof Error ? error.message : String(error)}`, 159 | content: { 160 | error: 161 | error instanceof Error 162 | ? error.message 163 | : String(error), 164 | }, 165 | }); 166 | } 167 | return; 168 | } 169 | }, 170 | template: stakeTemplate, 171 | validate: async (runtime: IAgentRuntime) => { 172 | elizaLogger.info("VALIDATING TON STAKING ACTION"); 173 | return true; 174 | }, 175 | examples: [ 176 | [ 177 | { 178 | user: "{{user1}}", 179 | name: "{{user1}}", 180 | content: { 181 | text: "Deposit 1.5 TON in pool pool123", 182 | action: "DEPOSIT_TON", 183 | }, 184 | }, 185 | { 186 | user: "assistant", 187 | name: "{{agent}}", 188 | content: { 189 | text: "I'll deposit 1.5 TON now...", 190 | action: "DEPOSIT_TON", 191 | }, 192 | }, 193 | { 194 | user: "assistant", 195 | name: "{{agent}}", 196 | content: { 197 | text: "Successfully deposited 1.5 TON in pool pool123, Transaction: abcd1234efgh5678", 198 | }, 199 | }, 200 | ], 201 | ], 202 | }; 203 | -------------------------------------------------------------------------------- /src/actions/loadWallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | type IAgentRuntime, 4 | type Memory, 5 | type State, 6 | type HandlerCallback, 7 | Content, 8 | composePromptFromState, 9 | parseKeyValueXml, 10 | ModelType, 11 | } from "@elizaos/core"; 12 | import { WalletProvider } from "../providers/wallet"; 13 | 14 | export interface RecoverWalletContent extends Content { 15 | password: string; 16 | walletAddress: string; 17 | } 18 | 19 | function isRecoverWalletContent( 20 | content: Content 21 | ): content is RecoverWalletContent { 22 | return ( 23 | typeof content.password === "string" && 24 | typeof content.walletAddress === "string" 25 | ); 26 | } 27 | 28 | // Define a template to guide object building (similar to the mint NFT example) 29 | const recoverWalletTemplate = `Extract the password and wallet address for wallet recovery from the recent messages. 30 | 31 | Respond with the values in the following XML format: 32 | 33 | extracted password here 34 | extracted wallet address here 35 | 36 | 37 | {{recentMessages}} 38 | 39 | Extract the password and wallet address for wallet recovery and respond with values in the XML format above.`; 40 | 41 | /** 42 | * Builds and validates a password object using the provided runtime, message, and state. 43 | * This function mimics the object building approach used in the mint NFT action. 44 | */ 45 | export async function buildRecoverWalletDetails( 46 | runtime: IAgentRuntime, 47 | message: Memory, 48 | state?: State 49 | ): Promise { 50 | // Compose the current state (or create one based on the message) 51 | const currentState = state || (await runtime.composeState(message)); 52 | 53 | // Compose a prompt from state and template 54 | const prompt = composePromptFromState({ 55 | state: currentState, 56 | template: recoverWalletTemplate, 57 | }); 58 | 59 | // Generate response using the small model 60 | const response = await runtime.useModel(ModelType.TEXT_SMALL, { 61 | prompt, 62 | }); 63 | 64 | // Parse the XML response 65 | const parsedResponse = parseKeyValueXml(response); 66 | 67 | const recoverWalletContent: RecoverWalletContent = { 68 | password: parsedResponse?.password || "", 69 | walletAddress: parsedResponse?.walletAddress || "", 70 | text: "", // Required by Content interface 71 | }; 72 | 73 | return recoverWalletContent; 74 | } 75 | 76 | export default { 77 | name: "RECOVER_TON_WALLET", 78 | similes: ["IMPORT_TON_WALLET", "RECOVER_WALLET"], 79 | description: 80 | "Loads an existing TON wallet from an encrypted backup file using the provided password.", 81 | handler: async ( 82 | runtime: IAgentRuntime, 83 | message: Memory, 84 | state?: State, 85 | _options?: Record, 86 | callback?: HandlerCallback 87 | ): Promise => { 88 | elizaLogger.log("Starting RECOVER_TON_WALLET action..."); 89 | 90 | const recoverWalletContent = await buildRecoverWalletDetails( 91 | runtime, 92 | message, 93 | state 94 | ); 95 | 96 | if (!isRecoverWalletContent(recoverWalletContent)) { 97 | if (callback) { 98 | callback({ 99 | text: "Unable to process load wallet request. No password or address provided.", 100 | content: { 101 | error: "Invalid load wallet. No password or address provided.", 102 | }, 103 | }); 104 | } 105 | return; 106 | } 107 | 108 | try { 109 | elizaLogger.debug("recoverWalletContent"); 110 | // Get the export password from settings. 111 | const password = recoverWalletContent.password; 112 | if (!password) { 113 | if (callback) { 114 | callback({ 115 | text: "Unable to process load wallet request. No password provided.", 116 | content: { 117 | error: "Invalid load wallet. No password provided.", 118 | }, 119 | }); 120 | return; 121 | } 122 | } 123 | // Get the backup file path. You can pass the filePath via message content or via settings. 124 | const walletAddress = recoverWalletContent.walletAddress; 125 | if (!walletAddress) { 126 | if (callback) { 127 | callback({ 128 | text: "Unable to process load wallet request. No wallet address provided.", 129 | content: { 130 | error: "Invalid load wallet. No wallet address provided.", 131 | }, 132 | }); 133 | return; 134 | } 135 | } 136 | 137 | const walletProvider = await WalletProvider.importWalletFromFile( 138 | runtime, 139 | walletAddress, 140 | password 141 | ); 142 | 143 | const result = { 144 | status: "success", 145 | walletAddress, 146 | message: ` 147 | Wallet recovered successfully. 148 | Your Decrypted wallet is: ${JSON.stringify(walletProvider.keypair)}. 149 | Please store it securely.`, 150 | }; 151 | 152 | if (callback) { 153 | callback({ 154 | text: `Wallet recovered successfully.\n\n Your Decrypted wallet is: ${JSON.stringify(walletProvider.keypair)}.\n\n Please store it securely.`, 155 | content: result, 156 | }); 157 | } 158 | 159 | return; 160 | } catch (error: any) { 161 | elizaLogger.error("Error recovering wallet:", error); 162 | if (callback) { 163 | callback({ 164 | text: `Error recovering wallet: ${error.message}`, 165 | content: { error: error.message }, 166 | }); 167 | } 168 | return; 169 | } 170 | }, 171 | validate: async (_runtime: IAgentRuntime) => true, 172 | examples: [ 173 | [ 174 | { 175 | user: "{{user1}}", 176 | name: "{{user1}}", 177 | content: { 178 | text: "Please recover my TON wallet. My decryption password is my_password and my wallet address is EQAXxxxxxxxxxxxxxxxxxxxxxx.", 179 | action: "RECOVER_TON_WALLET", 180 | }, 181 | }, 182 | { 183 | user: "assistant", 184 | name: "{{agent}}", 185 | content: { 186 | text: "Wallet recovered successfully. Your Decrypted wallet is: ${JSON.stringify(walletProvider.keypair)}. Please store it securely.", 187 | }, 188 | }, 189 | ], 190 | ], 191 | }; 192 | -------------------------------------------------------------------------------- /src/actions/cancelListing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | composePromptFromState, 4 | parseKeyValueXml, 5 | ModelType, 6 | type IAgentRuntime, 7 | type Memory, 8 | type State, 9 | type HandlerCallback, 10 | Content, 11 | } from "@elizaos/core"; 12 | import { Address, internal, SendMode, toNano, Cell, beginCell } from "@ton/ton"; 13 | import { z } from "zod"; 14 | import { initWalletProvider, WalletProvider } from "../providers/wallet"; 15 | import { waitSeqnoContract } from "../utils/util"; 16 | import { getListingData } from "../services/nft-marketplace/listingData"; 17 | import { cancelListing } from "../services/nft-marketplace/listingTransactions"; 18 | 19 | /** 20 | * Schema for cancel listing input. 21 | * Only requires: 22 | * - nftAddress: The NFT contract address. 23 | */ 24 | const cancelListingSchema = z 25 | .object({ 26 | nftAddress: z.string().nonempty("NFT address is required"), 27 | }) 28 | .refine((data) => data.nftAddress, { 29 | message: "NFT address is required", 30 | path: ["nftAddress"], 31 | }); 32 | 33 | export interface CancelListingContent extends Content { 34 | nftAddress: string; 35 | } 36 | 37 | function isCancelListingContent( 38 | content: Content 39 | ): content is CancelListingContent { 40 | return typeof content.nftAddress === "string"; 41 | } 42 | 43 | const cancelListingTemplate = ` 44 | Analyze the conversation and extract the following information about the requested NFT listing cancellation: 45 | - NFT address to cancel the listing for 46 | 47 | Format the response as key-value pairs in XML format. 48 | 49 | 50 | {{recentMessages}} 51 | 52 | 53 | Extract the NFT listing cancellation details from the conversation above. 54 | Respond with the following format: 55 | 56 | nft_address_to_cancel 57 | `; 58 | 59 | /** 60 | * Helper function to build cancel listing parameters. 61 | */ 62 | const buildCancelListingData = async ( 63 | runtime: IAgentRuntime, 64 | message: Memory, 65 | state?: State 66 | ): Promise => { 67 | // Initialize or update state 68 | let currentState = state; 69 | if (!currentState) { 70 | currentState = (await runtime.composeState(message)) as State; 71 | } else { 72 | currentState = await runtime.composeState(message, ["RECENT_MESSAGES"]); 73 | } 74 | 75 | const prompt = composePromptFromState({ 76 | state: currentState, 77 | template: cancelListingTemplate, 78 | }); 79 | 80 | const result = await runtime.useModel(ModelType.TEXT_SMALL, { 81 | prompt, 82 | }); 83 | 84 | const parsedContent = parseKeyValueXml( 85 | typeof result === "string" ? result : (result as any).value || "" 86 | ); 87 | 88 | const cancelContent: CancelListingContent = { 89 | nftAddress: parsedContent?.nftAddress || "", 90 | text: "", // Required by Content interface 91 | }; 92 | 93 | // Validate with schema 94 | const validatedContent = cancelListingSchema.parse(cancelContent); 95 | return validatedContent as CancelListingContent; 96 | }; 97 | 98 | /** 99 | * CancelListingAction encapsulates the logic to cancel an NFT listing. 100 | */ 101 | export class CancelListingAction { 102 | private walletProvider: WalletProvider; 103 | constructor(walletProvider: WalletProvider) { 104 | this.walletProvider = walletProvider; 105 | } 106 | 107 | /** 108 | * Cancels an NFT listing 109 | */ 110 | async cancel(nftAddress: string): Promise { 111 | try { 112 | elizaLogger.log( 113 | `Starting cancellation of NFT listing: ${nftAddress}` 114 | ); 115 | 116 | const receipt = await cancelListing( 117 | this.walletProvider, 118 | nftAddress 119 | ); 120 | return receipt; 121 | } catch (error) { 122 | elizaLogger.error( 123 | `Error cancelling NFT listing ${nftAddress}: ${error}` 124 | ); 125 | throw new Error( 126 | `Failed to cancel NFT listing: ${error instanceof Error ? error.message : String(error)}` 127 | ); 128 | } 129 | } 130 | } 131 | 132 | export default { 133 | name: "CANCEL_LISTING", 134 | similes: ["NFT_CANCEL", "CANCEL_NFT", "CANCEL_SALE"], 135 | description: 136 | "Cancels a listed NFT by sending a cancel operation to the listing contract.", 137 | handler: async ( 138 | runtime: IAgentRuntime, 139 | message: Memory, 140 | state?: State, 141 | _options?: any, 142 | callback?: HandlerCallback 143 | ): Promise => { 144 | elizaLogger.log("Starting CANCEL_LISTING handler..."); 145 | const params = await buildCancelListingData( 146 | runtime, 147 | message, 148 | state || (await runtime.composeState(message)) 149 | ); 150 | 151 | if (!isCancelListingContent(params)) { 152 | if (callback) { 153 | callback({ 154 | text: "Unable to process cancel listing request. Invalid content provided.", 155 | content: { error: "Invalid cancel listing content" }, 156 | }); 157 | } 158 | return; 159 | } 160 | 161 | try { 162 | const walletProvider = await initWalletProvider(runtime); 163 | const cancelListingAction = new CancelListingAction(walletProvider); 164 | 165 | const result = await cancelListingAction.cancel(params.nftAddress); 166 | 167 | if (callback) { 168 | callback({ 169 | text: JSON.stringify(result, null, 2), 170 | content: result, 171 | }); 172 | } 173 | } catch (error) { 174 | elizaLogger.error("Error in CANCEL_LISTING handler:"); 175 | if (callback) { 176 | callback({ 177 | text: `Error in CANCEL_LISTING: ${error instanceof Error ? error.message : String(error)}`, 178 | content: { 179 | error: 180 | error instanceof Error 181 | ? error.message 182 | : String(error), 183 | }, 184 | }); 185 | } 186 | } 187 | return; 188 | }, 189 | template: cancelListingTemplate, 190 | // eslint-disable-next-line 191 | validate: async (_runtime: IAgentRuntime) => { 192 | return true; 193 | }, 194 | examples: [ 195 | [ 196 | { 197 | user: "{{user1}}", 198 | name: "{{user1}}", 199 | content: { 200 | nftAddress: "EQNftAddressExample", 201 | action: "CANCEL_LISTING", 202 | }, 203 | }, 204 | { 205 | user: "assistant", 206 | name: "{{agent}}", 207 | content: { 208 | text: "Cancel listing transaction sent successfully", 209 | }, 210 | }, 211 | ], 212 | ], 213 | }; 214 | -------------------------------------------------------------------------------- /src/actions/unstake.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | composePromptFromState, 4 | type Content, 5 | type HandlerCallback, 6 | ModelType, 7 | parseKeyValueXml, 8 | type IAgentRuntime, 9 | type Memory, 10 | type State, 11 | } from "@elizaos/core"; 12 | import { initStakingProvider, IStakingProvider } from "../providers/staking"; 13 | 14 | export interface UnstakeContent extends Content { 15 | poolId: string; 16 | amount: string | number; 17 | } 18 | 19 | function isUnstakeContent(content: Content): content is UnstakeContent { 20 | return ( 21 | typeof content.poolId === "string" && 22 | (typeof content.amount === "string" || 23 | typeof content.amount === "number") 24 | ); 25 | } 26 | 27 | const unstakeTemplate = `Extract the unstaking details from the recent messages. 28 | 29 | Given the recent messages, extract the following information for unstaking TON: 30 | - Pool identifier (poolId) 31 | - Amount to unstake 32 | 33 | Respond with the values in the following XML format: 34 | 35 | extracted pool id 36 | extracted amount 37 | 38 | 39 | {{recentMessages}} 40 | 41 | Extract the unstaking details and respond with values in the XML format above.`; 42 | 43 | export class UnstakeAction { 44 | constructor(private stakingProvider: IStakingProvider) {} 45 | 46 | async unstake(params: UnstakeContent): Promise { 47 | elizaLogger.log( 48 | `Unstaking: ${params.amount} TON from pool (${params.poolId})` 49 | ); 50 | try { 51 | // Call the staking provider's unstake method. 52 | const result = await this.stakingProvider.unstake( 53 | params.poolId, 54 | Number(params.amount) 55 | ); 56 | return result ?? ""; 57 | } catch (error) { 58 | const errorMessage = 59 | error instanceof Error ? error.message : String(error); 60 | throw new Error(`Unstaking failed: ${errorMessage}`); 61 | } 62 | } 63 | } 64 | 65 | const buildUnstakeDetails = async ( 66 | runtime: IAgentRuntime, 67 | message: Memory, 68 | state: State 69 | ): Promise => { 70 | if (!state) { 71 | state = (await runtime.composeState(message)) as State; 72 | } 73 | 74 | // Compose prompt from state 75 | const prompt = composePromptFromState({ 76 | state, 77 | template: unstakeTemplate, 78 | }); 79 | 80 | // Generate response using the small model 81 | const response = await runtime.useModel(ModelType.TEXT_SMALL, { 82 | prompt, 83 | }); 84 | 85 | // Parse the XML response 86 | const responseText = 87 | typeof response === "string" ? response : (response as any).value || ""; 88 | const parsedResponse = parseKeyValueXml(responseText); 89 | 90 | const unstakeContent: UnstakeContent = { 91 | poolId: parsedResponse?.poolId || "", 92 | amount: parsedResponse?.amount || "0", 93 | text: "", // Required by Content interface 94 | }; 95 | 96 | return unstakeContent; 97 | }; 98 | 99 | export default { 100 | name: "WITHDRAW_TON", 101 | similes: ["UNSTAKE_TOKENS", "WITHDRAW_TON", "TON_UNSTAKE"], 102 | description: "Withdraw TON tokens from a specified pool.", 103 | handler: async ( 104 | runtime: IAgentRuntime, 105 | message: Memory, 106 | state?: State, 107 | options?: any, 108 | callback?: HandlerCallback 109 | ): Promise => { 110 | elizaLogger.log("Starting WITHDRAW_TON handler..."); 111 | const unstakeDetails = await buildUnstakeDetails( 112 | runtime, 113 | message, 114 | state || (await runtime.composeState(message)) 115 | ); 116 | 117 | if (!isUnstakeContent(unstakeDetails)) { 118 | elizaLogger.error("Invalid content for WITHDRAW_TON action."); 119 | if (callback) { 120 | callback({ 121 | text: "Invalid unstake details provided.", 122 | content: { error: "Invalid unstake content" }, 123 | }); 124 | } 125 | return; 126 | } 127 | 128 | try { 129 | const stakingProvider = await initStakingProvider(runtime); 130 | const action = new UnstakeAction(stakingProvider); 131 | const txHash = await action.unstake(unstakeDetails); 132 | 133 | if (callback) { 134 | callback({ 135 | text: `Successfully unstaked ${unstakeDetails.amount} TON from pool ${unstakeDetails.poolId}. Transaction: ${txHash}`, 136 | content: { 137 | success: true, 138 | hash: txHash, 139 | amount: unstakeDetails.amount, 140 | poolId: unstakeDetails.poolId, 141 | }, 142 | }); 143 | } 144 | return; 145 | } catch (error) { 146 | elizaLogger.error("Error during unstaking:"); 147 | const errorMessage = 148 | error instanceof Error ? error.message : String(error); 149 | if (callback) { 150 | callback({ 151 | text: `Error unstaking TON: ${errorMessage}`, 152 | content: { error: errorMessage }, 153 | }); 154 | } 155 | return; 156 | } 157 | }, 158 | template: unstakeTemplate, 159 | validate: async (runtime: IAgentRuntime) => true, 160 | examples: [ 161 | [ 162 | { 163 | user: "{{user1}}", 164 | name: "{{user1}}", 165 | content: { 166 | text: "Withdraw 1 TON from pool pool123", 167 | action: "WITHDRAW_TON", 168 | }, 169 | }, 170 | { 171 | user: "assistant", 172 | name: "{{agent}}", 173 | content: { 174 | text: "I'll unstake 1 TON now...", 175 | action: "WITHDRAW_TON", 176 | }, 177 | }, 178 | { 179 | user: "assistant", 180 | name: "{{agent}}", 181 | content: { 182 | text: "Successfully unstaked 1 TON from pool pool123, Transaction: efgh5678abcd1234", 183 | }, 184 | }, 185 | ], 186 | [ 187 | { 188 | user: "{{user1}}", 189 | name: "{{user1}}", 190 | content: { 191 | text: "withdraw 12 TON from pool eqw237595asd432", 192 | action: "WITHDRAW_TON", 193 | }, 194 | }, 195 | { 196 | user: "assistant", 197 | name: "{{agent}}", 198 | content: { 199 | text: "Withdrawing 12 TON right now...", 200 | action: "WITHDRAW_TON", 201 | }, 202 | }, 203 | { 204 | user: "assistant", 205 | name: "{{agent}}", 206 | content: { 207 | text: "Successfully unstaked 12 TON from pool eqw237595asd432, Transaction: efgesdrf234h5678abcd1234", 208 | }, 209 | }, 210 | ], 211 | ], 212 | }; 213 | -------------------------------------------------------------------------------- /src/services/staking/strategies/tonWhales.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Dictionary, 5 | fromNano, 6 | MessageRelaxed, 7 | Slice, 8 | toNano, 9 | TonClient, 10 | TupleReader, 11 | } from "@ton/ton"; 12 | import { StakingPlatform } from "../interfaces/stakingPlatform.ts"; 13 | import { internal } from "@ton/ton"; 14 | import { WalletProvider } from "../../../providers/wallet.ts"; 15 | import { 16 | PoolInfo, 17 | PoolMemberData, 18 | PoolMemberList, 19 | } from "../interfaces/pool.ts"; 20 | 21 | function generateQueryId() { 22 | // Generate a query ID that's unique for this transaction 23 | return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER); 24 | } 25 | 26 | function parseMembersRaw(stack: any): PoolMemberList { 27 | const cell = stack.items[0].cell; 28 | 29 | const dict = Dictionary.loadDirect( 30 | Dictionary.Keys.BigInt(256), 31 | { 32 | serialize: (src: any, builder: any) => {}, 33 | parse: (slice: Slice) => { 34 | try { 35 | const profitPerCoin = slice.loadUintBig(128); 36 | const balance = slice.loadCoins(); 37 | const pendingWithdraw = slice.loadCoins(); 38 | const pendingWithdrawAll = slice.loadUintBig(1) === 1n; 39 | const pendingDeposit = slice.loadCoins(); 40 | const memberWithdraw = slice.loadCoins(); 41 | 42 | return { 43 | profit_per_coin: profitPerCoin, 44 | balance: balance, 45 | pending_withdraw: pendingWithdraw, 46 | pending_withdraw_all: pendingWithdrawAll, 47 | pending_deposit: pendingDeposit, 48 | member_withdraw: memberWithdraw, 49 | }; 50 | } catch (e) { 51 | console.error("Parse error:", e); 52 | return { 53 | error: e instanceof Error ? e.message : String(e), 54 | sliceData: slice.toString(), 55 | }; 56 | } 57 | }, 58 | }, 59 | cell 60 | ); 61 | 62 | const members: PoolMemberList = []; 63 | 64 | for (const [key, value] of dict) { 65 | // Convert key to proper hex format 66 | let bigIntKey: bigint; 67 | if (typeof key === "bigint") { 68 | bigIntKey = key; 69 | } else if (typeof key === "string") { 70 | const numStr = (key as string).startsWith("b:") 71 | ? (key as string).substring(2) 72 | : key; 73 | bigIntKey = BigInt(numStr); 74 | } else { 75 | bigIntKey = BigInt((key as any).toString()); 76 | } 77 | 78 | if (bigIntKey < 0n) { 79 | bigIntKey = (1n << 256n) + bigIntKey; 80 | } 81 | 82 | const rawAddress = bigIntKey 83 | .toString(16) 84 | .replace("0x", "") 85 | .padStart(64, "0"); 86 | const address = new Address(0, Buffer.from(rawAddress, "hex")); 87 | 88 | members.push({ 89 | address, 90 | ...value, 91 | }); 92 | } 93 | 94 | return members; 95 | } 96 | 97 | export class TonWhalesStrategy implements StakingPlatform { 98 | constructor( 99 | readonly tonClient: TonClient, 100 | readonly walletProvider: WalletProvider 101 | ) {} 102 | 103 | async getPendingWithdrawal( 104 | walletAddress: Address, 105 | poolAddress: Address 106 | ): Promise { 107 | const memberData = await this.getMemberData(walletAddress, poolAddress); 108 | 109 | return memberData?.pending_withdraw ?? BigInt("0"); 110 | } 111 | 112 | async getStakedTon( 113 | walletAddress: Address, 114 | poolAddress: Address 115 | ): Promise { 116 | const memberData = await this.getMemberData(walletAddress, poolAddress); 117 | 118 | if (memberData?.pending_withdraw) 119 | return memberData.balance - memberData.pending_withdraw; 120 | 121 | return memberData?.balance ?? BigInt("0"); 122 | } 123 | 124 | async getPoolInfo(poolAddress: Address): Promise { 125 | try { 126 | const poolParams = ( 127 | await this.tonClient.runMethod(poolAddress, "get_params") 128 | ).stack; 129 | 130 | const poolStatus = ( 131 | await this.tonClient.runMethod(poolAddress, "get_pool_status") 132 | ).stack; 133 | 134 | // Parse the stack result based on TonWhales contract structure 135 | return { 136 | address: poolAddress, 137 | min_stake: poolParams.skip(2).readBigNumber(), 138 | deposit_fee: poolParams.readBigNumber(), 139 | withdraw_fee: poolParams.readBigNumber(), 140 | balance: poolStatus.readBigNumber(), 141 | pending_deposits: poolStatus.skip().readBigNumber(), 142 | pending_withdraws: poolStatus.readBigNumber(), 143 | }; 144 | } catch (error) { 145 | console.error("Error fetching TonWhales pool info:", error); 146 | throw error; 147 | } 148 | } 149 | 150 | async createStakeMessage( 151 | poolAddress: Address, 152 | amount: number 153 | ): Promise { 154 | const queryId = generateQueryId(); 155 | 156 | const payload = beginCell() 157 | .storeUint(2077040623, 32) 158 | .storeUint(queryId, 64) 159 | .storeCoins(100000) // gas 160 | .endCell(); 161 | 162 | const intMessage = internal({ 163 | to: poolAddress, 164 | value: toNano(amount), 165 | bounce: true, 166 | init: null, 167 | body: payload, 168 | }); 169 | 170 | return intMessage; 171 | } 172 | 173 | async createUnstakeMessage( 174 | poolAddress: Address, 175 | amount: number 176 | ): Promise { 177 | const queryId = generateQueryId(); 178 | 179 | const payload = beginCell() 180 | .storeUint(3665837821, 32) 181 | .storeUint(queryId, 64) 182 | .storeCoins(100000) // gas 183 | .storeCoins(toNano(amount)) 184 | .endCell(); 185 | 186 | const intMessage = internal({ 187 | to: poolAddress, 188 | value: 200000000n, //toNano(unstakeAmount), 189 | bounce: true, 190 | init: null, 191 | body: payload, // Adjust this message if your staking contract requires a different format. 192 | }); 193 | 194 | return intMessage; 195 | } 196 | 197 | private async getMemberData( 198 | address: Address, 199 | poolAddress: Address 200 | ): Promise { 201 | const result = await this.tonClient.runMethod( 202 | poolAddress, 203 | "get_members_raw" 204 | ); 205 | 206 | const memberData = await parseMembersRaw(result.stack); 207 | 208 | const member = memberData.find((member) => { 209 | try { 210 | return member.address.equals(address); 211 | } catch (e) { 212 | console.error(e, member.address, address); 213 | return false; 214 | } 215 | }); 216 | 217 | return member || null; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/providers/ston.ts: -------------------------------------------------------------------------------- 1 | import type { IAgentRuntime } from "@elizaos/core"; 2 | 3 | import { AssetTag, StonApiClient } from "@ston-fi/api"; 4 | import { TonClient } from "@ton/ton"; 5 | 6 | import { DEX, pTON } from "@ston-fi/sdk"; 7 | import { testnetAssets } from "../utils/testnetStonAssets"; 8 | 9 | const PROVIDER_CONFIG = { 10 | SWAP_WAITING_TIME: 1000, // [ms] 11 | SWAP_WAITING_STEPS: 180, // total waiting time = SWAP_WAITING_TIME * SWAP_WAITING_STEPS 12 | TX_WAITING_TIME: 1000, // [ms] 13 | TX_WAITING_STEPS: 60, // total waiting time = TX_WAITING_TIME * TX_WAITING_STEPS 14 | mainnet: { 15 | ROUTER_VERSION: "v1", 16 | ROUTER_ADDRESS: "", 17 | PTON_VERSION: "v1", 18 | PTON_ADDRESS: "", 19 | }, 20 | testnet: { 21 | ROUTER_VERSION: "v2_1", 22 | ROUTER_ADDRESS: "kQALh-JBBIKK7gr0o4AVf9JZnEsFndqO0qTCyT-D-yBsWk0v", 23 | PTON_VERSION: "v2_1", 24 | PTON_ADDRESS: "kQACS30DNoUQ7NfApPvzh7eBmSZ9L4ygJ-lkNWtba8TQT-Px", 25 | }, 26 | }; 27 | 28 | export interface StonAsset { 29 | balance?: string | undefined; 30 | blacklisted: boolean; 31 | community: boolean; 32 | contractAddress: string; 33 | decimals: number; 34 | defaultSymbol: boolean; 35 | deprecated: boolean; 36 | dexPriceUsd?: string | undefined; 37 | displayName?: string | undefined; 38 | imageUrl?: string | undefined; 39 | kind: "Ton" | "Wton" | "Jetton"; 40 | priority: number; 41 | symbol: string; 42 | thirdPartyPriceUsd?: string | undefined; 43 | walletAddress?: string | undefined; 44 | popularityIndex?: number | undefined; 45 | tags: AssetTag[]; 46 | customPayloadApiUri?: string | undefined; 47 | extensions?: string[] | undefined; 48 | } 49 | 50 | export class StonProvider { 51 | public client: StonApiClient; 52 | public NETWORK: "mainnet" | "testnet"; 53 | public SWAP_WAITING_TIME: number; 54 | public SWAP_WAITING_STEPS: number; 55 | public TX_WAITING_TIME: number; 56 | public TX_WAITING_STEPS: number; 57 | public ROUTER_VERSION: string; 58 | public ROUTER_ADDRESS: string; 59 | public PTON_VERSION: string; 60 | public PTON_ADDRESS: string; 61 | 62 | constructor(runtime: IAgentRuntime) { 63 | this.client = runtime.getSetting("STON_API_BASE_URL") 64 | ? new StonApiClient({ 65 | baseURL: runtime.getSetting("STON_API_BASE_URL"), 66 | }) 67 | : new StonApiClient(); 68 | // if not given, it uses mainnet 69 | this.NETWORK = runtime.getSetting("TON_RPC_URL")?.includes("testnet") 70 | ? "testnet" 71 | : "mainnet"; 72 | this.SWAP_WAITING_TIME = Number( 73 | runtime.getSetting("SWAP_WAITING_TIME") ?? 74 | PROVIDER_CONFIG.SWAP_WAITING_TIME 75 | ); 76 | this.SWAP_WAITING_STEPS = Number( 77 | runtime.getSetting("SWAP_WAITING_STEPS") ?? 78 | PROVIDER_CONFIG.SWAP_WAITING_STEPS 79 | ); 80 | this.TX_WAITING_TIME = Number( 81 | runtime.getSetting("TX_WAITING_TIME") ?? 82 | PROVIDER_CONFIG.TX_WAITING_TIME 83 | ); 84 | this.TX_WAITING_STEPS = Number( 85 | runtime.getSetting("TX_WAITING_STEPS") ?? 86 | PROVIDER_CONFIG.TX_WAITING_STEPS 87 | ); 88 | this.ROUTER_VERSION = 89 | runtime.getSetting("ROUTER_VERSION") ?? 90 | PROVIDER_CONFIG[this.NETWORK].ROUTER_VERSION; 91 | this.ROUTER_ADDRESS = 92 | runtime.getSetting("ROUTER_ADDRESS") ?? 93 | PROVIDER_CONFIG[this.NETWORK].ROUTER_ADDRESS; 94 | this.PTON_VERSION = 95 | runtime.getSetting("PTON_VERSION") ?? 96 | PROVIDER_CONFIG[this.NETWORK].PTON_VERSION; 97 | this.PTON_ADDRESS = 98 | runtime.getSetting("PTON_ADDRESS") ?? 99 | PROVIDER_CONFIG[this.NETWORK].PTON_ADDRESS; 100 | } 101 | 102 | async getAsset( 103 | symbol: string, 104 | condition: string = `${AssetTag.DefaultSymbol}` 105 | ) { 106 | if (this.NETWORK === "mainnet") { 107 | return await this.getAssetMainnet(symbol, condition); 108 | } else { 109 | return await this.getAssetTestnet(symbol); 110 | } 111 | } 112 | async getAssets( 113 | from: string, 114 | to: string, 115 | condition: string = `${AssetTag.DefaultSymbol}` 116 | ) { 117 | if (this.NETWORK === "mainnet") { 118 | return await this.getAssetsMainnet(from, to, condition); 119 | } else { 120 | return await this.getAssetsTestnet(from, to); 121 | } 122 | } 123 | 124 | async getAssetMainnet( 125 | symbol: string, 126 | condition: string = `${AssetTag.DefaultSymbol}` 127 | ) { 128 | // search assets across of all DEX assets based on search string and query condition 129 | const matchedInAssets = await this.client.searchAssets({ 130 | searchString: symbol, 131 | condition, 132 | limit: 1, 133 | }); 134 | if (matchedInAssets.length === 0) { 135 | throw new Error(`Asset ${symbol} not supported`); 136 | } 137 | 138 | const asset = await this.client.getAsset( 139 | matchedInAssets[0].contractAddress 140 | ); 141 | 142 | if (asset.deprecated) { 143 | throw new Error(`Asset ${asset.symbol} is deprecated`); 144 | } 145 | 146 | if (asset.blacklisted) { 147 | throw new Error(`Asset ${asset.symbol} is blacklisted`); 148 | } 149 | 150 | return asset; 151 | } 152 | 153 | async getAssetsMainnet( 154 | from: string, 155 | to: string, 156 | condition: string = `${AssetTag.DefaultSymbol}` 157 | ) { 158 | const inAsset = await this.getAssetMainnet(from, condition); 159 | const outAsset = await this.getAssetMainnet(to, condition); 160 | 161 | const pairs = await this.client.getSwapPairs(); 162 | if ( 163 | !pairs.find( 164 | (pair) => 165 | pair.includes(inAsset.contractAddress) && 166 | pair.includes(outAsset.contractAddress) 167 | ) 168 | ) { 169 | throw new Error( 170 | `Swap pair ${inAsset.symbol} to ${outAsset.symbol} is not supported` 171 | ); 172 | } 173 | 174 | return [inAsset, outAsset]; 175 | } 176 | 177 | async getAssetTestnet(symbol: string) { 178 | const asset = testnetAssets.find((asset) => asset.symbol === symbol); 179 | 180 | if (!asset) { 181 | throw new Error(`Asset ${symbol} not supported`); 182 | } 183 | 184 | return asset as StonAsset; 185 | } 186 | 187 | async getAssetsTestnet(from: string, to: string) { 188 | const inAsset = await this.getAssetTestnet(from); 189 | const outAsset = await this.getAssetTestnet(to); 190 | 191 | return [inAsset, outAsset]; 192 | } 193 | 194 | getRouterAndProxy(client: TonClient) { 195 | let router, proxyTON; 196 | if (this.ROUTER_VERSION === "v1") { 197 | router = client.open( 198 | new (DEX as any)[this.ROUTER_VERSION].Router() 199 | ); 200 | } else { 201 | router = client.open( 202 | (DEX as any)[this.ROUTER_VERSION].Router.create( 203 | this.ROUTER_ADDRESS 204 | ) 205 | ); 206 | } 207 | if (this.PTON_VERSION === "v1") { 208 | proxyTON = new (pTON as any)[this.PTON_VERSION](); 209 | } else { 210 | proxyTON = (pTON as any)[this.PTON_VERSION].create( 211 | this.PTON_ADDRESS 212 | ); 213 | } 214 | return [router, proxyTON]; 215 | } 216 | } 217 | 218 | export async function initStonProvider( 219 | runtime: IAgentRuntime 220 | ): Promise { 221 | return new StonProvider(runtime); 222 | } 223 | -------------------------------------------------------------------------------- /src/actions/createWallet.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | type IAgentRuntime, 4 | type Memory, 5 | type State, 6 | type HandlerCallback, 7 | ModelType, 8 | parseKeyValueXml, 9 | Content, 10 | composePromptFromState, 11 | } from "@elizaos/core"; 12 | import { WalletProvider, initWalletProvider } from "../providers/wallet"; 13 | import { cacheManager } from "src/cache"; 14 | 15 | interface RuntimeContext { 16 | cacheManager?: CacheManager; 17 | } 18 | 19 | interface CacheManager { 20 | get?: (key: string) => any; 21 | set?: (key: string, value: any) => void; 22 | } 23 | 24 | export interface CreateWalletContent extends Content { 25 | encryptionPassword: string; 26 | } 27 | 28 | function isCreateWalletContent( 29 | content: Content 30 | ): content is CreateWalletContent { 31 | return typeof content.encryptionPassword === "string"; 32 | } 33 | 34 | // Define a template to guide object building (similar to the mint NFT example) 35 | export const passwordTemplate = `Extract the encryption password for wallet creation from the recent messages. 36 | 37 | Respond with the value in the following XML format: 38 | 39 | extracted password here 40 | 41 | 42 | {{recentMessages}} 43 | 44 | Extract the encryption password and respond with the value in the XML format above.`; 45 | 46 | /** 47 | * Builds and validates a password object using the provided runtime, message, and state. 48 | * This function mimics the object building approach used in the mint NFT action. 49 | */ 50 | export async function buildCreateWalletDetails( 51 | runtime: IAgentRuntime, 52 | message: Memory, 53 | state?: State 54 | ): Promise { 55 | // Compose the current state (or create one based on the message) 56 | const currentState = state || (await runtime.composeState(message)); 57 | 58 | // Compose prompt from state 59 | const prompt = composePromptFromState({ 60 | state: currentState, 61 | template: passwordTemplate, 62 | }); 63 | 64 | // Generate response using the small model 65 | const response = await runtime.useModel(ModelType.TEXT_SMALL, { 66 | prompt, 67 | }); 68 | 69 | // Parse the XML response 70 | const parsedResponse = parseKeyValueXml(response); 71 | 72 | const createWalletContent: CreateWalletContent = { 73 | encryptionPassword: parsedResponse?.encryptionPassword || "", 74 | text: "", // Required by Content interface 75 | }; 76 | 77 | return createWalletContent; 78 | } 79 | 80 | export class CreateWalletAction { 81 | async createWallet( 82 | runtime: IAgentRuntime, 83 | params: { rpcUrl: string; encryptionPassword: string } 84 | ): Promise<{ walletAddress: string; mnemonic: string[] }> { 85 | 86 | const { walletProvider, mnemonic } = await WalletProvider.generateNew( 87 | params.rpcUrl, 88 | params.encryptionPassword, 89 | cacheManager 90 | ); 91 | const walletAddress = walletProvider.getAddress(); 92 | return { walletAddress, mnemonic }; 93 | } 94 | } 95 | 96 | export default { 97 | name: "CREATE_TON_WALLET", 98 | similes: ["NEW_TON_WALLET", "MAKE_NEW_TON_WALLET"], 99 | description: 100 | "Creates a new TON wallet on demand. Returns the public address and mnemonic backup (store it securely). The wallet keypair is also encrypted to a file using the provided password.", 101 | handler: async ( 102 | runtime: IAgentRuntime, 103 | message: Memory, 104 | state?: State, 105 | _options?: Record, 106 | callback?: HandlerCallback 107 | ): Promise => { 108 | elizaLogger.log("Starting CREATE_TON_WALLET action..."); 109 | 110 | // Build password details using the object building approach like in the mint NFT action. 111 | const createWalletContent = await buildCreateWalletDetails( 112 | runtime, 113 | message, 114 | state 115 | ); 116 | 117 | elizaLogger.debug("createWalletContent"); 118 | if (!isCreateWalletContent(createWalletContent)) { 119 | if (callback) { 120 | callback({ 121 | text: "Unable to process create wallet request. No password provided.", 122 | content: { 123 | error: "Invalid create wallet. No password provided.", 124 | }, 125 | }); 126 | } 127 | return; 128 | } 129 | try { 130 | // Generate a new wallet using the provided password. 131 | 132 | const rpcUrl = 133 | runtime.getSetting("TON_RPC_URL") || 134 | "https://toncenter.com/api/v2/jsonRPC"; 135 | const action = new CreateWalletAction(); 136 | 137 | const { walletAddress, mnemonic } = await action.createWallet( 138 | runtime, 139 | { 140 | rpcUrl, 141 | encryptionPassword: createWalletContent.encryptionPassword, 142 | } 143 | ); 144 | const result = { 145 | status: "success", 146 | walletAddress, 147 | mnemonic, // IMPORTANT: The mnemonic backup must be stored securely! 148 | message: 149 | "New TON wallet created. Store the mnemonic securely for recovery.", 150 | }; 151 | 152 | if (callback) { 153 | callback({ 154 | text: ` 155 | New TON wallet created! 156 | Your password was used to encrypt the wallet keypair, but never stored. 157 | Wallet Address: ${walletAddress} 158 | I've used both your password and the mnemonic to create the wallet. 159 | Please securely store your mnemonic: 160 | ${mnemonic.join(" ")}`, 161 | content: result, 162 | }); 163 | } 164 | 165 | return; 166 | } catch (error: any) { 167 | elizaLogger.error("Error creating wallet:", error); 168 | if (callback) { 169 | callback({ 170 | text: `Error creating wallet: ${error.message}`, 171 | content: { error: error.message }, 172 | }); 173 | } 174 | return; 175 | } 176 | }, 177 | validate: async (_runtime: IAgentRuntime) => true, 178 | examples: [ 179 | [ 180 | { 181 | user: "{{user1}}", 182 | name: "{{user1}}", 183 | content: { 184 | text: "Please create a new TON wallet for me.", 185 | action: "CREATE_TON_WALLET", 186 | }, 187 | }, 188 | { 189 | user: "assistant", 190 | name: "{{agent}}", 191 | content: { 192 | text: "New TON wallet created!/n Your password was used to encrypt the wallet keypair, but never stored./nWallet Address: EQAXxxxxxxxxxxxxxxxxxxxxxx./n I've used both your password and the mnemonic to create the wallet./nPlease securely store your mnemonic", 193 | }, 194 | }, 195 | ], 196 | [ 197 | { 198 | user: "{{user1}}", 199 | name: "{{user1}}", 200 | content: { 201 | text: "Please make me a new TON wallet.", 202 | action: "CREATE_TON_WALLET", 203 | }, 204 | }, 205 | { 206 | user: "assistant", 207 | name: "{{agent}}", 208 | content: { 209 | text: "New TON wallet created!/n Your password was used to encrypt the wallet keypair, but never stored./nWallet Address: EQAXxxxxxxxxxxxxxxxxxxxxxx./n I've used both your password and the mnemonic to create the wallet./nPlease securely store your mnemonic", 210 | }, 211 | }, 212 | ], 213 | ], 214 | }; 215 | -------------------------------------------------------------------------------- /src/actions/tokenPrice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | type Content, 4 | type HandlerCallback, 5 | type IAgentRuntime, 6 | type Memory, 7 | type State, 8 | Action, 9 | } from "@elizaos/core"; 10 | import { TonTokenPriceProvider } from "../providers/tokenProvider.ts"; 11 | 12 | export interface PriceContent extends Content { 13 | token: string; 14 | } 15 | 16 | interface ActionOptions { 17 | [key: string]: unknown; 18 | } 19 | 20 | export class TONPriceAction { 21 | private priceProvider: TonTokenPriceProvider; 22 | 23 | constructor(priceProvider: TonTokenPriceProvider) { 24 | this.priceProvider = priceProvider; 25 | } 26 | } 27 | 28 | const priceTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined. 29 | 30 | Example response: 31 | \`\`\`json 32 | { 33 | "token": "TON" 34 | } 35 | \`\`\` 36 | 37 | {{recentMessages}} 38 | 39 | Given the recent messages, extract the following information about the requested token price: 40 | - Token symbol or address 41 | 42 | Respond with a JSON markdown block containing only the extracted values.`; 43 | 44 | export default { 45 | name: "GET_TOKEN_PRICE_TON", 46 | similes: [ 47 | "FETCH_TOKEN_PRICE_TON", 48 | "CHECK_TOKEN_PRICE_TON", 49 | "TOKEN_PRICE_TON", 50 | ], 51 | description: 52 | "Fetches and returns token price information on TON blockchain", 53 | handler: async ( 54 | runtime: IAgentRuntime, 55 | message: Memory, 56 | state?: State, 57 | _options?: ActionOptions, 58 | callback?: HandlerCallback 59 | ): Promise => { 60 | console.log("token price action handler started"); 61 | elizaLogger.log("Starting GET_TOKEN_PRICE_TON handler..."); 62 | 63 | try { 64 | const provider = runtime.providers.find( 65 | (p) => p instanceof TonTokenPriceProvider 66 | ); 67 | if (!provider) { 68 | throw new Error("Token price provider not found"); 69 | } 70 | const priceData = await provider.get( 71 | runtime, 72 | message, 73 | state || (await runtime.composeState(message)) 74 | ); 75 | console.log(priceData); 76 | // console.log("callback", callback); 77 | if (callback) { 78 | callback({ 79 | text: (priceData as any).text || String(priceData), 80 | content: { 81 | success: true, 82 | priceData: priceData, 83 | }, 84 | }); 85 | } 86 | 87 | return; 88 | } catch (error) { 89 | console.error("Error during price fetch:", error); 90 | if (callback) { 91 | callback({ 92 | text: `Error fetching token price: ${error instanceof Error ? error.message : String(error)}`, 93 | content: { 94 | error: 95 | error instanceof Error 96 | ? error.message 97 | : String(error), 98 | }, 99 | }); 100 | } 101 | return; 102 | } 103 | }, 104 | template: priceTemplate, 105 | validate: async ( 106 | _runtime: IAgentRuntime, 107 | message: Memory 108 | ): Promise => { 109 | const content = 110 | typeof message.content === "string" 111 | ? message.content 112 | : message.content?.text; 113 | 114 | console.log("content", content); 115 | if (!content) return false; 116 | // console.log("inside the token price action"); 117 | const priceKeywords = 118 | /\b(price|market|status|situation|data|stats|insights|update|check)\b/i; 119 | const questionWords = /\b(what'?s|how'?s|give|show|tell|check)\b/i; 120 | const tokenSymbols = /\b(TON|NOT|NOTCOIN|DDST|DEDUST|DOGS|STON)\b/i; 121 | 122 | const hasContext = priceKeywords.test(content); 123 | const hasQuestion = questionWords.test(content); 124 | const hasToken = tokenSymbols.test(content); 125 | console.log( 126 | "hasContext,", 127 | hasContext, 128 | "hasQuestion ", 129 | hasQuestion, 130 | hasToken 131 | ); 132 | // Match if either a direct question about price/market or a general status request 133 | return hasToken && (hasContext || hasQuestion); 134 | }, 135 | examples: [ 136 | [ 137 | { 138 | name: "{{user1}}", 139 | content: { 140 | text: "Hey, could you check TON market data?", 141 | action: "GET_TOKEN_PRICE_TON", 142 | }, 143 | }, 144 | { 145 | name: "{{agent}}", 146 | content: { 147 | text: "📊 Analyzing TON market data...", 148 | action: "GET_TOKEN_PRICE_TON", 149 | }, 150 | }, 151 | { 152 | name: "{{agent}}", 153 | content: { 154 | text: "📈 TON Market Update:\n• Current Price: $5.67 (+5.43% 24h)\n• Volume: $1.87B\n• Liquidity: $233M\n• Market Cap: $7.8B", 155 | metadata: { 156 | price: 5.67, 157 | change_24h: 5.43, 158 | volume_24h: 1870000000, 159 | liquidity: 233000000, 160 | market_cap: 7800000000, 161 | }, 162 | }, 163 | }, 164 | ], 165 | [ 166 | { 167 | name: "{{user1}}", 168 | content: { 169 | text: "give me a quick update on the notcoin situation", 170 | action: "GET_TOKEN_PRICE_TON", 171 | }, 172 | }, 173 | { 174 | name: "{{agent}}", 175 | content: { 176 | text: "🔍 Fetching Notcoin stats...", 177 | action: "GET_TOKEN_PRICE_TON", 178 | }, 179 | }, 180 | { 181 | name: "{{agent}}", 182 | content: { 183 | text: "NOT Token Status:\nPrice: $0.0003 | 24h: +2.19%\nLiquidity Pool: $15M\nDaily Volume: $1M\nMarket Rank: #892", 184 | metadata: { 185 | price: 0.0003, 186 | change_24h: 2.19, 187 | liquidity: 15000000, 188 | volume_24h: 1000000, 189 | rank: 892, 190 | }, 191 | }, 192 | }, 193 | ], 194 | [ 195 | { 196 | name: "{{user1}}", 197 | content: { 198 | text: "what's happening with dedust price?", 199 | action: "GET_TOKEN_PRICE_TON", 200 | }, 201 | }, 202 | { 203 | name: "{{agent}}", 204 | content: { 205 | text: "⚡ Getting DeDust market insights...", 206 | action: "GET_TOKEN_PRICE_TON", 207 | }, 208 | }, 209 | { 210 | name: "{{agent}}", 211 | content: { 212 | text: "DeDust (DDST)\nTrading at: $1.23\nTrend: -2.5% (24h)\nVolume: $892K\nPool: $4.2M\nHolder Count: 15.2K", 213 | metadata: { 214 | price: 1.23, 215 | change_24h: -2.5, 216 | volume_24h: 892000, 217 | liquidity: 4200000, 218 | holders: 15200, 219 | }, 220 | }, 221 | }, 222 | ], 223 | ], 224 | }; 225 | -------------------------------------------------------------------------------- /src/services/staking/strategies/hipo/sdk/Treasury.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Address, 3 | beginCell, 4 | Builder, 5 | Cell, 6 | Contract, 7 | ContractProvider, 8 | Dictionary, 9 | DictionaryValue, 10 | Slice, 11 | } from "@ton/ton"; 12 | 13 | export interface Times { 14 | currentRoundSince: bigint; 15 | participateSince: bigint; 16 | participateUntil: bigint; 17 | nextRoundSince: bigint; 18 | nextRoundUntil: bigint; 19 | stakeHeldFor: bigint; 20 | } 21 | 22 | export enum ParticipationState { 23 | Open, 24 | Distributing, 25 | Staked, 26 | Validating, 27 | Held, 28 | Recovering, 29 | Burning, 30 | } 31 | 32 | export interface Request { 33 | minPayment: bigint; 34 | borrowerRewardShare: bigint; 35 | loanAmount: bigint; 36 | accrueAmount: bigint; 37 | stakeAmount: bigint; 38 | newStakeMsg: Cell; 39 | } 40 | 41 | export interface Participation { 42 | state?: ParticipationState; 43 | size?: bigint; 44 | sorted?: Dictionary>; 45 | requests?: Dictionary; 46 | rejected?: Dictionary; 47 | accepted?: Dictionary; 48 | accrued?: Dictionary; 49 | staked?: Dictionary; 50 | recovering?: Dictionary; 51 | totalStaked?: bigint; 52 | totalRecovered?: bigint; 53 | currentVsetHash?: bigint; 54 | stakeHeldFor?: bigint; 55 | stakeHeldUntil?: bigint; 56 | } 57 | 58 | export interface TreasuryConfig { 59 | totalCoins: bigint; 60 | totalTokens: bigint; 61 | totalStaking: bigint; 62 | totalUnstaking: bigint; 63 | totalBorrowersStake: bigint; 64 | parent: Address | null; 65 | participations: Dictionary; 66 | roundsImbalance: bigint; 67 | stopped: boolean; 68 | instantMint: boolean; 69 | loanCodes: Dictionary; 70 | lastStaked: bigint; 71 | lastRecovered: bigint; 72 | halter: Address; 73 | governor: Address; 74 | proposedGovernor: Cell | null; 75 | governanceFee: bigint; 76 | collectionCodes: Dictionary; 77 | billCodes: Dictionary; 78 | oldParents: Dictionary; 79 | } 80 | 81 | export const emptyDictionaryValue: DictionaryValue = { 82 | serialize: function () { 83 | return; 84 | }, 85 | parse: function (): unknown { 86 | return {}; 87 | }, 88 | }; 89 | 90 | export const sortedDictionaryValue: DictionaryValue< 91 | Dictionary 92 | > = { 93 | serialize: function (src: Dictionary, builder: Builder) { 94 | builder.storeRef(beginCell().storeDictDirect(src)); 95 | }, 96 | parse: function (src: Slice): Dictionary { 97 | return src 98 | .loadRef() 99 | .beginParse() 100 | .loadDictDirect(Dictionary.Keys.BigUint(256), emptyDictionaryValue); 101 | }, 102 | }; 103 | 104 | export const requestDictionaryValue: DictionaryValue = { 105 | serialize: function (src: Request, builder: Builder) { 106 | builder 107 | .storeCoins(src.minPayment) 108 | .storeUint(src.borrowerRewardShare, 8) 109 | .storeCoins(src.loanAmount) 110 | .storeCoins(src.accrueAmount) 111 | .storeCoins(src.stakeAmount) 112 | .storeRef(src.newStakeMsg); 113 | }, 114 | parse: function (src: Slice): Request { 115 | return { 116 | minPayment: src.loadCoins(), 117 | borrowerRewardShare: src.loadUintBig(8), 118 | loanAmount: src.loadCoins(), 119 | accrueAmount: src.loadCoins(), 120 | stakeAmount: src.loadCoins(), 121 | newStakeMsg: src.loadRef(), 122 | }; 123 | }, 124 | }; 125 | 126 | export const participationDictionaryValue: DictionaryValue = { 127 | serialize: function (src: Participation, builder: Builder) { 128 | builder 129 | .storeUint(src.state ?? 0, 4) 130 | .storeUint(src.size ?? 0, 16) 131 | .storeDict(src.sorted) 132 | .storeDict(src.requests) 133 | .storeDict(src.rejected) 134 | .storeDict(src.accepted) 135 | .storeDict(src.accrued) 136 | .storeDict(src.staked) 137 | .storeDict(src.recovering) 138 | .storeCoins(src.totalStaked ?? 0) 139 | .storeCoins(src.totalRecovered ?? 0) 140 | .storeUint(src.currentVsetHash ?? 0, 256) 141 | .storeUint(src.stakeHeldFor ?? 0, 32) 142 | .storeUint(src.stakeHeldUntil ?? 0, 32); 143 | }, 144 | parse: function (src: Slice): Participation { 145 | return { 146 | state: src.loadUint(4), 147 | size: src.loadUintBig(16), 148 | sorted: src.loadDict( 149 | Dictionary.Keys.BigUint(112), 150 | sortedDictionaryValue 151 | ), 152 | requests: src.loadDict( 153 | Dictionary.Keys.BigUint(256), 154 | requestDictionaryValue 155 | ), 156 | rejected: src.loadDict( 157 | Dictionary.Keys.BigUint(256), 158 | requestDictionaryValue 159 | ), 160 | accepted: src.loadDict( 161 | Dictionary.Keys.BigUint(256), 162 | requestDictionaryValue 163 | ), 164 | accrued: src.loadDict( 165 | Dictionary.Keys.BigUint(256), 166 | requestDictionaryValue 167 | ), 168 | staked: src.loadDict( 169 | Dictionary.Keys.BigUint(256), 170 | requestDictionaryValue 171 | ), 172 | recovering: src.loadDict( 173 | Dictionary.Keys.BigUint(256), 174 | requestDictionaryValue 175 | ), 176 | totalStaked: src.loadCoins(), 177 | totalRecovered: src.loadCoins(), 178 | currentVsetHash: src.loadUintBig(256), 179 | stakeHeldFor: src.loadUintBig(32), 180 | stakeHeldUntil: src.loadUintBig(32), 181 | }; 182 | }, 183 | }; 184 | 185 | export class Treasury implements Contract { 186 | constructor(readonly address: Address) {} 187 | 188 | static createFromAddress(address: Address) { 189 | return new Treasury(address); 190 | } 191 | 192 | async getTimes(provider: ContractProvider): Promise { 193 | const { stack } = await provider.get("get_times", []); 194 | return { 195 | currentRoundSince: stack.readBigNumber(), 196 | participateSince: stack.readBigNumber(), 197 | participateUntil: stack.readBigNumber(), 198 | nextRoundSince: stack.readBigNumber(), 199 | nextRoundUntil: stack.readBigNumber(), 200 | stakeHeldFor: stack.readBigNumber(), 201 | }; 202 | } 203 | 204 | async getTreasuryState( 205 | provider: ContractProvider 206 | ): Promise { 207 | const { stack } = await provider.get("get_treasury_state", []); 208 | return { 209 | totalCoins: stack.readBigNumber(), 210 | totalTokens: stack.readBigNumber(), 211 | totalStaking: stack.readBigNumber(), 212 | totalUnstaking: stack.readBigNumber(), 213 | totalBorrowersStake: stack.readBigNumber(), 214 | parent: stack.readAddressOpt(), 215 | participations: Dictionary.loadDirect( 216 | Dictionary.Keys.BigUint(32), 217 | participationDictionaryValue, 218 | stack.readCellOpt() 219 | ), 220 | roundsImbalance: stack.readBigNumber(), 221 | stopped: stack.readBoolean(), 222 | instantMint: stack.readBoolean(), 223 | loanCodes: Dictionary.loadDirect( 224 | Dictionary.Keys.BigUint(32), 225 | Dictionary.Values.Cell(), 226 | stack.readCell() 227 | ), 228 | lastStaked: stack.readBigNumber(), 229 | lastRecovered: stack.readBigNumber(), 230 | halter: stack.readAddress(), 231 | governor: stack.readAddress(), 232 | proposedGovernor: stack.readCellOpt(), 233 | governanceFee: stack.readBigNumber(), 234 | collectionCodes: Dictionary.loadDirect( 235 | Dictionary.Keys.BigUint(32), 236 | Dictionary.Values.Cell(), 237 | stack.readCell() 238 | ), 239 | billCodes: Dictionary.loadDirect( 240 | Dictionary.Keys.BigUint(32), 241 | Dictionary.Values.Cell(), 242 | stack.readCell() 243 | ), 244 | oldParents: Dictionary.loadDirect( 245 | Dictionary.Keys.BigUint(256), 246 | emptyDictionaryValue, 247 | stack.readCellOpt() 248 | ), 249 | }; 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/actions/createListing.ts: -------------------------------------------------------------------------------- 1 | import { 2 | elizaLogger, 3 | composePromptFromState, 4 | parseKeyValueXml, 5 | ModelType, 6 | type IAgentRuntime, 7 | type Memory, 8 | type State, 9 | type HandlerCallback, 10 | Content, 11 | } from "@elizaos/core"; 12 | import { Address, internal, SendMode, toNano } from "@ton/ton"; 13 | import { z } from "zod"; 14 | import { initWalletProvider, WalletProvider } from "../providers/wallet"; 15 | import { waitSeqnoContract } from "../utils/util"; 16 | import { 17 | buildNftFixPriceSaleV3R3DeploymentBody, 18 | destinationAddress, 19 | marketplaceAddress, 20 | marketplaceFeeAddress, 21 | } from "../services/nft-marketplace/listingFactory"; 22 | 23 | // Configuration constants 24 | const CONFIG = { 25 | royaltyPercent: 5, 26 | marketplaceFeePercent: 5, 27 | }; 28 | 29 | /** 30 | * Schema for create listing input. 31 | * Only requires: 32 | * - nftAddress: The NFT contract address. 33 | * - fullPrice: The full price of the NFT in TON. 34 | */ 35 | const createListingSchema = z 36 | .object({ 37 | nftAddress: z.string().nonempty("NFT address is required"), 38 | fullPrice: z.string().nonempty("Full price is required"), 39 | }) 40 | .refine((data) => data.nftAddress && data.fullPrice, { 41 | message: "NFT address and full price are required", 42 | path: ["nftAddress", "fullPrice"], 43 | }); 44 | 45 | export interface CreateListingContent extends Content { 46 | nftAddress: string; 47 | fullPrice: string; 48 | } 49 | 50 | function isCreateListingContent( 51 | content: Content 52 | ): content is CreateListingContent { 53 | return ( 54 | typeof content.nftAddress === "string" && 55 | typeof content.fullPrice === "string" 56 | ); 57 | } 58 | 59 | const createListingTemplate = ` 60 | Analyze the conversation and extract the following information about the requested NFT listing: 61 | - NFT address to list for sale 62 | - Full price in TON 63 | 64 | Format the response as key-value pairs in XML format. 65 | 66 | 67 | {{recentMessages}} 68 | 69 | 70 | Extract the NFT listing details from the conversation above. 71 | Respond with the following format: 72 | 73 | nft_address 74 | price_in_ton 75 | `; 76 | 77 | /** 78 | * Helper function to build create listing parameters. 79 | */ 80 | const buildCreateListingData = async ( 81 | runtime: IAgentRuntime, 82 | message: Memory, 83 | state?: State 84 | ): Promise => { 85 | // Initialize or update state 86 | let currentState = state; 87 | if (!currentState) { 88 | currentState = (await runtime.composeState(message)) as State; 89 | } else { 90 | currentState = await runtime.composeState(message, ["RECENT_MESSAGES"]); 91 | } 92 | 93 | const prompt = composePromptFromState({ 94 | state: currentState, 95 | template: createListingTemplate, 96 | }); 97 | 98 | const result = await runtime.useModel(ModelType.TEXT_SMALL, { 99 | prompt, 100 | }); 101 | 102 | const parsedContent = parseKeyValueXml( 103 | typeof result === "string" ? result : (result as any).value || "" 104 | ); 105 | 106 | const listingContent: CreateListingContent = { 107 | nftAddress: parsedContent?.nftAddress || "", 108 | fullPrice: parsedContent?.fullPrice || "", 109 | text: "", // Required by Content interface 110 | }; 111 | 112 | // Validate with schema 113 | const validatedContent = createListingSchema.parse(listingContent); 114 | return validatedContent as CreateListingContent; 115 | }; 116 | 117 | /** 118 | * CreateListingAction encapsulates the logic to list an NFT for sale. 119 | */ 120 | export class CreateListingAction { 121 | private walletProvider: WalletProvider; 122 | constructor(walletProvider: WalletProvider) { 123 | this.walletProvider = walletProvider; 124 | } 125 | 126 | /** 127 | * Lists an NFT for sale using default marketplace configuration 128 | */ 129 | async list(params: CreateListingContent): Promise { 130 | const client = this.walletProvider.getWalletClient(); 131 | const contract = client.open(this.walletProvider.wallet); 132 | 133 | const fullPrice = toNano(params.fullPrice); 134 | const royalty = CONFIG.royaltyPercent; 135 | const fee = CONFIG.marketplaceFeePercent; 136 | 137 | const saleData = { 138 | nftAddress: Address.parse(params.nftAddress), 139 | nftOwnerAddress: this.walletProvider.wallet.address, 140 | deployerAddress: destinationAddress, 141 | marketplaceAddress: marketplaceAddress, 142 | marketplaceFeeAddress: marketplaceFeeAddress, 143 | marketplaceFeePercent: (fullPrice / BigInt(100)) * BigInt(fee), 144 | royaltyAddress: this.walletProvider.wallet.address, // Using wallet address as royalty recipient 145 | royaltyPercent: (fullPrice / BigInt(100)) * BigInt(royalty), 146 | fullTonPrice: fullPrice, 147 | }; 148 | 149 | const saleBody = await buildNftFixPriceSaleV3R3DeploymentBody(saleData); 150 | 151 | const seqno = await contract.getSeqno(); 152 | const listMessage = internal({ 153 | to: params.nftAddress, 154 | value: toNano("0.3"), // Sufficient value for all operations 155 | bounce: true, 156 | body: saleBody, 157 | }); 158 | 159 | const transfer = await contract.sendTransfer({ 160 | seqno, 161 | secretKey: this.walletProvider.keypair.secretKey, 162 | messages: [listMessage], 163 | sendMode: SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, 164 | }); 165 | 166 | await waitSeqnoContract(seqno, contract); 167 | 168 | return { 169 | nftAddress: params.nftAddress, 170 | fullPrice: params.fullPrice, 171 | message: "NFT listed for sale successfully", 172 | marketplaceFee: `${fee}%`, 173 | royaltyFee: `${royalty}%`, 174 | }; 175 | } 176 | } 177 | 178 | export default { 179 | name: "CREATE_LISTING", 180 | similes: ["NFT_LISTING", "LIST_NFT", "SELL_NFT"], 181 | description: 182 | "Creates a listing for an NFT by sending the appropriate message to the NFT contract. Only requires NFT address and price.", 183 | handler: async ( 184 | runtime: IAgentRuntime, 185 | message: Memory, 186 | state?: State, 187 | _options?: any, 188 | callback?: HandlerCallback 189 | ): Promise => { 190 | elizaLogger.log("Starting CREATE_LISTING handler..."); 191 | const params = await buildCreateListingData( 192 | runtime, 193 | message, 194 | state || (await runtime.composeState(message)) 195 | ); 196 | 197 | if (!isCreateListingContent(params)) { 198 | if (callback) { 199 | callback({ 200 | text: "Unable to process create listing request. Invalid content provided.", 201 | content: { error: "Invalid create listing content" }, 202 | }); 203 | } 204 | return; 205 | } 206 | 207 | try { 208 | const walletProvider = await initWalletProvider(runtime); 209 | const createListingAction = new CreateListingAction(walletProvider); 210 | 211 | const result = await createListingAction.list(params); 212 | 213 | if (callback) { 214 | callback({ 215 | text: JSON.stringify(result, null, 2), 216 | content: result, 217 | }); 218 | } 219 | } catch (error) { 220 | elizaLogger.error("Error in CREATE_LISTING handler:"); 221 | if (callback) { 222 | callback({ 223 | text: `Error in CREATE_LISTING: ${error instanceof Error ? error.message : String(error)}`, 224 | content: { 225 | error: 226 | error instanceof Error 227 | ? error.message 228 | : String(error), 229 | }, 230 | }); 231 | } 232 | } 233 | return; 234 | }, 235 | template: createListingTemplate, 236 | // eslint-disable-next-line 237 | validate: async (_runtime: IAgentRuntime) => { 238 | return true; 239 | }, 240 | examples: [ 241 | [ 242 | { 243 | user: "{{user1}}", 244 | name: "{{user1}}", 245 | content: { 246 | nftAddress: "EQNftAddressExample", 247 | fullPrice: "10", 248 | action: "CREATE_LISTING", 249 | }, 250 | }, 251 | { 252 | user: "assistant", 253 | name: "{{agent}}", 254 | content: { 255 | text: "NFT listed for sale successfully", 256 | }, 257 | }, 258 | ], 259 | ], 260 | }; 261 | --------------------------------------------------------------------------------