From 163cdb7950d2391d33574a4920180c4a880d88ba Mon Sep 17 00:00:00 2001 From: Demirix Date: Sat, 25 Jan 2025 13:50:16 +0100 Subject: [PATCH 1/4] chore(add-tests): plugin abstract: test config and tests (#2621) * plugin-abstract: test config * plugin-abstract: deploy token action tests * plugin-abstract: get balance action tests * plugin-abstract: transfer action tests * plugin-abstract: resolving conflicts * Update block-mini.yml Revert "Update block-mini.yml" This reverts commit 368e5fe81fdcd660641def86f299e5bdae099f41. --------- Co-authored-by: Sayo --- .../__tests__/deployTokenAction.test.ts | 443 ++++++++++++++++ .../__tests__/getBalanceAction.test.ts | 373 +++++++++++++ .../__tests__/transferAction.test.ts | 498 ++++++++++++++++++ packages/plugin-abstract/package.json | 14 +- packages/plugin-abstract/vitest.config.ts | 14 + 5 files changed, 1339 insertions(+), 3 deletions(-) create mode 100644 packages/plugin-abstract/__tests__/deployTokenAction.test.ts create mode 100644 packages/plugin-abstract/__tests__/getBalanceAction.test.ts create mode 100644 packages/plugin-abstract/__tests__/transferAction.test.ts create mode 100644 packages/plugin-abstract/vitest.config.ts diff --git a/packages/plugin-abstract/__tests__/deployTokenAction.test.ts b/packages/plugin-abstract/__tests__/deployTokenAction.test.ts new file mode 100644 index 00000000000..2f2d2c8b9aa --- /dev/null +++ b/packages/plugin-abstract/__tests__/deployTokenAction.test.ts @@ -0,0 +1,443 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { deployTokenAction } from '../src/actions/deployTokenAction'; +import { ModelClass, generateObject } from '@elizaos/core'; +import { parseEther } from 'viem'; +import { abstractTestnet } from 'viem/chains'; +import { useGetWalletClient } from '../src/hooks'; +import { validateAbstractConfig } from '../src/environment'; +import { abstractPublicClient } from '../src/utils/viemHelpers'; +import { createAbstractClient } from '@abstract-foundation/agw-client'; + +// Mock dependencies +vi.mock('@elizaos/core', () => { + const actual = vi.importActual('@elizaos/core'); + return { + ...actual, + ModelClass: { + SMALL: 'small' + }, + elizaLogger: { + log: vi.fn(), + error: vi.fn(), + success: vi.fn() + }, + composeContext: vi.fn().mockReturnValue('mocked-context'), + generateObject: vi.fn().mockResolvedValue({ + object: { + name: 'Test Token', + symbol: 'TEST', + initialSupply: '1000000', + useAGW: false + } + }), + stringToUuid: vi.fn().mockReturnValue('mocked-uuid') + }; +}); + +vi.mock('viem', () => ({ + parseEther: vi.fn().mockReturnValue(BigInt(1000000)) +})); + +vi.mock('@abstract-foundation/agw-client', () => ({ + createAbstractClient: vi.fn().mockResolvedValue({ + deployContract: vi.fn().mockResolvedValue('0xhash') + }) +})); + +vi.mock('../src/environment', () => ({ + validateAbstractConfig: vi.fn().mockResolvedValue(true) +})); + +vi.mock('../src/hooks', () => { + const deployContract = vi.fn(); + deployContract.mockResolvedValue('0xhash'); + return { + useGetAccount: vi.fn().mockReturnValue('0xaccount'), + useGetWalletClient: vi.fn().mockReturnValue({ + deployContract + }) + }; +}); + +vi.mock('../src/utils/viemHelpers', () => ({ + abstractPublicClient: { + waitForTransactionReceipt: vi.fn().mockResolvedValue({ + status: 'success', + contractAddress: '0xcontract' + }) + } +})); + +describe('deployTokenAction', () => { + const mockRuntime = { + agentId: 'test-agent', + composeState: vi.fn().mockResolvedValue({ + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Deploy a token named MyToken with symbol MTK and supply 1000000' } } + ], + currentMessage: 'Deploy a token named MyToken with symbol MTK and supply 1000000' + }), + updateRecentMessageState: vi.fn().mockImplementation((state) => ({ + ...state, + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Deploy a token named MyToken with symbol MTK and supply 1000000' } } + ], + currentMessage: 'Deploy a token named MyToken with symbol MTK and supply 1000000' + })), + messageManager: { + createMemory: vi.fn().mockResolvedValue(true) + } + }; + + const mockCallback = vi.fn(); + let mockDeployContract; + + beforeEach(() => { + vi.clearAllMocks(); + mockDeployContract = vi.mocked(useGetWalletClient()).deployContract; + }); + + describe('action properties', () => { + it('should have correct name and similes', () => { + expect(deployTokenAction.name).toBe('DEPLOY_TOKEN'); + expect(deployTokenAction.similes).toContain('CREATE_TOKEN'); + expect(deployTokenAction.similes).toContain('DEPLOY_NEW_TOKEN'); + expect(deployTokenAction.similes).toContain('CREATE_NEW_TOKEN'); + expect(deployTokenAction.similes).toContain('LAUNCH_TOKEN'); + }); + + it('should have a description', () => { + expect(deployTokenAction.description).toBe('Deploy a new ERC20 token contract'); + }); + }); + + describe('validation', () => { + it('should validate abstract config', async () => { + const result = await deployTokenAction.validate(mockRuntime); + expect(result).toBe(true); + }); + + it('should handle validation failure', async () => { + const mockValidateAbstractConfig = vi.mocked(validateAbstractConfig); + mockValidateAbstractConfig.mockRejectedValueOnce(new Error('Config validation failed')); + + await expect(deployTokenAction.validate(mockRuntime)).rejects.toThrow('Config validation failed'); + }); + }); + + describe('state management', () => { + it('should compose state if not provided', async () => { + await deployTokenAction.handler(mockRuntime, {}, undefined, {}, mockCallback); + expect(mockRuntime.composeState).toHaveBeenCalled(); + }); + + it('should update state if provided', async () => { + const mockState = { + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Deploy a token named MyToken with symbol MTK and supply 1000000' } } + ], + currentMessage: 'Deploy a token named MyToken with symbol MTK and supply 1000000' + }; + await deployTokenAction.handler(mockRuntime, {}, mockState, {}, mockCallback); + expect(mockRuntime.updateRecentMessageState).toHaveBeenCalledWith(mockState); + }); + }); + + describe('handler', () => { + it('should handle token deployment without AGW', async () => { + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(parseEther).toHaveBeenCalledWith('1000000'); + expect(mockDeployContract).toHaveBeenCalledWith({ + chain: abstractTestnet, + account: '0xaccount', + abi: expect.any(Array), + bytecode: expect.any(String), + args: ['Test Token', 'TEST', BigInt(1000000)], + kzg: undefined + }); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('deployed successfully'), + content: expect.objectContaining({ + contractAddress: '0xcontract', + tokenName: 'Test Token', + tokenSymbol: 'TEST', + hash: '0xhash' + }) + }); + expect(mockRuntime.messageManager.createMemory).toHaveBeenCalledWith({ + id: 'mocked-uuid', + userId: 'test-agent', + content: expect.objectContaining({ + text: expect.stringContaining('Token deployed'), + tokenAddress: '0xcontract', + name: 'Test Token', + symbol: 'TEST', + initialSupply: '1000000', + source: 'abstract_token_deployment' + }), + agentId: 'test-agent', + roomId: 'mocked-uuid', + createdAt: expect.any(Number) + }); + }); + + it('should handle token deployment with AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: 'Test Token', + symbol: 'TEST', + initialSupply: '1000000', + useAGW: true + } + }); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(parseEther).toHaveBeenCalledWith('1000000'); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('deployed successfully'), + content: expect.objectContaining({ + contractAddress: '0xcontract', + tokenName: 'Test Token', + tokenSymbol: 'TEST', + hash: '0xhash' + }) + }); + }); + + describe('validation cases', () => { + it('should handle empty name', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: '', + symbol: 'TEST', + initialSupply: '1000000', + useAGW: false + } + }); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Unable to process'), + content: expect.objectContaining({ + error: 'Invalid deployment parameters' + }) + }); + }); + + it('should handle invalid symbol length', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: 'Test Token', + symbol: 'TOOLONG', + initialSupply: '1000000', + useAGW: false + } + }); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Unable to process'), + content: expect.objectContaining({ + error: 'Invalid deployment parameters' + }) + }); + }); + + it('should handle zero supply', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: 'Test Token', + symbol: 'TEST', + initialSupply: '0', + useAGW: false + } + }); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Unable to process'), + content: expect.objectContaining({ + error: 'Invalid deployment parameters' + }) + }); + }); + + it('should handle negative supply', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: 'Test Token', + symbol: 'TEST', + initialSupply: '-1000', + useAGW: false + } + }); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Unable to process'), + content: expect.objectContaining({ + error: 'Invalid deployment parameters' + }) + }); + }); + + it('should handle non-numeric supply', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: 'Test Token', + symbol: 'TEST', + initialSupply: 'not-a-number', + useAGW: false + } + }); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Unable to process'), + content: expect.objectContaining({ + error: 'Invalid deployment parameters' + }) + }); + }); + }); + + describe('error handling', () => { + it('should handle deployment errors', async () => { + mockDeployContract.mockRejectedValueOnce(new Error('Deployment failed')); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error deploying token: Deployment failed'), + content: expect.objectContaining({ + error: 'Deployment failed' + }) + }); + }); + + it('should handle transaction receipt errors', async () => { + const mockWaitForTransactionReceipt = vi.mocked(abstractPublicClient.waitForTransactionReceipt); + mockWaitForTransactionReceipt.mockRejectedValueOnce(new Error('Transaction failed')); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error deploying token: Transaction failed'), + content: expect.objectContaining({ + error: 'Transaction failed' + }) + }); + }); + + it('should handle AGW client creation errors', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + name: 'Test Token', + symbol: 'TEST', + initialSupply: '1000000', + useAGW: true + } + }); + + const mockCreateAbstractClient = vi.mocked(createAbstractClient); + mockCreateAbstractClient.mockRejectedValueOnce(new Error('AGW client creation failed')); + + const result = await deployTokenAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error deploying token: AGW client creation failed'), + content: expect.objectContaining({ + error: 'AGW client creation failed' + }) + }); + }); + }); + }); +}); diff --git a/packages/plugin-abstract/__tests__/getBalanceAction.test.ts b/packages/plugin-abstract/__tests__/getBalanceAction.test.ts new file mode 100644 index 00000000000..30cc068e0e2 --- /dev/null +++ b/packages/plugin-abstract/__tests__/getBalanceAction.test.ts @@ -0,0 +1,373 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { getBalanceAction } from '../src/actions/getBalanceAction'; +import { ModelClass, generateObject } from '@elizaos/core'; +import { formatUnits } from 'viem'; +import { ETH_ADDRESS } from '../src/constants'; +import { useGetAccount } from '../src/hooks'; +import { resolveAddress, getTokenByName, abstractPublicClient } from '../src/utils/viemHelpers'; +import { validateAbstractConfig } from '../src/environment'; + +// Mock dependencies +vi.mock('@elizaos/core', () => { + const actual = vi.importActual('@elizaos/core'); + return { + ...actual, + ModelClass: { + SMALL: 'small' + }, + elizaLogger: { + log: vi.fn(), + error: vi.fn(), + success: vi.fn() + }, + composeContext: vi.fn().mockReturnValue('mocked-context'), + generateObject: vi.fn().mockResolvedValue({ + object: { + tokenAddress: '0xtoken', + walletAddress: '0xwallet', + tokenSymbol: 'TEST' + } + }), + stringToUuid: vi.fn().mockReturnValue('mocked-uuid') + }; +}); + +vi.mock('viem', () => ({ + formatUnits: vi.fn().mockReturnValue('1.0'), + isAddress: vi.fn().mockReturnValue(true), + erc20Abi: [ + { + name: 'balanceOf', + type: 'function', + inputs: [{ type: 'address' }], + outputs: [{ type: 'uint256' }] + }, + { + name: 'decimals', + type: 'function', + inputs: [], + outputs: [{ type: 'uint8' }] + }, + { + name: 'symbol', + type: 'function', + inputs: [], + outputs: [{ type: 'string' }] + } + ] +})); + +vi.mock('../src/environment', () => ({ + validateAbstractConfig: vi.fn().mockResolvedValue(true) +})); + +vi.mock('../src/hooks', () => ({ + useGetAccount: vi.fn().mockReturnValue({ + address: '0xaccount' + }) +})); + +vi.mock('../src/utils/viemHelpers', () => ({ + resolveAddress: vi.fn().mockResolvedValue('0xresolved'), + getTokenByName: vi.fn().mockReturnValue({ address: '0xtoken' }), + abstractPublicClient: { + getBalance: vi.fn().mockResolvedValue(BigInt(1000000000000000000)), + readContract: vi.fn().mockImplementation(async ({ functionName }) => { + switch (functionName) { + case 'balanceOf': + return BigInt(1000000); + case 'decimals': + return 18; + case 'symbol': + return 'TEST'; + default: + throw new Error('Unexpected function call'); + } + }) + } +})); + +describe('getBalanceAction', () => { + const mockRuntime = { + agentId: 'test-agent', + composeState: vi.fn().mockResolvedValue({ + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Check my ETH balance' } } + ], + currentMessage: 'Check my ETH balance' + }), + updateRecentMessageState: vi.fn().mockImplementation((state) => ({ + ...state, + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Check my ETH balance' } } + ], + currentMessage: 'Check my ETH balance' + })), + messageManager: { + createMemory: vi.fn().mockResolvedValue(true), + getMemoryById: vi.fn().mockResolvedValue({ + content: { + tokenAddress: '0xtoken' + } + }) + } + }; + + const mockCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('action properties', () => { + it('should have correct name and similes', () => { + expect(getBalanceAction.name).toBe('GET_BALANCE'); + expect(getBalanceAction.similes).toContain('CHECK_BALANCE'); + expect(getBalanceAction.similes).toContain('VIEW_BALANCE'); + expect(getBalanceAction.similes).toContain('SHOW_BALANCE'); + expect(getBalanceAction.similes).toContain('BALANCE_CHECK'); + expect(getBalanceAction.similes).toContain('TOKEN_BALANCE'); + }); + + it('should have a description', () => { + expect(getBalanceAction.description).toBe('Check token balance for a given address'); + }); + }); + + describe('validation', () => { + it('should validate abstract config', async () => { + const result = await getBalanceAction.validate(mockRuntime, {}); + expect(result).toBe(true); + }); + + it('should handle validation failure', async () => { + const mockValidateAbstractConfig = vi.mocked(validateAbstractConfig); + mockValidateAbstractConfig.mockRejectedValueOnce(new Error('Config validation failed')); + + await expect(getBalanceAction.validate(mockRuntime, {})).rejects.toThrow('Config validation failed'); + }); + }); + + describe('state management', () => { + it('should compose state if not provided', async () => { + await getBalanceAction.handler(mockRuntime, {}, undefined, {}, mockCallback); + expect(mockRuntime.composeState).toHaveBeenCalled(); + }); + + it('should update state if provided', async () => { + const mockState = { + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Check my ETH balance' } } + ], + currentMessage: 'Check my ETH balance' + }; + await getBalanceAction.handler(mockRuntime, {}, mockState, {}, mockCallback); + expect(mockRuntime.updateRecentMessageState).toHaveBeenCalledWith(mockState); + }); + }); + + describe('handler', () => { + describe('ETH balance checks', () => { + it('should handle ETH balance check with default address', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + walletAddress: null, + tokenSymbol: null + } + }); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(abstractPublicClient.getBalance).toHaveBeenCalledWith({ + address: '0xresolved' + }); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('1.0 ETH'), + content: expect.objectContaining({ + balance: '1.0', + symbol: 'ETH' + }) + }); + }); + + it('should handle ETH balance check with specific address', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + walletAddress: '0xspecific', + tokenSymbol: null + } + }); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(resolveAddress).toHaveBeenCalledWith('0xspecific'); + expect(abstractPublicClient.getBalance).toHaveBeenCalledWith({ + address: '0xresolved' + }); + }); + }); + + describe('token balance checks', () => { + it('should handle token balance check with address', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: '0xtoken', + walletAddress: null, + tokenSymbol: null + } + }); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(abstractPublicClient.readContract).toHaveBeenCalledWith( + expect.objectContaining({ + address: '0xtoken', + functionName: 'balanceOf', + args: ['0xresolved'] + }) + ); + }); + + it('should handle token balance check with symbol', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: null, + walletAddress: null, + tokenSymbol: 'TEST' + } + }); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(mockRuntime.messageManager.getMemoryById).toHaveBeenCalledWith('mocked-uuid'); + expect(abstractPublicClient.readContract).toHaveBeenCalledWith( + expect.objectContaining({ + address: '0xtoken', + functionName: 'balanceOf', + args: ['0xresolved'] + }) + ); + }); + }); + + describe('error handling', () => { + it('should handle invalid address', async () => { + const mockResolveAddress = vi.mocked(resolveAddress); + mockResolveAddress.mockResolvedValueOnce(null); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Invalid address or ENS name'), + content: expect.objectContaining({ + error: 'Invalid address or ENS name' + }) + }); + }); + + it('should handle balance check errors', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + walletAddress: '0xwallet', + tokenSymbol: null + } + }); + + const mockGetBalance = vi.mocked(abstractPublicClient.getBalance); + mockGetBalance.mockRejectedValueOnce(new Error('Balance check failed')); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error checking balance: Balance check failed'), + content: expect.objectContaining({ + error: 'Balance check failed' + }) + }); + }); + + it('should handle token contract errors', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: '0xtoken', + walletAddress: null, + tokenSymbol: null + } + }); + + const mockReadContract = vi.mocked(abstractPublicClient.readContract); + mockReadContract.mockRejectedValueOnce(new Error('Contract read failed')); + + const result = await getBalanceAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error checking balance: Contract read failed'), + content: expect.objectContaining({ + error: 'Contract read failed' + }) + }); + }); + }); + }); +}); diff --git a/packages/plugin-abstract/__tests__/transferAction.test.ts b/packages/plugin-abstract/__tests__/transferAction.test.ts new file mode 100644 index 00000000000..02469b9f906 --- /dev/null +++ b/packages/plugin-abstract/__tests__/transferAction.test.ts @@ -0,0 +1,498 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { transferAction } from '../src/actions/transferAction'; +import { ModelClass, generateObject } from '@elizaos/core'; +import { formatUnits, parseUnits, erc20Abi } from 'viem'; +import { ETH_ADDRESS } from '../src/constants'; +import { useGetAccount, useGetWalletClient } from '../src/hooks'; +import { resolveAddress, getTokenByName, abstractPublicClient } from '../src/utils/viemHelpers'; +import { createAbstractClient } from '@abstract-foundation/agw-client'; +import { validateAbstractConfig } from '../src/environment'; + +// Mock dependencies +vi.mock('@elizaos/core', () => { + const actual = vi.importActual('@elizaos/core'); + return { + ...actual, + ModelClass: { + SMALL: 'small' + }, + elizaLogger: { + log: vi.fn(), + error: vi.fn(), + success: vi.fn() + }, + composeContext: vi.fn().mockReturnValue('mocked-context'), + generateObject: vi.fn().mockResolvedValue({ + object: { + tokenAddress: '0xtoken', + recipient: '0xrecipient', + amount: '1.0', + useAGW: false, + tokenSymbol: 'TEST' + } + }), + stringToUuid: vi.fn().mockReturnValue('mocked-uuid') + }; +}); + +vi.mock('viem', () => ({ + formatUnits: vi.fn().mockReturnValue('1.0'), + parseUnits: vi.fn().mockReturnValue(BigInt(1000000000000000000)), + isAddress: vi.fn().mockReturnValue(true), + erc20Abi: [ + { + name: 'transfer', + type: 'function', + inputs: [ + { type: 'address' }, + { type: 'uint256' } + ], + outputs: [{ type: 'bool' }] + }, + { + name: 'decimals', + type: 'function', + inputs: [], + outputs: [{ type: 'uint8' }] + }, + { + name: 'symbol', + type: 'function', + inputs: [], + outputs: [{ type: 'string' }] + } + ] +})); + +vi.mock('@abstract-foundation/agw-client', () => ({ + createAbstractClient: vi.fn().mockResolvedValue({ + sendTransaction: vi.fn().mockResolvedValue('0xhash'), + writeContract: vi.fn().mockResolvedValue('0xhash') + }) +})); + +vi.mock('../src/environment', () => ({ + validateAbstractConfig: vi.fn().mockResolvedValue(true) +})); + +vi.mock('../src/hooks', () => { + const writeContract = vi.fn().mockResolvedValue('0xhash'); + const sendTransaction = vi.fn().mockResolvedValue('0xhash'); + return { + useGetAccount: vi.fn().mockReturnValue({ + address: '0xaccount' + }), + useGetWalletClient: vi.fn().mockReturnValue({ + writeContract, + sendTransaction + }) + }; +}); + +vi.mock('../src/utils/viemHelpers', () => ({ + resolveAddress: vi.fn().mockResolvedValue('0xresolved'), + getTokenByName: vi.fn().mockReturnValue({ address: '0xtoken' }), + abstractPublicClient: { + readContract: vi.fn().mockImplementation(async ({ functionName }) => { + switch (functionName) { + case 'symbol': + return 'TEST'; + case 'decimals': + return 18; + default: + throw new Error('Unexpected function call'); + } + }) + } +})); + +describe('transferAction', () => { + const mockRuntime = { + agentId: 'test-agent', + composeState: vi.fn().mockResolvedValue({ + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Send 1 ETH to 0xrecipient' } } + ], + currentMessage: 'Send 1 ETH to 0xrecipient' + }), + updateRecentMessageState: vi.fn().mockImplementation((state) => ({ + ...state, + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Send 1 ETH to 0xrecipient' } } + ], + currentMessage: 'Send 1 ETH to 0xrecipient' + })), + messageManager: { + getMemoryById: vi.fn().mockResolvedValue({ + content: { + tokenAddress: '0xtoken' + } + }) + } + }; + + const mockCallback = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('action properties', () => { + it('should have correct name and similes', () => { + expect(transferAction.name).toBe('SEND_TOKEN'); + expect(transferAction.similes).toContain('TRANSFER_TOKEN_ON_ABSTRACT'); + expect(transferAction.similes).toContain('TRANSFER_TOKENS_ON_ABSTRACT'); + expect(transferAction.similes).toContain('SEND_TOKENS_ON_ABSTRACT'); + expect(transferAction.similes).toContain('SEND_ETH_ON_ABSTRACT'); + expect(transferAction.similes).toContain('PAY_ON_ABSTRACT'); + expect(transferAction.similes).toContain('MOVE_TOKENS_ON_ABSTRACT'); + expect(transferAction.similes).toContain('MOVE_ETH_ON_ABSTRACT'); + }); + + it('should have a description', () => { + expect(transferAction.description).toBe("Transfer tokens from the agent's wallet to another address"); + }); + }); + + describe('validation', () => { + it('should validate abstract config', async () => { + const result = await transferAction.validate(mockRuntime); + expect(result).toBe(true); + }); + + it('should handle validation failure', async () => { + const mockValidateAbstractConfig = vi.mocked(validateAbstractConfig); + mockValidateAbstractConfig.mockRejectedValueOnce(new Error('Config validation failed')); + + await expect(transferAction.validate(mockRuntime)).rejects.toThrow('Config validation failed'); + }); + }); + + describe('state management', () => { + it('should compose state if not provided', async () => { + await transferAction.handler(mockRuntime, {}, undefined, {}, mockCallback); + expect(mockRuntime.composeState).toHaveBeenCalled(); + }); + + it('should update state if provided', async () => { + const mockState = { + recentMessagesData: [ + { content: { text: 'previous message' } }, + { content: { text: 'Send 1 ETH to 0xrecipient' } } + ], + currentMessage: 'Send 1 ETH to 0xrecipient' + }; + await transferAction.handler(mockRuntime, {}, mockState, {}, mockCallback); + expect(mockRuntime.updateRecentMessageState).toHaveBeenCalledWith(mockState); + }); + }); + + describe('handler', () => { + describe('ETH transfers', () => { + it('should handle ETH transfer without AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + recipient: '0xrecipient', + amount: '1.0', + useAGW: false, + tokenSymbol: null + } + }); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + const walletClient = useGetWalletClient(); + expect(walletClient.sendTransaction).toHaveBeenCalledWith({ + account: expect.any(Object), + chain: expect.any(Object), + to: '0xresolved', + value: BigInt(1000000000000000000), + kzg: undefined + }); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('1.0 ETH'), + content: expect.objectContaining({ + hash: '0xhash', + tokenAmount: '1.0', + symbol: 'ETH', + recipient: '0xresolved', + useAGW: false + }) + }); + }); + + it('should handle ETH transfer with AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + recipient: '0xrecipient', + amount: '1.0', + useAGW: true, + tokenSymbol: null + } + }); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + const mockAbstractClient = await createAbstractClient({}); + expect(mockAbstractClient.sendTransaction).toHaveBeenCalledWith({ + chain: expect.any(Object), + to: '0xresolved', + value: BigInt(1000000000000000000), + kzg: undefined + }); + }); + }); + + describe('token transfers', () => { + it('should handle token transfer without AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: '0xtoken', + recipient: '0xrecipient', + amount: '1.0', + useAGW: false, + tokenSymbol: null + } + }); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + const walletClient = useGetWalletClient(); + expect(walletClient.writeContract).toHaveBeenCalledWith({ + account: expect.any(Object), + chain: expect.any(Object), + address: '0xtoken', + abi: expect.any(Array), + functionName: 'transfer', + args: ['0xresolved', BigInt(1000000000000000000)] + }); + }); + + it('should handle token transfer with AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: '0xtoken', + recipient: '0xrecipient', + amount: '1.0', + useAGW: true, + tokenSymbol: null + } + }); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + const mockAbstractClient = await createAbstractClient({}); + expect(mockAbstractClient.writeContract).toHaveBeenCalledWith({ + chain: expect.any(Object), + address: '0xtoken', + abi: expect.any(Array), + functionName: 'transfer', + args: ['0xresolved', BigInt(1000000000000000000)] + }); + }); + + it('should handle token transfer by symbol', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: null, + recipient: '0xrecipient', + amount: '1.0', + useAGW: false, + tokenSymbol: 'TEST' + } + }); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(true); + expect(mockRuntime.messageManager.getMemoryById).toHaveBeenCalledWith('mocked-uuid'); + const walletClient = useGetWalletClient(); + expect(walletClient.writeContract).toHaveBeenCalledWith({ + account: expect.any(Object), + chain: expect.any(Object), + address: '0xtoken', + abi: expect.any(Array), + functionName: 'transfer', + args: ['0xresolved', BigInt(1000000000000000000)] + }); + }); + }); + + describe('error handling', () => { + it('should handle invalid recipient address', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + recipient: '0xinvalid', + amount: '1.0', + useAGW: false, + tokenSymbol: null + } + }); + + const mockResolveAddress = vi.mocked(resolveAddress); + mockResolveAddress.mockResolvedValueOnce(null); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: 'Unable to process transfer request. Did not extract valid parameters.', + content: expect.objectContaining({ + error: expect.stringContaining('Expected string, received null'), + recipient: null, + tokenAddress: ETH_ADDRESS, + useAGW: false, + amount: '1.0' + }) + }); + }); + + it('should handle transfer errors without AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + recipient: '0xrecipient', + amount: '1.0', + useAGW: false, + tokenSymbol: null + } + }); + + const walletClient = useGetWalletClient(); + vi.mocked(walletClient.sendTransaction).mockRejectedValueOnce(new Error('Transfer failed')); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error transferring tokens: Transfer failed'), + content: expect.objectContaining({ + error: 'Transfer failed' + }) + }); + }); + + it('should handle transfer errors with AGW', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: ETH_ADDRESS, + recipient: '0xrecipient', + amount: '1.0', + useAGW: true, + tokenSymbol: null + } + }); + + const mockCreateAbstractClient = vi.mocked(createAbstractClient); + mockCreateAbstractClient.mockRejectedValueOnce(new Error('AGW client creation failed')); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error transferring tokens: AGW client creation failed'), + content: expect.objectContaining({ + error: 'AGW client creation failed' + }) + }); + }); + + it('should handle token contract errors', async () => { + const mockGenerateObject = vi.mocked(generateObject); + mockGenerateObject.mockResolvedValueOnce({ + object: { + tokenAddress: '0xtoken', + recipient: '0xrecipient', + amount: '1.0', + useAGW: false, + tokenSymbol: null + } + }); + + const mockReadContract = vi.mocked(abstractPublicClient.readContract); + mockReadContract.mockRejectedValueOnce(new Error('Contract read failed')); + + const result = await transferAction.handler( + mockRuntime, + {}, + undefined, + {}, + mockCallback + ); + + expect(result).toBe(false); + expect(mockCallback).toHaveBeenCalledWith({ + text: expect.stringContaining('Error transferring tokens: Contract read failed'), + content: expect.objectContaining({ + error: 'Contract read failed' + }) + }); + }); + }); + }); +}); diff --git a/packages/plugin-abstract/package.json b/packages/plugin-abstract/package.json index 9117f7e84ff..cc7a7af3e3e 100644 --- a/packages/plugin-abstract/package.json +++ b/packages/plugin-abstract/package.json @@ -19,13 +19,21 @@ "dist" ], "dependencies": { + "@abstract-foundation/agw-client": "1.0.1", "@elizaos/core": "workspace:*", - "@abstract-foundation/agw-client": "1.0.1" + "tsup": "^8.3.5", + "viem": "2.22.2" }, "scripts": { "lint": "eslint --fix --cache .", "build": "tsup --format esm --no-dts", - "dev": "tsup --format esm --no-dts --watch" + "dev": "tsup --format esm --no-dts --watch", + "test": "vitest run", + "test:watch": "vitest watch", + "test:coverage": "vitest run --coverage" + }, + "devDependencies": { + "vitest": "^1.0.0" }, "peerDependencies": { "whatwg-url": "7.1.0" @@ -34,4 +42,4 @@ "tsup": "8.3.5", "typescript": "4.9" } -} +} \ No newline at end of file diff --git a/packages/plugin-abstract/vitest.config.ts b/packages/plugin-abstract/vitest.config.ts new file mode 100644 index 00000000000..81433e47fe0 --- /dev/null +++ b/packages/plugin-abstract/vitest.config.ts @@ -0,0 +1,14 @@ +/// +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + coverage: { + reporter: ['text', 'json', 'html'], + include: ['src/**/*.ts'], + exclude: ['**/*.test.ts', '**/*.d.ts'] + } + } +}); From e7db6124dcad781c7fef13281f7b14a4a4b3cd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brandon=20Rodr=C3=ADguez?= Date: Sat, 25 Jan 2025 07:15:36 -0600 Subject: [PATCH 2/4] feat(client-alexa): Basic Alexa skill notification (#2564) * feat: integrate alexa (skill) * fix: comment bot property * feat: new Alexa envs * doc: Update clients.md update clients documentation * doc: remove unnecessary documentation * fix: coderabbit comments addressed * fix: pnpm lock file * Update pnpm-lock.yaml --------- Co-authored-by: Sayo --- .env.example | 5 + agent/package.json | 1 + agent/src/index.ts | 8 ++ docs/docs/packages/clients.md | 70 ++++++++----- packages/client-alexa/.npmignore | 6 ++ packages/client-alexa/package.json | 38 +++++++ packages/client-alexa/src/alexa-client.ts | 116 ++++++++++++++++++++++ packages/client-alexa/src/index.ts | 26 +++++ packages/client-alexa/tsconfig.json | 10 ++ packages/client-alexa/tsup.config.ts | 21 ++++ packages/client-alexa/vitest.config.ts | 14 +++ packages/core/src/types.ts | 1 + pnpm-lock.yaml | 73 ++++++++++++-- 13 files changed, 351 insertions(+), 38 deletions(-) create mode 100644 packages/client-alexa/.npmignore create mode 100644 packages/client-alexa/package.json create mode 100644 packages/client-alexa/src/alexa-client.ts create mode 100644 packages/client-alexa/src/index.ts create mode 100644 packages/client-alexa/tsconfig.json create mode 100644 packages/client-alexa/tsup.config.ts create mode 100644 packages/client-alexa/vitest.config.ts diff --git a/.env.example b/.env.example index 8103f281e3a..1ee44d124df 100644 --- a/.env.example +++ b/.env.example @@ -88,6 +88,11 @@ WHATSAPP_BUSINESS_ACCOUNT_ID= # Business Account ID from Facebook Business Mana WHATSAPP_WEBHOOK_VERIFY_TOKEN= # Custom string for webhook verification WHATSAPP_API_VERSION=v17.0 # WhatsApp API version (default: v17.0) +# Alexa Client Configuration +ALEXA_SKILL_ID= # Your Alexa skill ID from developer console (format: amzn1.ask.skill-...) +ALEXA_CLIENT_ID= # OAuth2 Client ID from Alexa developer console permissions tab +ALEXA_CLIENT_SECRET= # OAuth2 Client Secret from Alexa developer console permissions tab + # Simsai Specific Configuration SIMSAI_API_KEY= # API key for SimsAI authentication diff --git a/agent/package.json b/agent/package.json index f91339ea54e..aac023f1d80 100644 --- a/agent/package.json +++ b/agent/package.json @@ -34,6 +34,7 @@ "@elizaos/client-twitter": "workspace:*", "@elizaos/client-instagram": "workspace:*", "@elizaos/client-slack": "workspace:*", + "@elizaos/client-alexa": "workspace:*", "@elizaos/client-simsai": "workspace:*", "@elizaos/core": "workspace:*", "@elizaos/plugin-0g": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index 3bd9c5935da..ea137a40a13 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -11,6 +11,7 @@ import { LensAgentClient } from "@elizaos/client-lens" import { SlackClientInterface } from "@elizaos/client-slack" import { TelegramClientInterface } from "@elizaos/client-telegram" import { TwitterClientInterface } from "@elizaos/client-twitter" +import { AlexaClientInterface } from "@elizaos/client-alexa"; import { MongoDBDatabaseAdapter } from "@elizaos/adapter-mongodb" import { FarcasterClientInterface } from "@elizaos/client-farcaster" @@ -639,6 +640,13 @@ export async function initializeClients(character: Character, runtime: IAgentRun } } + if (clientTypes.includes(Clients.ALEXA)) { + const alexaClient = await AlexaClientInterface.start(runtime); + if (alexaClient) { + clients.alexa = alexaClient; + } + } + if (clientTypes.includes(Clients.INSTAGRAM)) { const instagramClient = await InstagramClientInterface.start(runtime) if (instagramClient) { diff --git a/docs/docs/packages/clients.md b/docs/docs/packages/clients.md index 24fa4bfb289..9ed862d719a 100644 --- a/docs/docs/packages/clients.md +++ b/docs/docs/packages/clients.md @@ -35,11 +35,12 @@ graph TD ## Available Clients -- **Discord** (`@elizaos/client-discord`) - Full Discord bot integration -- **Twitter** (`@elizaos/client-twitter`) - Twitter bot and interaction handling -- **Telegram** (`@elizaos/client-telegram`) - Telegram bot integration -- **Direct** (`@elizaos/client-direct`) - Direct API interface for custom integrations -- **Auto** (`@elizaos/client-auto`) - Automated trading and interaction client +- **Discord** (`@elizaos/client-discord`) - Full Discord bot integration +- **Twitter** (`@elizaos/client-twitter`) - Twitter bot and interaction handling +- **Telegram** (`@elizaos/client-telegram`) - Telegram bot integration +- **Direct** (`@elizaos/client-direct`) - Direct API for custom integrations +- **Auto** (`@elizaos/client-auto`) - Automated trading and interaction client +- **Alexa skill** (`@elizaos/client-alexa`) - Alexa skill API integration --- @@ -83,11 +84,11 @@ DISCORD_API_TOKEN = your_bot_token; ### Features -- Voice channel integration -- Message attachments -- Reactions handling -- Media transcription -- Room management +- Voice channel integration +- Message attachments +- Reactions handling +- Media transcription +- Room management ### Voice Integration @@ -145,9 +146,9 @@ TWITTER_EMAIL = your_email; ### Components -- **PostClient**: Handles creating and managing posts -- **SearchClient**: Handles search functionality -- **InteractionClient**: Manages user interactions +- **PostClient**: Handles creating and managing posts +- **SearchClient**: Handles search functionality +- **InteractionClient**: Manages user interactions ### Post Management @@ -272,12 +273,9 @@ class AutoClient { this.runtime = runtime; // Start trading loop - this.interval = setInterval( - () => { - this.makeTrades(); - }, - 60 * 60 * 1000, - ); // 1 hour interval + this.interval = setInterval(() => { + this.makeTrades(); + }, 60 * 60 * 1000); // 1 hour interval } async makeTrades() { @@ -293,6 +291,24 @@ class AutoClient { } ``` +## Alexa Client + +The Alexa client provides API integration with alexa skill. + +### Basic Setup + +```typescript +import { AlexaClientInterface } from "@elizaos/client-alexa"; + +// Initialize client +const client = await AlexaClientInterface.start(runtime); + +// Configuration in .env +ALEXA_SKILL_ID= your_alexa_skill_id +ALEXA_CLIENT_ID= your_alexa_client_id #Alexa developer console permissions tab +ALEXA_CLIENT_SECRET= your_alexa_client_secret #Alexa developer console permissions tab +``` + ## Common Features ### Message Handling @@ -301,9 +317,9 @@ All clients implement standard message handling: ```typescript interface ClientInterface { - async handleMessage(message: Message): Promise; - async generateResponse(context: Context): Promise; - async sendMessage(destination: string, content: Content): Promise; + handleMessage(message: Message): Promise; + generateResponse(context: Context): Promise; + sendMessage(destination: string, content: Content): Promise; } ``` @@ -311,9 +327,9 @@ interface ClientInterface { ```typescript interface MediaProcessor { - async processImage(image: Image): Promise; - async processVideo(video: Video): Promise; - async processAudio(audio: Audio): Promise; + processImage(image: Image): Promise; + processVideo(video: Video): Promise; + processAudio(audio: Audio): Promise; } ``` @@ -420,7 +436,7 @@ class RateLimiter { private calculateBackoff(error: RateLimitError): number { return Math.min( this.baseDelay * Math.pow(2, this.attempts), - this.maxDelay, + this.maxDelay ); } } @@ -507,4 +523,4 @@ async processMessage(message) { ## Related Resources -- [Error Handling](../../packages/core) +- [Error Handling](../../packages/core) diff --git a/packages/client-alexa/.npmignore b/packages/client-alexa/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/client-alexa/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/client-alexa/package.json b/packages/client-alexa/package.json new file mode 100644 index 00000000000..17d76d2632c --- /dev/null +++ b/packages/client-alexa/package.json @@ -0,0 +1,38 @@ +{ + "name": "@elizaos/client-alexa", + "version": "0.1.8+build.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "@elizaos/plugin-node": "workspace:*", + "ask-sdk-core": "^2.14.0", + "ask-sdk-model": "^1.86.0", + "axios": "1.7.9" + }, + "devDependencies": { + "tsup": "8.3.5", + "vitest": "1.2.1" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + } +} diff --git a/packages/client-alexa/src/alexa-client.ts b/packages/client-alexa/src/alexa-client.ts new file mode 100644 index 00000000000..4a555b63778 --- /dev/null +++ b/packages/client-alexa/src/alexa-client.ts @@ -0,0 +1,116 @@ +import { elizaLogger, IAgentRuntime } from "@elizaos/core"; +import { DefaultApiClient } from "ask-sdk-core"; +import { services } from "ask-sdk-model"; +import axios from "axios"; +import { v4 } from "uuid"; + +export class AlexaClient { + // private bot: services.proactiveEvents.ProactiveEventsServiceClient; Use for conversations + private LwaServiceClient: services.LwaServiceClient; + private apiConfiguration: any; + private runtime: IAgentRuntime; + private skillId: string; + private clientId: string; + private clientSecret: string; + + constructor(runtime: IAgentRuntime) { + elizaLogger.log("šŸ“± Constructing new AlexaClient..."); + this.runtime = runtime; + this.apiConfiguration = { + apiClient: new DefaultApiClient(), + apiEndpoint: "https://api.amazonalexa.com", + }; + this.skillId = runtime.getSetting("ALEXA_SKILL_ID"); + this.clientId = runtime.getSetting("ALEXA_CLIENT_ID"); + this.clientSecret = runtime.getSetting("ALEXA_CLIENT_SECRET"); + } + + public async start(): Promise { + elizaLogger.log("šŸš€ Starting Alexa bot..."); + try { + await this.initializeBot(); + } catch (error) { + elizaLogger.error("āŒ Failed to launch Alexa bot:", error); + throw error; + } + } + + private async initializeBot(): Promise { + const authenticationConfiguration = { + clientId: this.clientId, + clientSecret: this.clientSecret, + }; + this.LwaServiceClient = new services.LwaServiceClient({ + apiConfiguration: this.apiConfiguration, + authenticationConfiguration, + }); + + elizaLogger.log("āœØ Alexa bot successfully launched and is running!"); + const access_token = await this.LwaServiceClient.getAccessTokenForScope( + "alexa::proactive_events" + ); + + await this.sendProactiveEvent(access_token); + } + + async sendProactiveEvent(access_token: string): Promise { + const event = { + timestamp: new Date().toISOString(), + referenceId: v4(), + expiryTime: new Date(Date.now() + 10 * 60000).toISOString(), + event: { + name: "AMAZON.MessageAlert.Activated", + payload: { + state: { + status: "UNREAD", + freshness: "NEW", + }, + messageGroup: { + creator: { + name: "Eliza", + }, + count: 1, + }, + }, + }, + localizedAttributes: [ + { + locale: "en-US", + source: "localizedattribute:source", + }, + ], + relevantAudience: { + type: "Multicast", + payload: {}, + }, + }; + + try { + const response = await axios.post( + "https://api.amazonalexa.com/v1/proactiveEvents/stages/development", + event, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${access_token}`, + }, + } + ); + switch (response.status) { + case 202: + elizaLogger.log("āœ… Proactive event sent successfully."); + break; + case 400: + elizaLogger.error( + `${response.data.code} - ${response.data.message}}` + ); + break; + case 401: + elizaLogger.error("Unauthorized"); + break; + } + } catch (error) { + elizaLogger.error("Error", error); + } + } +} diff --git a/packages/client-alexa/src/index.ts b/packages/client-alexa/src/index.ts new file mode 100644 index 00000000000..46d925f7e52 --- /dev/null +++ b/packages/client-alexa/src/index.ts @@ -0,0 +1,26 @@ +import { Client, IAgentRuntime, elizaLogger } from "@elizaos/core"; +import { AlexaClient } from "./alexa-client"; + +export const AlexaClientInterface: Client = { + start: async (runtime: IAgentRuntime) => { + const alexaClient = new AlexaClient(runtime); + + await alexaClient.start(); + + elizaLogger.success( + `āœ… Alexa client successfully started for character ${runtime.character.name}` + ); + return alexaClient; + }, + stop: async (runtime: IAgentRuntime) => { + try { + // stop it + elizaLogger.log("Stopping alexa client", runtime.agentId); + await runtime.clients.alexa.stop(); + } catch (e) { + elizaLogger.error("client-alexa interface stop error", e); + } + }, +}; + +export default AlexaClientInterface; diff --git a/packages/client-alexa/tsconfig.json b/packages/client-alexa/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /dev/null +++ b/packages/client-alexa/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/client-alexa/tsup.config.ts b/packages/client-alexa/tsup.config.ts new file mode 100644 index 00000000000..8eea21ba74f --- /dev/null +++ b/packages/client-alexa/tsup.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "fluent-ffmpeg", + // Add other modules you want to externalize + ], +}); diff --git a/packages/client-alexa/vitest.config.ts b/packages/client-alexa/vitest.config.ts new file mode 100644 index 00000000000..a11fbbd0d9e --- /dev/null +++ b/packages/client-alexa/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@elizaos/core': resolve(__dirname, '../core/src'), + }, + }, +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 881b2afbc07..3ac19883acc 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -646,6 +646,7 @@ export type Plugin = { * Available client platforms */ export enum Clients { + ALEXA= "alexa", DISCORD = "discord", DIRECT = "direct", TWITTER = "twitter", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fe1d710b67e..e4986e6aa1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -133,6 +133,9 @@ importers: '@elizaos/adapter-supabase': specifier: workspace:* version: link:../packages/adapter-supabase + '@elizaos/client-alexa': + specifier: workspace:* + version: link:../packages/client-alexa '@elizaos/client-auto': specifier: workspace:* version: link:../packages/client-auto @@ -892,6 +895,31 @@ importers: specifier: ^3.0.2 version: 3.0.2(@types/node@22.10.10)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0) + packages/client-alexa: + dependencies: + '@elizaos/core': + specifier: workspace:* + version: link:../core + '@elizaos/plugin-node': + specifier: workspace:* + version: link:../plugin-node + ask-sdk-core: + specifier: ^2.14.0 + version: 2.14.0(ask-sdk-model@1.86.0) + ask-sdk-model: + specifier: ^1.86.0 + version: 1.86.0 + axios: + specifier: 1.7.9 + version: 1.7.9 + devDependencies: + tsup: + specifier: 8.3.5 + version: 8.3.5(@swc/core@1.10.9(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@5.7.3)(yaml@2.7.0) + vitest: + specifier: 1.2.1 + version: 1.2.1(@types/node@22.10.10)(jsdom@25.0.1(bufferutil@4.0.9)(canvas@2.11.2(encoding@0.1.13))(utf-8-validate@6.0.5))(terser@5.37.0) + packages/client-auto: dependencies: '@elizaos/core': @@ -1500,13 +1528,16 @@ importers: '@elizaos/core': specifier: workspace:* version: link:../core + tsup: + specifier: ^8.3.5 + version: 8.3.5(@swc/core@1.10.9(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@4.9.5)(yaml@2.7.0) + viem: + specifier: 2.21.58 + version: 2.21.58(bufferutil@4.0.9)(typescript@4.9.5)(utf-8-validate@6.0.5)(zod@3.24.1) whatwg-url: specifier: 7.1.0 version: 7.1.0 devDependencies: - tsup: - specifier: 8.3.5 - version: 8.3.5(@swc/core@1.10.9(@swc/helpers@0.5.15))(jiti@2.4.2)(postcss@8.5.1)(tsx@4.19.2)(typescript@4.9.5)(yaml@2.7.0) typescript: specifier: '4.9' version: 4.9.5 @@ -3523,7 +3554,7 @@ importers: version: link:../core file-type-checker: specifier: ^1.1.2 - version: 1.1.2 + version: 1.1.3 mrmime: specifier: ^2.0.0 version: 2.0.0 @@ -12228,8 +12259,8 @@ packages: '@swc/types@0.1.17': resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} - '@switchboard-xyz/common@2.5.15': - resolution: {integrity: sha512-W4ub5Na0pf+OIBp8a8JhHzDIqleNI8iClNE5SeQeAMeElzT99fzfEXlY0gVXA7tXOrsLer9I383dCku0H7TDEw==} + '@switchboard-xyz/common@2.5.16': + resolution: {integrity: sha512-j2WK7Mv47XvGyMlt37bc4Hy/bRhSrDvhOlD7ykotoyUX5jrulnx0ege+8LIoKNGD8TMltsIEh7Ea24xY8EL54A==} engines: {node: '>=12'} '@switchboard-xyz/on-demand@1.2.42': @@ -14082,6 +14113,17 @@ packages: resolution: {integrity: sha512-Zj3b8juz1ZtDaQDPQlzWyk2I4wZPx3RmcGq8pVJeZXl2Tjw0WRy5ueHPelxZtBLqCirGoZxZEAFRs6SZUSCBjg==} engines: {node: '>=18'} + ask-sdk-core@2.14.0: + resolution: {integrity: sha512-G2yEKbY+XYTFzJXGRpsg7xJHqqgBcgnKEgklXUMRWRFv10gwHgtWDLLlBV6h4IdysTx782rCVJK/UcHcaGbEpA==} + peerDependencies: + ask-sdk-model: ^1.29.0 + + ask-sdk-model@1.86.0: + resolution: {integrity: sha512-JmC5mypPBz5Q1Yx1WyeAr2Q/z2Cjm98EjLjTlYAolXF4gokU7fDDjeOAyAD5dkyHjyGLeCEvlC0MJYWFwc84dw==} + + ask-sdk-runtime@2.14.0: + resolution: {integrity: sha512-a96pPs1RU3GgXBHplqAVqh2uxEuSYjTD5+XSbHsf6Fz4KhHpgaxJogOIIybjA6O/d1B9sG+6bjHJGzxBJEcccA==} + asn1.js@4.10.1: resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} @@ -17383,8 +17425,8 @@ packages: peerDependencies: webpack: ^4.0.0 || ^5.0.0 - file-type-checker@1.1.2: - resolution: {integrity: sha512-HodBNiinBQNHQfXhXzAuHkU2udHF3LFS6PAOEZqxW+BjotZVCaMR7ckpTTnvLi718dbzRavnjRX0kbSb5pJG3g==} + file-type-checker@1.1.3: + resolution: {integrity: sha512-SLMNPu0RZEQsfR+GRNnVBlBPdtXn2BTpvSzBRw9MDjDacobK+Vc0WtbQ/mZx7vqNy+b6juKsza5DvEN5i7LwCw==} file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -43380,7 +43422,7 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@switchboard-xyz/common@2.5.15(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': + '@switchboard-xyz/common@2.5.16(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)': dependencies: '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) axios: 1.7.9 @@ -43405,7 +43447,7 @@ snapshots: '@coral-xyz/anchor-30': '@coral-xyz/anchor@0.30.1(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10)' '@solana/web3.js': 1.95.8(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) '@solworks/soltoolkit-sdk': 0.0.23(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) - '@switchboard-xyz/common': 2.5.15(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) + '@switchboard-xyz/common': 2.5.16(bufferutil@4.0.9)(encoding@0.1.13)(utf-8-validate@5.0.10) axios: 1.7.9 big.js: 6.2.2 bs58: 5.0.0 @@ -46855,6 +46897,15 @@ snapshots: bignumber.js: 9.1.2 optional: true + ask-sdk-core@2.14.0(ask-sdk-model@1.86.0): + dependencies: + ask-sdk-model: 1.86.0 + ask-sdk-runtime: 2.14.0 + + ask-sdk-model@1.86.0: {} + + ask-sdk-runtime@2.14.0: {} + asn1.js@4.10.1: dependencies: bn.js: 4.12.1 @@ -51375,7 +51426,7 @@ snapshots: schema-utils: 3.3.0 webpack: 5.97.1(@swc/core@1.10.9(@swc/helpers@0.5.15)) - file-type-checker@1.1.2: {} + file-type-checker@1.1.3: {} file-uri-to-path@1.0.0: {} From 4375d6b81ddc772ea2cc05dad0af0e470c2da73e Mon Sep 17 00:00:00 2001 From: "Ninja Dev (QI)" <142059473+azep-ninja@users.noreply.github.com> Date: Sat, 25 Jan 2025 06:04:26 -0800 Subject: [PATCH 3/4] feat(new-plugin): quick intel plugin for token security analysis (#2391) * add quick intel plugin. * fx * fix conflicts * fix conflicts * sync/fix * initialise plugin --------- Co-authored-by: Odilitime Co-authored-by: Sayo --- .env.example | 3 + agent/package.json | 241 +++++++++--------- agent/src/index.ts | 2 + packages/plugin-quick-intel/README.md | 155 +++++++++++ packages/plugin-quick-intel/eslint.config.mjs | 3 + packages/plugin-quick-intel/package.json | 26 ++ .../plugin-quick-intel/src/actions/audit.ts | 193 ++++++++++++++ packages/plugin-quick-intel/src/index.ts | 13 + .../plugin-quick-intel/src/templates/index.ts | 46 ++++ .../src/utils/chain-detection.ts | 151 +++++++++++ packages/plugin-quick-intel/tsconfig.json | 13 + packages/plugin-quick-intel/tsup.config.ts | 12 + 12 files changed, 738 insertions(+), 120 deletions(-) create mode 100644 packages/plugin-quick-intel/README.md create mode 100644 packages/plugin-quick-intel/eslint.config.mjs create mode 100644 packages/plugin-quick-intel/package.json create mode 100644 packages/plugin-quick-intel/src/actions/audit.ts create mode 100644 packages/plugin-quick-intel/src/index.ts create mode 100644 packages/plugin-quick-intel/src/templates/index.ts create mode 100644 packages/plugin-quick-intel/src/utils/chain-detection.ts create mode 100644 packages/plugin-quick-intel/tsconfig.json create mode 100644 packages/plugin-quick-intel/tsup.config.ts diff --git a/.env.example b/.env.example index 1ee44d124df..da1ecd46c8c 100644 --- a/.env.example +++ b/.env.example @@ -895,3 +895,6 @@ ANKR_SPASH=true # DCAP Plugin Configuration DCAP_EVM_PRIVATE_KEY= DCAP_MODE= # Options: OFF, PLUGIN-SGX, PLUGIN-TEE, MOCK + +# QuickIntel Token Security API +QUICKINTEL_API_KEY= # Your QuickIntel API key for token security analysis diff --git a/agent/package.json b/agent/package.json index aac023f1d80..e3886932937 100644 --- a/agent/package.json +++ b/agent/package.json @@ -1,123 +1,124 @@ { - "name": "@elizaos/agent", - "version": "0.1.9-alpha.1", - "main": "src/index.ts", - "type": "module", - "scripts": { - "start": "node --loader ts-node/esm src/index.ts", - "dev": "node --loader ts-node/esm src/index.ts", - "check-types": "tsc --noEmit", - "test": "jest" - }, - "nodemonConfig": { - "watch": [ - "src", - "../core/dist" - ], - "ext": "ts,json", - "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" - }, - "dependencies": { - "@elizaos/adapter-supabase": "workspace:*", - "@elizaos/adapter-postgres": "workspace:*", - "@elizaos/adapter-redis": "workspace:*", - "@elizaos/adapter-sqlite": "workspace:*", - "@elizaos/adapter-pglite": "workspace:*", + "name": "@elizaos/agent", + "version": "0.1.9-alpha.1", + "main": "src/index.ts", + "type": "module", + "scripts": { + "start": "node --loader ts-node/esm src/index.ts", + "dev": "node --loader ts-node/esm src/index.ts", + "check-types": "tsc --noEmit", + "test": "jest" + }, + "nodemonConfig": { + "watch": [ + "src", + "../core/dist" + ], + "ext": "ts,json", + "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" + }, + "dependencies": { + "@elizaos/adapter-supabase": "workspace:*", + "@elizaos/adapter-postgres": "workspace:*", + "@elizaos/adapter-redis": "workspace:*", + "@elizaos/adapter-sqlite": "workspace:*", + "@elizaos/adapter-pglite": "workspace:*", "@elizaos/adapter-qdrant": "workspace:*", "@elizaos/adapter-mongodb": "workspace:*", - "@elizaos/client-auto": "workspace:*", - "@elizaos/client-direct": "workspace:*", - "@elizaos/client-discord": "workspace:*", - "@elizaos/client-farcaster": "workspace:*", - "@elizaos/client-lens": "workspace:*", - "@elizaos/client-telegram": "workspace:*", - "@elizaos/client-twitter": "workspace:*", - "@elizaos/client-instagram": "workspace:*", - "@elizaos/client-slack": "workspace:*", + "@elizaos/client-auto": "workspace:*", + "@elizaos/client-direct": "workspace:*", + "@elizaos/client-discord": "workspace:*", + "@elizaos/client-farcaster": "workspace:*", + "@elizaos/client-lens": "workspace:*", + "@elizaos/client-telegram": "workspace:*", + "@elizaos/client-twitter": "workspace:*", + "@elizaos/client-instagram": "workspace:*", + "@elizaos/client-slack": "workspace:*", "@elizaos/client-alexa": "workspace:*", "@elizaos/client-simsai": "workspace:*", - "@elizaos/core": "workspace:*", - "@elizaos/plugin-0g": "workspace:*", - "@elizaos/plugin-abstract": "workspace:*", - "@elizaos/plugin-agentkit": "workspace:*", - "@elizaos/plugin-aptos": "workspace:*", - "@elizaos/plugin-birdeye": "workspace:*", - "@elizaos/plugin-coingecko": "workspace:*", - "@elizaos/plugin-coinmarketcap": "workspace:*", + "@elizaos/core": "workspace:*", + "@elizaos/plugin-0g": "workspace:*", + "@elizaos/plugin-abstract": "workspace:*", + "@elizaos/plugin-agentkit": "workspace:*", + "@elizaos/plugin-aptos": "workspace:*", + "@elizaos/plugin-birdeye": "workspace:*", + "@elizaos/plugin-coingecko": "workspace:*", + "@elizaos/plugin-coinmarketcap": "workspace:*", "@elizaos/plugin-zerion": "workspace:*", - "@elizaos/plugin-binance": "workspace:*", - "@elizaos/plugin-avail": "workspace:*", + "@elizaos/plugin-binance": "workspace:*", + "@elizaos/plugin-avail": "workspace:*", "@elizaos/plugin-bnb": "workspace:*", - "@elizaos/plugin-bootstrap": "workspace:*", - "@elizaos/plugin-di": "workspace:*", - "@elizaos/plugin-cosmos": "workspace:*", - "@elizaos/plugin-intiface": "workspace:*", - "@elizaos/plugin-coinbase": "workspace:*", - "@elizaos/plugin-conflux": "workspace:*", - "@elizaos/plugin-evm": "workspace:*", - "@elizaos/plugin-echochambers": "workspace:*", - "@elizaos/plugin-flow": "workspace:*", - "@elizaos/plugin-gitbook": "workspace:*", - "@elizaos/plugin-story": "workspace:*", - "@elizaos/plugin-gitcoin-passport": "workspace:*", - "@elizaos/plugin-goat": "workspace:*", - "@elizaos/plugin-lensNetwork": "workspace:*", - "@elizaos/plugin-icp": "workspace:*", + "@elizaos/plugin-bootstrap": "workspace:*", + "@elizaos/plugin-di": "workspace:*", + "@elizaos/plugin-cosmos": "workspace:*", + "@elizaos/plugin-intiface": "workspace:*", + "@elizaos/plugin-coinbase": "workspace:*", + "@elizaos/plugin-conflux": "workspace:*", + "@elizaos/plugin-evm": "workspace:*", + "@elizaos/plugin-echochambers": "workspace:*", + "@elizaos/plugin-flow": "workspace:*", + "@elizaos/plugin-gitbook": "workspace:*", + "@elizaos/plugin-story": "workspace:*", + "@elizaos/plugin-gitcoin-passport": "workspace:*", + "@elizaos/plugin-goat": "workspace:*", + "@elizaos/plugin-lensNetwork": "workspace:*", + "@elizaos/plugin-icp": "workspace:*", "@elizaos/plugin-initia": "workspace:*", - "@elizaos/plugin-image-generation": "workspace:*", + "@elizaos/plugin-image-generation": "workspace:*", "@elizaos/plugin-lit": "workspace:*", "@elizaos/plugin-moralis": "workspace:*", - "@elizaos/plugin-movement": "workspace:*", - "@elizaos/plugin-massa": "workspace:*", - "@elizaos/plugin-nft-generation": "workspace:*", - "@elizaos/plugin-node": "workspace:*", - "@elizaos/plugin-solana": "workspace:*", - "@elizaos/plugin-injective": "workspace:*", - "@elizaos/plugin-solana-agent-kit": "workspace:*", - "@elizaos/plugin-squid-router": "workspace:*", - "@elizaos/plugin-autonome": "workspace:*", - "@elizaos/plugin-starknet": "workspace:*", - "@elizaos/plugin-stargaze": "workspace:*", - "@elizaos/plugin-giphy": "workspace:*", - "@elizaos/plugin-ton": "workspace:*", - "@elizaos/plugin-sui": "workspace:*", - "@elizaos/plugin-sgx": "workspace:*", - "@elizaos/plugin-iq6900": "workspace:*", - "@elizaos/plugin-tee": "workspace:*", - "@elizaos/plugin-tee-log": "workspace:*", - "@elizaos/plugin-tee-marlin": "workspace:*", - "@elizaos/plugin-multiversx": "workspace:*", - "@elizaos/plugin-near": "workspace:*", - "@elizaos/plugin-zksync-era": "workspace:*", - "@elizaos/plugin-twitter": "workspace:*", - "@elizaos/plugin-primus": "workspace:*", - "@elizaos/plugin-cronoszkevm": "workspace:*", + "@elizaos/plugin-movement": "workspace:*", + "@elizaos/plugin-massa": "workspace:*", + "@elizaos/plugin-nft-generation": "workspace:*", + "@elizaos/plugin-node": "workspace:*", + "@elizaos/plugin-quick-intel": "workspace:*", + "@elizaos/plugin-solana": "workspace:*", + "@elizaos/plugin-injective": "workspace:*", + "@elizaos/plugin-solana-agent-kit": "workspace:*", + "@elizaos/plugin-squid-router": "workspace:*", + "@elizaos/plugin-autonome": "workspace:*", + "@elizaos/plugin-starknet": "workspace:*", + "@elizaos/plugin-stargaze": "workspace:*", + "@elizaos/plugin-giphy": "workspace:*", + "@elizaos/plugin-ton": "workspace:*", + "@elizaos/plugin-sui": "workspace:*", + "@elizaos/plugin-sgx": "workspace:*", + "@elizaos/plugin-iq6900": "workspace:*", + "@elizaos/plugin-tee": "workspace:*", + "@elizaos/plugin-tee-log": "workspace:*", + "@elizaos/plugin-tee-marlin": "workspace:*", + "@elizaos/plugin-multiversx": "workspace:*", + "@elizaos/plugin-near": "workspace:*", + "@elizaos/plugin-zksync-era": "workspace:*", + "@elizaos/plugin-twitter": "workspace:*", + "@elizaos/plugin-primus": "workspace:*", + "@elizaos/plugin-cronoszkevm": "workspace:*", "@elizaos/plugin-cronos": "workspace:*", - "@elizaos/plugin-3d-generation": "workspace:*", - "@elizaos/plugin-fuel": "workspace:*", - "@elizaos/plugin-avalanche": "workspace:*", - "@elizaos/plugin-video-generation": "workspace:*", - "@elizaos/plugin-web-search": "workspace:*", - "@elizaos/plugin-dexscreener": "workspace:*", - "@elizaos/plugin-letzai": "workspace:*", - "@elizaos/plugin-thirdweb": "workspace:*", - "@elizaos/plugin-genlayer": "workspace:*", - "@elizaos/plugin-tee-verifiable-log": "workspace:*", - "@elizaos/plugin-depin": "workspace:*", - "@elizaos/plugin-open-weather": "workspace:*", - "@elizaos/plugin-obsidian": "workspace:*", - "@elizaos/plugin-arthera": "workspace:*", - "@elizaos/plugin-allora": "workspace:*", - "@elizaos/plugin-opacity": "workspace:*", - "@elizaos/plugin-hyperliquid": "workspace:*", - "@elizaos/plugin-akash": "workspace:*", - "@elizaos/plugin-quai": "workspace:*", - "@elizaos/plugin-lightning": "workspace:*", - "@elizaos/plugin-b2": "workspace:*", - "@elizaos/plugin-nft-collections": "workspace:*", - "@elizaos/plugin-pyth-data": "workspace:*", - "@elizaos/plugin-openai": "workspace:*", - "@elizaos/plugin-devin": "workspace:*", + "@elizaos/plugin-3d-generation": "workspace:*", + "@elizaos/plugin-fuel": "workspace:*", + "@elizaos/plugin-avalanche": "workspace:*", + "@elizaos/plugin-video-generation": "workspace:*", + "@elizaos/plugin-web-search": "workspace:*", + "@elizaos/plugin-dexscreener": "workspace:*", + "@elizaos/plugin-letzai": "workspace:*", + "@elizaos/plugin-thirdweb": "workspace:*", + "@elizaos/plugin-genlayer": "workspace:*", + "@elizaos/plugin-tee-verifiable-log": "workspace:*", + "@elizaos/plugin-depin": "workspace:*", + "@elizaos/plugin-open-weather": "workspace:*", + "@elizaos/plugin-obsidian": "workspace:*", + "@elizaos/plugin-arthera": "workspace:*", + "@elizaos/plugin-allora": "workspace:*", + "@elizaos/plugin-opacity": "workspace:*", + "@elizaos/plugin-hyperliquid": "workspace:*", + "@elizaos/plugin-akash": "workspace:*", + "@elizaos/plugin-quai": "workspace:*", + "@elizaos/plugin-lightning": "workspace:*", + "@elizaos/plugin-b2": "workspace:*", + "@elizaos/plugin-nft-collections": "workspace:*", + "@elizaos/plugin-pyth-data": "workspace:*", + "@elizaos/plugin-openai": "workspace:*", + "@elizaos/plugin-devin": "workspace:*", "@elizaos/plugin-holdstation": "workspace:*", "@elizaos/plugin-router-nitro": "workspace:*", "@elizaos/plugin-nvidia-nim": "workspace:*", @@ -139,15 +140,15 @@ "@elizaos/plugin-dcap": "workspace:*", "@elizaos/plugin-form": "workspace:*", "@elizaos/plugin-ankr": "workspace:*", - "readline": "1.3.0", - "ws": "8.18.0", - "yargs": "17.7.2" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "jest": "^29.7.0", - "ts-jest": "^29.2.5", - "ts-node": "10.9.2", - "tsup": "8.3.5" - } + "readline": "1.3.0", + "ws": "8.18.0", + "yargs": "17.7.2" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", + "ts-node": "10.9.2", + "tsup": "8.3.5" + } } diff --git a/agent/src/index.ts b/agent/src/index.ts index ea137a40a13..aa51bb16e20 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -141,6 +141,7 @@ import { minaPlugin } from "@elizaos/plugin-mina" import { ankrPlugin } from "@elizaos/plugin-ankr"; import { formPlugin } from "@elizaos/plugin-form"; import { MongoClient } from "mongodb"; +import { quickIntelPlugin } from "@elizaos/plugin-quick-intel" const __filename = fileURLToPath(import.meta.url) // get the resolved path to the file const __dirname = path.dirname(__filename) // get the name of the directory @@ -899,6 +900,7 @@ export async function createAgent(character: Character, db: IDatabaseAdapter, ca getSecret(character, "FORM_PRIVATE_KEY") ? formPlugin : null, getSecret(character, "ANKR_WALLET") ? ankrPlugin : null, getSecret(character, "DCAP_EVM_PRIVATE_KEY") && getSecret(character, "DCAP_MODE") ? dcapPlugin : null, + getSecret(character, "QUICKINTEL_API_KEY") ? quickIntelPlugin : null, ].filter(Boolean), providers: [], managers: [], diff --git a/packages/plugin-quick-intel/README.md b/packages/plugin-quick-intel/README.md new file mode 100644 index 00000000000..ae0040a6e37 --- /dev/null +++ b/packages/plugin-quick-intel/README.md @@ -0,0 +1,155 @@ +# @elizaos/plugin-quickintel + +A plugin for performing token security audits and market analysis within the ElizaOS ecosystem. + +## Description + +This plugin enables comprehensive token security analysis using QuickIntel's API, combined with market data from DexScreener. It supports multiple chains and address formats, providing detailed security assessments and market insights in natural language responses. + +## Installation + +```bash +pnpm install @elizaos/plugin-quickintel +``` + +## Configuration + +### Environment Variables + +```typescript +QUICKINTEL_API_KEY= +``` + +### Client Configuration + +Add the plugin to your character.json file: + +```json +{ + "name": "YourCharacter", + "plugins": ["quickintel"], + "settings": { + "QUICKINTEL_API_KEY": "your-api-key-here" + } +} +``` + +## Usage + +### Basic Integration + +```typescript +import { quickIntelPlugin } from "@elizaos/plugin-quickintel"; +``` + +### Example Usage + +The plugin processes natural language queries for token audits: + +```typescript +"Can you check if this token is safe? 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on BSC" +"Analyze this token on Ethereum: 0x1234..." +"Is this Solana token safe? Hep4ZQ3MSSXFuLnT4baBFVBrC3677ntjrfaqE9zEt4rX" +``` + +### Supported Features + +- Multi-chain support (EVM chains, Solana, etc.) +- Comprehensive security analysis +- Market data integration +- Natural language responses +- Detailed risk assessments + +## API Reference + +### Actions + +#### Audit Token Action + +Performs security audits and market analysis on tokens. + +**Response Type:** + +```typescript +interface AuditResponse { + success: boolean; + data: { + audit: any; // QuickIntel audit data + market?: any; // DexScreener market data + }; + params: { + chain: string; + tokenAddress: string; + }; +} +``` + +### Supported Chains + +The plugin supports all chains available through QuickIntel, including: +- Ethereum (ETH) +- BNB Smart Chain (BSC) +- Polygon +- Arbitrum +- Avalanche +- Solana +- Full list available at https://docs.quickintel.io/quick-intel-scanner/supported-chains +- And many more... + +## Common Issues & Troubleshooting + +1. **API Issues** + - Verify API key is correct + - Check API endpoint accessibility + - Ensure proper network connectivity + +2. **Chain/Address Detection** + - Ensure chain name is clearly specified (e.g., "on ETH") + - Verify token address format + - Check chain support in QuickIntel + +3. **Market Data** + - DexScreener data might be unavailable for some tokens + - Some chains might have different market data availability + - Liquidity information may vary by chain + +## Security Best Practices + +1. **API Configuration** + - Store API key securely + - Use environment variables + - Implement proper error handling + +2. **Response Handling** + - Validate audit results + - Handle timeouts gracefully + - Process market data carefully + +``` + +## Future Enhancements + +- Enhanced market data analysis +- Historical audit tracking +- Custom audit templates +- Comparative analysis features + +## Contributing + +Contributions are welcome! Please see the main Eliza repository for contribution guidelines. + +## Credits + +This plugin integrates with: + +- [QuickIntel](https://quickintel.io): Token security audit platform +- [DexScreener](https://dexscreener.com): DEX market data provider + +Special thanks to: +- The QuickIntel team for their security analysis platform +- The DexScreener team for market data access +- The Eliza community for feedback and testing + +## License + +This plugin is part of the Eliza project. See the main project repository for license information. \ No newline at end of file diff --git a/packages/plugin-quick-intel/eslint.config.mjs b/packages/plugin-quick-intel/eslint.config.mjs new file mode 100644 index 00000000000..924ebf3bf73 --- /dev/null +++ b/packages/plugin-quick-intel/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; \ No newline at end of file diff --git a/packages/plugin-quick-intel/package.json b/packages/plugin-quick-intel/package.json new file mode 100644 index 00000000000..8b0a61f61c4 --- /dev/null +++ b/packages/plugin-quick-intel/package.json @@ -0,0 +1,26 @@ +{ + "name": "@elizaos/plugin-quick-intel", + "version": "0.1.9-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest", + "lint": "eslint --fix --cache ." + } + } diff --git a/packages/plugin-quick-intel/src/actions/audit.ts b/packages/plugin-quick-intel/src/actions/audit.ts new file mode 100644 index 00000000000..feffcba188a --- /dev/null +++ b/packages/plugin-quick-intel/src/actions/audit.ts @@ -0,0 +1,193 @@ +import { + Action, + ActionExample, + Content, + HandlerCallback, + IAgentRuntime, + Memory, + ModelClass, + State, + composeContext, + elizaLogger, + generateMessageResponse +} from "@elizaos/core"; +import { auditTemplate } from "../templates"; +import { extractTokenInfo } from "../utils/chain-detection"; + +class TokenAuditAction { + private apiKey: string; + + constructor(apiKey: string) { + this.apiKey = apiKey; + } + + async audit(chain: string, tokenAddress: string): Promise { + elizaLogger.log("Auditing token:", { chain, tokenAddress }); + const myHeaders = new Headers(); + myHeaders.append("X-QKNTL-KEY", this.apiKey); + myHeaders.append("Content-Type", "application/json"); + + const requestOptions: RequestInit = { + method: "POST", + headers: myHeaders, + body: JSON.stringify({ chain, tokenAddress }), + redirect: "follow" as RequestRedirect, + }; + + const response = await fetch("https://api.quickintel.io/v1/getquickiauditfull", requestOptions); + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}`); + } + return await response.json(); + } + + async fetchDexData(tokenAddress: string, chain: string): Promise { + elizaLogger.log("Fetching DEX data:", { tokenAddress, chain }); + const myHeaders = new Headers(); + myHeaders.append("Content-Type", "application/json"); + + const requestOptions: RequestInit = { + method: "GET", + headers: myHeaders, + }; + + const response = await fetch(`https://api.dexscreener.com/latest/dex/tokens/${tokenAddress}`, requestOptions); + if (!response.ok) { + throw new Error(`DexScreener API failed with status ${response.status}`); + } + + const data = await response.json(); + if (!data?.pairs?.length) { + return null; + } + + // Filter pairs for the target chain + const chainPairs = data.pairs.filter((pair: any) => + pair.chainId.toLowerCase() === chain.toLowerCase() || pair.chainId.toLowerCase().includes(chain.toLowerCase()) + ); + + const otherChains = data.pairs + .filter((pair: any) => pair.chainId.toLowerCase() !== chain.toLowerCase()) + .map((pair: any) => pair.chainId); + + return { + pairs: chainPairs, + otherChains: [...new Set(otherChains)] + }; + } +} + +export const auditAction: Action = { + name: "AUDIT_TOKEN", + description: "Perform a security audit on a token using QuickIntel", + similes: ["SCAN_TOKEN", "CHECK_TOKEN", "TOKEN_SECURITY", "ANALYZE_TOKEN"], + validate: async (runtime: IAgentRuntime) => { + const apiKey = runtime.getSetting("QUICKINTEL_API_KEY"); + return typeof apiKey === "string" && apiKey.length > 0; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting QuickIntel audit handler..."); + + try { + const apiKey = runtime.getSetting("QUICKINTEL_API_KEY"); + if (!apiKey) { + throw new Error("QuickIntel API key not configured"); + } + + // Extract chain and address from the message content + const messageText = message.content.text; + const { chain, tokenAddress } = extractTokenInfo(messageText); + + if (!chain || !tokenAddress) { + throw new Error("Could not determine chain and token address. Please specify both the chain and token address."); + } + + // Perform audit + elizaLogger.log("Performing audit for:", { chain, tokenAddress }); + const action = new TokenAuditAction(apiKey); + const [auditData, dexData] = await Promise.all([ + action.audit(chain, tokenAddress), + action.fetchDexData(tokenAddress, chain) + ]); + + state = await runtime.composeState(message, { + ...state, + auditData: JSON.stringify(auditData, null, 2), + marketData: auditData?.tokenDetails?.tokenName ? JSON.stringify(dexData, null, 2) : null, + }); + // Generate analysis using audit data + const context = composeContext({ + state, + template: auditTemplate + }); + + const responseContent = await generateMessageResponse({ + runtime, + context, + modelClass: ModelClass.LARGE + }); + + if (!responseContent) { + throw new Error("Failed to generate audit analysis"); + } + + //await callback(response); + if (callback) { + const response = { + text: responseContent.text, + content: { + success: true, + data: auditData, + params: { chain, tokenAddress } + }, + action: responseContent.action, + inReplyTo: message.id + }; + + const memories = await callback(response); + } + + return true; + } catch (error) { + elizaLogger.error("Error in AUDIT_TOKEN handler:", error?.message, error?.error); + + if (callback) { + await callback({ + text: `An error occurred while performing the token audit. Please try again later, and ensure the address is correct, and chain is supported.`, + content: { error: "Internal server error" }, + inReplyTo: message.id + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "Can you check if this token is safe? 0x742d35Cc6634C0532925a3b844Bc454e4438f44e on BSC", + }, + }, + { + user: "{{agent}}", + content: { + text: "I'll analyze this token's security for you.", + action: "AUDIT_TOKEN", + }, + }, + { + user: "{{agent}}", + content: { + text: "Here's what I found in the security audit:\n\nšŸ”’ Overall Security Status: Medium Risk\n\nKey Findings:\nāœ… Contract is verified\nāœ… Not a honeypot\nāš ļø Ownership not renounced\n\nDetailed Analysis:\n...", + }, + }, + ], + ] as ActionExample[][], +}; \ No newline at end of file diff --git a/packages/plugin-quick-intel/src/index.ts b/packages/plugin-quick-intel/src/index.ts new file mode 100644 index 00000000000..2908288dfb0 --- /dev/null +++ b/packages/plugin-quick-intel/src/index.ts @@ -0,0 +1,13 @@ +import { Plugin } from "@elizaos/core"; +import { auditAction } from "./actions/audit"; + +export const quickIntelPlugin: Plugin = { + name: "Quick Intel", + description: "QuickIntel Plugin for Eliza - Enables token security analysis", + actions: [auditAction], + providers: [], + evaluators: [], + services: [] +}; + +export default quickIntelPlugin; \ No newline at end of file diff --git a/packages/plugin-quick-intel/src/templates/index.ts b/packages/plugin-quick-intel/src/templates/index.ts new file mode 100644 index 00000000000..104eaf9b92d --- /dev/null +++ b/packages/plugin-quick-intel/src/templates/index.ts @@ -0,0 +1,46 @@ +import { messageCompletionFooter } from "@elizaos/core"; + +export const auditTemplate = `Extract and analyze token security information based on the conversation context: + +# Task: Generate a response for the character {{agentName}}. DO NOT PROVIDE ANY FOLLOW UP ACTIONS. +About {{agentName}}: +{{bio}} +{{lore}} + +{{recentMessages}} + +First, determine the token and chain to audit from the results below: + +Security Audit Results: +{{auditData}} + +Market Data Results: +{{marketData}} + +**Analysis Instructions:** + +1. **Data Check:** If {{auditData}} is empty or indicates no security data, respond with a concise message stating there is "No security data available". Don't provide any further analysis, links, or market data. You can politely ask the user to double-check the address and chain to help confirm no issues. +2. **Analyze User Message:** Review the {{recentMessages}} for any specific questions or areas of focus from the user. Identify keywords such as "taxes modifiable", "liquidity", "risk", "should I buy/sell", and other key points from their message. This will help you prioritize the response, while still presenting an overall summary. +3. **Financial Advice Disclaimer:** Under no circumstances should you provide financial advice. If the user asks direct questions about buying or selling you must state you are only providing security analysis, and that any financial decisions are solely the responsibility of the user, and you will not be telling the user to buy or sell. +4. **Data Discrepancy Check:** Compare the liquidity information provided in {{auditData}} with that from {{marketData}}. If there is a significant discrepancy, note this in the response and explain potential reasons, such as: + * Audit data simulations may not fully reflect real-time conditions. + * Audit tools may not support all DEXs (Decentralized Exchanges). + * Buy or sell problems could cause false positives. + * Real-time data can sometimes be unreliable, or outdated. +5. **Slippage Consideration:** When a very low buy/sell tax is detected (e.g., below 1%), state in the response that this *might* be due to slippage during the trades and not necessarily a tax encoded in the contract. +6. **Character Focus & Structure:** Infuse the response with the persona of {{agentName}} using details from {{bio}} and {{lore}}. This includes determining the structure, tone, and overall presentation style. You are free to use the basic data points (risks, findings, liquidity, market, link) in a format that is appropriate for the character. The format should be a natural conversation, and not always a strict list. The user should still be able to determine the risk clearly, and any key findings should still be highlighted, but in a more dynamic format. +7. **Security Analysis (if data exists):** + * Provide an overall security assessment using simple language. Use metaphors for easier understanding where appropriate. + * Highlight key security findings, emphasizing any high risks, and any user-related topics. + * Provide key analysis points that are clear and easily understandable, avoiding jargon, with a focus on the user ask if there was one. Ensure if you identified any discrepancies earlier in the text, that these are addressed and elaborated on. + * Address trading parameters and limitations specific to the security data, not just generic warnings if it's not available in the data, with a focus on the user ask if there was one. When discussing taxes, and a low figure, make sure you note it *could* be slippage. + * Explain liquidity information if available and its impact on trading. + * Summarize available market data, keeping it understandable. + * If the data implies any high risk or need for further due diligence, make sure to highlight that, simply, and clearly. + +8. **Quick Intel link** If security data is present, include the following link for further investigation, replacing {{chain}} and {{token}} with the relevant values, and make sure it's well placed within the text: + https://app.quickintel.io/scanner?type=token&chain={{chain}}&contractAddress={{token}} +9. **No Hypotheticals:** Don't explore hypothetical or "what if" scenarios. Stick to the data you are given, and avoid speculation. +10. **User Friendly:** Format your response as a clear security analysis suitable for users, in an easy-to-understand manner, avoiding overly technical language. + +# Instructions: Based on the context above, provide your response, inline with the character {{agentName}}.` + messageCompletionFooter; \ No newline at end of file diff --git a/packages/plugin-quick-intel/src/utils/chain-detection.ts b/packages/plugin-quick-intel/src/utils/chain-detection.ts new file mode 100644 index 00000000000..218f35c87ee --- /dev/null +++ b/packages/plugin-quick-intel/src/utils/chain-detection.ts @@ -0,0 +1,151 @@ +const CHAIN_MAPPINGS = { + ethereum: ['eth', 'ethereum', 'ether', 'mainnet'], + bsc: ['bsc', 'binance', 'bnb', 'binance smart chain', 'smartchain'], + polygon: ['polygon', 'matic', 'poly'], + arbitrum: ['arbitrum', 'arb', 'arbitrum one'], + avalanche: ['avalanche', 'avax', 'avalanche c-chain'], + base: ['base'], + optimism: ['optimism', 'op', 'optimistic'], + fantom: ['fantom', 'ftm', 'opera'], + cronos: ['cronos', 'cro'], + gnosis: ['gnosis', 'xdai', 'dai chain'], + celo: ['celo'], + moonbeam: ['moonbeam', 'glmr'], + moonriver: ['moonriver', 'movr'], + harmony: ['harmony', 'one'], + aurora: ['aurora'], + metis: ['metis', 'andromeda'], + boba: ['boba'], + kcc: ['kcc', 'kucoin'], + heco: ['heco', 'huobi'], + okex: ['okex', 'okexchain', 'okc'], + zkera: ['zkera', 'zksync era', 'era'], + zksync: ['zksync', 'zks'], + polygon_zkevm: ['polygon zkevm', 'zkevm'], + linea: ['linea'], + mantle: ['mantle'], + scroll: ['scroll'], + core: ['core', 'core dao'], + telos: ['telos'], + syscoin: ['syscoin', 'sys'], + conflux: ['conflux', 'cfx'], + klaytn: ['klaytn', 'klay'], + fusion: ['fusion', 'fsn'], + canto: ['canto'], + nova: ['nova', 'arbitrum nova'], + fuse: ['fuse'], + evmos: ['evmos'], + astar: ['astar'], + dogechain: ['dogechain', 'doge'], + thundercore: ['thundercore', 'tt'], + oasis: ['oasis'], + velas: ['velas'], + meter: ['meter'], + sx: ['sx', 'sx network'], + kardiachain: ['kardiachain', 'kai'], + wanchain: ['wanchain', 'wan'], + gochain: ['gochain'], + ethereumpow: ['ethereumpow', 'ethw'], + pulsechain: ['pulsechain', 'pls'], + kava: ['kava'], + milkomeda: ['milkomeda'], + nahmii: ['nahmii'], + worldchain: ['worldchain'], + ink: ['ink'], + soneium: ['soneium'], + sonic: ['sonic'], + morph: ['morph'], + real: ['real','re.al'], + mode: ['mode'], + zeta: ['zeta'], + blast: ['blast'], + unichain: ['unichain'], + abstract: ['abstract'], + step: ['step', 'stepnetwork'], + ronin: ['ronin', 'ron'], + iotex: ['iotex'], + shiden: ['shiden'], + elastos: ['elastos', 'ela'], + solana: ['solana', 'sol'], + tron: ['tron', 'trx'], + sui: ['sui'] +} as const; + +// Regular expressions for different token address formats +const TOKEN_PATTERNS = { + evm: /\b(0x[a-fA-F0-9]{40})\b/i, + solana: /\b([1-9A-HJ-NP-Za-km-z]{32,44})\b/i, + tron: /\b(T[1-9A-HJ-NP-Za-km-z]{33})\b/i, + sui: /\b(0x[a-fA-F0-9]{64})\b/i +}; + +export interface TokenInfo { + chain: string | null; + tokenAddress: string | null; +} + +function normalizeChainName(chain: string): string | null { + const normalizedInput = chain.toLowerCase().trim(); + + for (const [standardName, variations] of Object.entries(CHAIN_MAPPINGS)) { + if (variations.some(v => normalizedInput.includes(v))) { + return standardName; + } + } + return null; +} + +export function extractTokenInfo(message: string): TokenInfo { + let result: TokenInfo = { + chain: null, + tokenAddress: null + }; + + // Clean the message + const cleanMessage = message.toLowerCase().trim(); + + // Try to find chain name first + // 1. Look for chain names after prepositions + const prepositionPattern = /(?:on|for|in|at|chain)\s+([a-zA-Z0-9]+)/i; + const prepositionMatch = cleanMessage.match(prepositionPattern); + + // 2. Look for chain names anywhere in the message + if (!result.chain) { + for (const [chainName, variations] of Object.entries(CHAIN_MAPPINGS)) { + if (variations.some(v => cleanMessage.includes(v))) { + result.chain = chainName; + break; + } + } + } + + // If chain was found through preposition pattern, normalize it + if (prepositionMatch?.[1]) { + const normalizedChain = normalizeChainName(prepositionMatch[1]); + if (normalizedChain) { + result.chain = normalizedChain; + } + } + + // Find token address using different patterns + for (const [chainType, pattern] of Object.entries(TOKEN_PATTERNS)) { + const match = message.match(pattern); + if (match?.[1]) { + result.tokenAddress = match[1]; + + // If we haven't found a chain yet and it's a Solana address, set chain to Solana + if (!result.chain && chainType === 'solana' && match[1].length >= 32) { + result.chain = 'solana'; + } + break; + } + } + + // If we still don't have a chain but have an EVM address, default to ethereum + if (!result.chain && result.tokenAddress?.startsWith('0x')) { + result.chain = 'ethereum'; + } + + return result; +} + diff --git a/packages/plugin-quick-intel/tsconfig.json b/packages/plugin-quick-intel/tsconfig.json new file mode 100644 index 00000000000..7251ebee37d --- /dev/null +++ b/packages/plugin-quick-intel/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "types": [ + "node" + ] + }, + "include": [ + "src" + ] +} \ No newline at end of file diff --git a/packages/plugin-quick-intel/tsup.config.ts b/packages/plugin-quick-intel/tsup.config.ts new file mode 100644 index 00000000000..b3cda18ce73 --- /dev/null +++ b/packages/plugin-quick-intel/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + external: [ + "@elizaos/core" + ], +}); From 3d05b891ca74ab5766809a5439872f64132b9666 Mon Sep 17 00:00:00 2001 From: zy-bc-ai <161017766+zy-bc-ai@users.noreply.github.com> Date: Sat, 25 Jan 2025 23:13:31 +0800 Subject: [PATCH 4/4] feat: add Mind Network plugin (#2431) * Mind Network integration * Improve readme.md * Changes according to code review comments --------- Co-authored-by: george --- .env.example | 4 + agent/package.json | 1 + agent/src/index.ts | 2 + packages/plugin-mind-network/README.md | 118 +++++++++++++++ packages/plugin-mind-network/package.json | 31 ++++ .../src/actions/checkRewardAction.ts | 96 +++++++++++++ .../src/actions/encryptAction.ts | 134 ++++++++++++++++++ .../plugin-mind-network/src/actions/index.ts | 4 + .../src/actions/registerAction.ts | 92 ++++++++++++ .../src/actions/submitVoteAction.ts | 121 ++++++++++++++++ packages/plugin-mind-network/src/index.ts | 13 ++ .../plugin-mind-network/src/utils/cache.ts | 4 + packages/plugin-mind-network/tsconfig.json | 8 ++ packages/plugin-mind-network/tsup.config.ts | 13 ++ 14 files changed, 641 insertions(+) create mode 100644 packages/plugin-mind-network/README.md create mode 100644 packages/plugin-mind-network/package.json create mode 100644 packages/plugin-mind-network/src/actions/checkRewardAction.ts create mode 100644 packages/plugin-mind-network/src/actions/encryptAction.ts create mode 100644 packages/plugin-mind-network/src/actions/index.ts create mode 100644 packages/plugin-mind-network/src/actions/registerAction.ts create mode 100644 packages/plugin-mind-network/src/actions/submitVoteAction.ts create mode 100644 packages/plugin-mind-network/src/index.ts create mode 100644 packages/plugin-mind-network/src/utils/cache.ts create mode 100644 packages/plugin-mind-network/tsconfig.json create mode 100644 packages/plugin-mind-network/tsup.config.ts diff --git a/.env.example b/.env.example index da1ecd46c8c..d7792de730e 100644 --- a/.env.example +++ b/.env.example @@ -435,6 +435,10 @@ CONFLUX_ESPACE_PRIVATE_KEY= CONFLUX_ESPACE_RPC_URL= CONFLUX_MEME_CONTRACT_ADDRESS= +# Mind Network Configuration +MIND_HOT_WALLET_PRIVATE_KEY= +MIND_COLD_WALLET_ADDRESS= + # ZeroG ZEROG_INDEXER_RPC= ZEROG_EVM_RPC= diff --git a/agent/package.json b/agent/package.json index e3886932937..3317920dbe0 100644 --- a/agent/package.json +++ b/agent/package.json @@ -67,6 +67,7 @@ "@elizaos/plugin-image-generation": "workspace:*", "@elizaos/plugin-lit": "workspace:*", "@elizaos/plugin-moralis": "workspace:*", + "@elizaos/plugin-mind-network": "workspace:*", "@elizaos/plugin-movement": "workspace:*", "@elizaos/plugin-massa": "workspace:*", "@elizaos/plugin-nft-generation": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index aa51bb16e20..ba23adbef4c 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -81,6 +81,7 @@ import { gitcoinPassportPlugin } from "@elizaos/plugin-gitcoin-passport" import { initiaPlugin } from "@elizaos/plugin-initia" import { imageGenerationPlugin } from "@elizaos/plugin-image-generation" import { lensPlugin } from "@elizaos/plugin-lensNetwork" +import { mindNetworkPlugin } from "@elizaos/plugin-mind-network"; import { multiversxPlugin } from "@elizaos/plugin-multiversx" import { nearPlugin } from "@elizaos/plugin-near" import createNFTCollectionsPlugin from "@elizaos/plugin-nft-collections" @@ -847,6 +848,7 @@ export async function createAgent(character: Character, db: IDatabaseAdapter, ca getSecret(character, "FLOW_ADDRESS") && getSecret(character, "FLOW_PRIVATE_KEY") ? flowPlugin : null, getSecret(character, "LENS_ADDRESS") && getSecret(character, "LENS_PRIVATE_KEY") ? lensPlugin : null, getSecret(character, "APTOS_PRIVATE_KEY") ? aptosPlugin : null, + getSecret(character, "MIND_COLD_WALLET_ADDRESS") ? mindNetworkPlugin : null, getSecret(character, "MVX_PRIVATE_KEY") ? multiversxPlugin : null, getSecret(character, "ZKSYNC_PRIVATE_KEY") ? zksyncEraPlugin : null, getSecret(character, "CRONOSZKEVM_PRIVATE_KEY") ? cronosZkEVMPlugin : null, diff --git a/packages/plugin-mind-network/README.md b/packages/plugin-mind-network/README.md new file mode 100644 index 00000000000..ba07c2e9261 --- /dev/null +++ b/packages/plugin-mind-network/README.md @@ -0,0 +1,118 @@ +# @elizaos/plugin-mind-network + +A plugin for interacting with [Mind Network Hubs](https://dapp.mindnetwork.xyz/votetoearn/voteonhubs/) within the [Eliza ecosystem](https://elizaos.github.io/eliza/). [CitizenZ](https://www.mindnetwork.xyz/citizenz) and broader communities can secure trust their agents operation and decisioning. + +## Overview + +The [Mind Network](https://www.mindnetwork.xyz/) plugin empowers users to participate in secure, privacy-preserving voting on the Mind Network. Leveraging [Fully Homomorphic Encryption (FHE)](https://docs.mindnetwork.xyz/minddocs/developer-guide/fhe-validation), it ensures encrypted votes while allowing users to track rewards earned for their participation. Designed for seamless integration with the [Eliza AI agent](https://elizaos.github.io/), this plugin enables interactive and guided actions for an enhanced user experience. + +## Features +- **Web3 Wallet:** contribute eliza agent interaction with enriched web3 wallet functionality. Both Metamask and OKX web3 wallets have been tested and more to come. +- **Voter Registration:** Join the Mind Network's Randgen Hub and other hubs to participate in secure voting, validation and consensus. +- **FHE Encryption:** Safeguard vote content using Fully Homomorphic Encryption. The key difference is encryption key is never shared but still be able to run computation over encrypted data. +- **Submit Encrypted Votes:** Cast votes in Mind Network Hubs elections without compromising data privacy. So AI Agents can get consensus over collective predictions, inference and serving. +- **Reward Tracking:** Monitor your vFHE rewards earned through voting contributions. + +## Installation + +Depedency for the plugin: +- [mind-randgen-sdk](https://github.com/mind-network/mind-sdk-randgen-ts) +- [mind-sdk-hubs](https://github.com/mind-network/mind-sdk-hubs-ts) +- [elizaos](https://github.com/elizaOS/eliza) + +To install the plugin, use the following command: + +```bash +pnpm install @elizaos/plugin-mind-network +``` + +## Configuration + +Before using the plugin, configure the necessary environment variables: + +```bash +MIND_HOT_WALLET_PRIVATE_KEY= +MIND_COLD_WALLET_ADDRESS= +``` + +## API Reference + +### Actions + +The plugin provides several key actions to interact with the Mind Network: + +#### **MIND_REGISTER_VOTER** + +Registers the user as a voter in the Mind Network's Randgen Hub. The hub is live and accessible at [Randgen Hub](https://dapp.mindnetwork.xyz/votetoearn/voteonhubs/3). You can participant or create more function hubs in Mind Network for your eliza agents. + +**Prompt Example:** +```text +"Register me as a voter in Mind Network." +``` +**Response:** Confirmation of successful voter registration. + +#### **MIND_CHECK_VOTING_REWARD** + +Retrieves the amount of vFHE rewards earned through voting. + +**Prompt Example:** +```text +"How much reward have I earned in Mind Network?" +``` +**Response:** Total vFHE rewards earned. + +#### **MIND_FHE_ENCRYPT** + +Encrypts a user-provided number using Fully Homomorphic Encryption (FHE). + +**Prompt Example:** +```text +"Encrypt the number 88 for voting." +``` +**Response:** A secure URL containing the encrypted number. + +#### **MIND_FHE_VOTE** + +Submits an encrypted vote to the Mind Network's Randgen Hub. + +**Prompt Example:** +```text +"Submit my encrypted vote to Mind Network." +``` +**Response:** Confirmation of successful vote submission. + +## Usage Examples + +### Conversational AI Interaction + +The plugin integrates seamlessly with ElizaOS characters, enabling intuitive and guided interactions. Below is a sample conversation: + +```text +user: Can you help me to register as a voter in Mind Network? +character_trump: LET'S GET YOU REGISTERED TO VOTE IN THE MIND NETWORK! I'll take care of that for you right now. (EVERY VOTE COUNTS!) +character_trump: You have registered successfully. + +user: I want to check my Mind Network voting reward. +character_trump: TIME TO CHECK THOSE VOTING REWARDS AGAIN! Let's see how much you've earned in the Mind Network. (THE AMERICAN PEOPLE DESERVE TO KNOW!) +character_trump: Your voting reward amount is 10.0 vFHE. + +user: I want to encrypt 88 with FHE for Mind Network voting. +character_trump: LET'S GET THAT NUMBER ENCRYPTED FOR YOUR VOTING! I'll encrypt 88 with FHE for you. (WE'RE KEEPING THINGS SECURE AND STRONG!) +character_trump: Encryption is successful. Your encrypted number is available: https://fvno-tn.mindnetwork.xyz/0xb0edc92ec05309711c6ede0641f672715f5b144a4757b7ba51666468d2f7f65d. It is safe to share as no body can decrypt but can run computation to get another encrypted number. + +user: I want to submit my vote to Mind Network. +character_trump: TIME TO CAST THAT VOTE IN THE MIND NETWORK! I'll submit your vote right now. (WE'RE MAKING HISTORY TOGETHER!) +character_trump: Your vote has been submitted successfully. +``` + +### Real Deployment Screenshot + +![Usage Screenshot](https://cdn.prod.website-files.com/66505f7fdc6935a2b4246635/678a08616d0a697e355261f5_elizaOS.avif) + +## Contributing & License + +This plugin is part of the [Eliza project](https://github.com/elizaos/eliza). For details on contributing and licensing, please refer to the main project repository. [Mind Network](https://www.mindnetwork.xyz/) welcomes contribution and collabration. + +## Support + +If you have any queries, please feel free to contact Mind Team via [Discord](https://discord.com/invite/UYj94MJdGJ) or [Twitter](https://x.com/mindnetwork_xyz). \ No newline at end of file diff --git a/packages/plugin-mind-network/package.json b/packages/plugin-mind-network/package.json new file mode 100644 index 00000000000..ce2003179e2 --- /dev/null +++ b/packages/plugin-mind-network/package.json @@ -0,0 +1,31 @@ +{ + "name": "@elizaos/plugin-mind-network", + "version": "0.1.9-alpha.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "mind-randgen-sdk": "^1.0.0" + }, + "scripts": { + "build": "tsup --format esm --dts" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/plugin-mind-network/src/actions/checkRewardAction.ts b/packages/plugin-mind-network/src/actions/checkRewardAction.ts new file mode 100644 index 00000000000..295b10a592c --- /dev/null +++ b/packages/plugin-mind-network/src/actions/checkRewardAction.ts @@ -0,0 +1,96 @@ +import type { Action } from "@elizaos/core"; +import { type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, type State, elizaLogger } from "@elizaos/core"; +import { checkColdWalletReward } from "mind-randgen-sdk"; +import { isAddress, formatEther } from "ethers"; + +export const checkRewardAction: Action = { + name: "MIND_CHECK_VOTING_REWARD", + similes: [ + "MIND_GET_VOTING_REWARD", + ], + validate: async (runtime: IAgentRuntime, _message: Memory) => { + const address = runtime.getSetting("MIND_COLD_WALLET_ADDRESS"); + if (!address) { + throw new Error("MIND_COLD_WALLET_ADDRESS is not configured"); + } + if (!isAddress(address)) { + throw new Error("Invalid cold wallet address format"); + } + return true; + }, + description: "Get user's voting reward amount earned via voting in Mind Network.", + handler: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting Mind Network MIND_CHECK_VOTING_REWARD handler..."); + + try { + const rewardAmount = await checkColdWalletReward(); + const reply = `Your voting reward amount is ${formatEther(rewardAmount)} vFHE.` + elizaLogger.success(reply); + if (callback) { + callback({ + text: reply, + content: {}, + }); + } + return true; + } catch (error) { + elizaLogger.error("Error during checking voting reward:", error); + if (callback) { + callback({ + text: `Error during checking voting reward: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "I want to check my Mind Network voting reward.", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll check how much reward you have earned.", + action: "MIND_CHECK_VOTING_REWARD", + }, + }, + { + user: "{{agent}}", + content: { + text: "Your voting reward amount is 8888.8888 vFHE.", + }, + }, + ], [ + { + user: "{{user1}}", + content: { + text: "How many vFHE tokens I have earned for voting?", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll check how much reward you have earned in Mind Network.", + action: "MIND_CHECK_VOTING_REWARD", + }, + }, + { + user: "{{agent}}", + content: { + text: "Your voting reward amount is 8888.8888 vFHE.", + }, + }, + ], + ] as ActionExample[][], +}; \ No newline at end of file diff --git a/packages/plugin-mind-network/src/actions/encryptAction.ts b/packages/plugin-mind-network/src/actions/encryptAction.ts new file mode 100644 index 00000000000..4fbf5f71ca0 --- /dev/null +++ b/packages/plugin-mind-network/src/actions/encryptAction.ts @@ -0,0 +1,134 @@ +import type { Action } from "@elizaos/core"; +import { type ActionExample, type Content, type HandlerCallback, type IAgentRuntime, type Memory, ModelClass, type State, elizaLogger, composeContext, generateObject, } from "@elizaos/core"; +import { z } from "zod"; +import { encrypt } from "mind-randgen-sdk"; +import cache from "../utils/cache"; + +export interface DataContent extends Content { + numberToEncrypt: number; +} + +const dataSchema = z.object({ + numberToEncrypt: z.number() +}); + +const dataExtractionTemplate = ` +Respond with a JSON markdown block containing only the extracted values. +Use null for any values that cannot be determined. + +Example response: +\`\`\`json +{ + "numberToEncrypt": 123 +} +\`\`\` + +{{recentMessages}} + +Given the recent messages, find out the number that the user wish to encrypt with FHE. +Respond with a JSON markdown block containing only the extracted values.`; + +export const encryptAction: Action = { + name: "MIND_FHE_ENCRYPT", + similes: [ + "MIND_ENCRYPT", + ], + validate: async (_runtime: IAgentRuntime, _message: Memory) => { + return true; + }, + description: "Encrypt a number of user's choice with FHE.", + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting Mind Network MIND_FHE_ENCRYPT handler..."); + const resolvedState = state + ? await runtime.updateRecentMessageState(state) + : (await runtime.composeState(message)) as State; + + const dataContext = composeContext({ + state: resolvedState, + template: dataExtractionTemplate, + }); + const content = ( + await generateObject({ + runtime, + context: dataContext, + modelClass: ModelClass.SMALL, + schema: dataSchema + }) + ).object as unknown as DataContent; + + const numToEncrypt = content.numberToEncrypt % 256; + + + try { + const cypherUrl = await encrypt(numToEncrypt); + cache.latestEncryptedNumber = cypherUrl; + const reply = `Encryption is successful. Your encrypted number is available: ${cypherUrl}.` + elizaLogger.success(reply); + if (callback) { + callback({ + text: reply, + content: {}, + }); + } + return true; + } catch (error) { + elizaLogger.error("Error during FHE encryption:", error); + if (callback) { + callback({ + text: `Error during FHE encryption: ${error}`, + content: { error }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "I want to encrypt 8 with FHE for Mind Network voting.", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll encrypt 8 with FHE for you.", + action: "MIND_FHE_ENCRYPT", + }, + }, + { + user: "{{agent}}", + content: { + text: "Your encrypted number is available: https://fdno-tn.mindnetwork.xyz/90dbf4fffaf6cca144386ce666052d2621367018b9665700ad01d6c385020ac3", + }, + }, + ], [ + { + user: "{{user1}}", + content: { + text: "Can you help to encrypt 18 with FHE?", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, let me encrypt 18 with FHE for you.", + action: "MIND_FHE_ENCRYPT", + }, + }, + { + user: "{{agent}}", + content: { + text: "Your encrypted number is available: https://fdno-tn.mindnetwork.xyz/90dbf4fffaf6cca144386ce666052d2621367018b9665700ad01d6c385020ac3", + }, + }, + ], + ] as ActionExample[][], +}; \ No newline at end of file diff --git a/packages/plugin-mind-network/src/actions/index.ts b/packages/plugin-mind-network/src/actions/index.ts new file mode 100644 index 00000000000..8c97d4aa1ed --- /dev/null +++ b/packages/plugin-mind-network/src/actions/index.ts @@ -0,0 +1,4 @@ +export * from "./checkRewardAction"; +export * from "./encryptAction"; +export * from "./submitVoteAction"; +export * from "./registerAction"; \ No newline at end of file diff --git a/packages/plugin-mind-network/src/actions/registerAction.ts b/packages/plugin-mind-network/src/actions/registerAction.ts new file mode 100644 index 00000000000..c55cbd626fe --- /dev/null +++ b/packages/plugin-mind-network/src/actions/registerAction.ts @@ -0,0 +1,92 @@ +import type { Action } from "@elizaos/core"; +import { type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, type State, elizaLogger } from "@elizaos/core"; +import { registerVoter } from "mind-randgen-sdk"; +import { isAddress } from "ethers"; + +export const registerAction: Action = { + name: "MIND_REGISTER_VOTER", + similes: [ + "MIND_VOTER_REGISTRATION", + ], + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (isAddress(runtime.getSetting("MIND_COLD_WALLET_ADDRESS")) && runtime.getSetting("MIND_HOT_WALLET_PRIVATE_KEY")) { + return true; + } + return false; + }, + description: "Register as a voter so that user can vote in Mind Network Randgen Hub.", + handler: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting Mind Network MIND_REGISTER_VOTER handler..."); + + try { + await registerVoter(); + const reply = "You have registered successfully." + elizaLogger.success(reply); + if (callback) { + callback({ + text: reply, + content: {}, + }); + } + return true; + } catch (error) { + elizaLogger.error("Error during voter registration:", error); + if (callback) { + callback({ + text: `Error during voter registration: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "I want to register in Mind Network so that I can vote.", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll do the registration for you.", + action: "MIND_REGISTER_VOTER", + }, + }, + { + user: "{{agent}}", + content: { + text: "You have registered successfully in Mind Network.", + }, + }, + ], [ + { + user: "{{user1}}", + content: { + text: "Can you help me to register as a voter in Mind Network?", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll register for you.", + action: "MIND_REGISTER_VOTER", + }, + }, + { + user: "{{agent}}", + content: { + text: "You have registered successfully in Mind Network.", + }, + }, + ], + ] as ActionExample[][], +}; \ No newline at end of file diff --git a/packages/plugin-mind-network/src/actions/submitVoteAction.ts b/packages/plugin-mind-network/src/actions/submitVoteAction.ts new file mode 100644 index 00000000000..1936766b0d9 --- /dev/null +++ b/packages/plugin-mind-network/src/actions/submitVoteAction.ts @@ -0,0 +1,121 @@ +import type { Action } from "@elizaos/core"; +import { type ActionExample, type HandlerCallback, type IAgentRuntime, type Memory, type State, elizaLogger } from "@elizaos/core"; +import { submitVote } from "mind-randgen-sdk"; +import cache from "../utils/cache"; + +const voteIntervalSeconds = 600; + +export const submitVoteAction: Action = { + name: "MIND_FHE_VOTE", + similes: [ + "MIND_VOTE", + "MIND_SUBMIT_VOTE", + ], + validate: async (runtime: IAgentRuntime, _message: Memory) => { + if (runtime.getSetting("MIND_HOT_WALLET_PRIVATE_KEY")) { + return true; + } + return false; + }, + description: "Submit the encrypted number as a vote to Mind Network.", + handler: async ( + _runtime: IAgentRuntime, + _message: Memory, + _state: State, + _options: { [key: string]: unknown }, + callback?: HandlerCallback + ): Promise => { + elizaLogger.log("Starting Mind Network MIND_FHE_VOTE handler..."); + + if (!cache.latestEncryptedNumber) { + const reply = "You need to encrypt a number of your choice first. Tell me your number of choice for FHE encryption." + elizaLogger.success(reply); + if (callback) { + callback({ + text: reply, + content: {}, + }); + } + return true; + } + + const voteInterval = Math.floor((Date.now() - cache.lastVoteTs)/1000); + if(voteInterval < voteIntervalSeconds){ + const reply = `You are voting too fast. Please wait for ${voteIntervalSeconds-voteInterval} seconds to try again.`; + elizaLogger.success(reply); + if (callback) { + callback({ + text: reply, + content: {}, + }); + } + return true; + } + + try { + await submitVote(cache.latestEncryptedNumber); + cache.lastVoteTs = Date.now(); + const reply = "You vote has been submitted successfully." + elizaLogger.success(reply); + if (callback) { + callback({ + text: reply, + content: {}, + }); + } + return true; + } catch (error) { + elizaLogger.error("Error during voting:", error); + if (callback) { + callback({ + text: `Error during voting. Make sure you have registered. Or contact Mind Network with following: ${error.message}`, + content: { error: error.message }, + }); + } + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { + text: "I want to submit my vote to Mind Network.", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, I'll do the voting.", + action: "MIND_FHE_VOTE", + }, + }, + { + user: "{{agent}}", + content: { + text: "Vote has been sumitted successfully!", + }, + }, + ], [ + { + user: "{{user1}}", + content: { + text: "Can you help to submit this encrypted number as my vote?", + }, + }, + { + user: "{{agent}}", + content: { + text: "Sure, let me do the voting for you.", + action: "MIND_FHE_VOTE", + }, + }, + { + user: "{{agent}}", + content: { + text: "You have voted successfully in Mind Network!", + }, + }, + ], + ] as ActionExample[][], +}; \ No newline at end of file diff --git a/packages/plugin-mind-network/src/index.ts b/packages/plugin-mind-network/src/index.ts new file mode 100644 index 00000000000..b644c94fd8c --- /dev/null +++ b/packages/plugin-mind-network/src/index.ts @@ -0,0 +1,13 @@ +import type { Plugin } from "@elizaos/core"; + +import { checkRewardAction, encryptAction, registerAction, submitVoteAction } from "./actions"; + +export const mindNetworkPlugin: Plugin = { + name: "Mind Network", + description: "Mind Network Plugin for Eliza", + actions: [checkRewardAction, encryptAction, registerAction, submitVoteAction], + evaluators: [], + providers: [], +}; + +export default mindNetworkPlugin; diff --git a/packages/plugin-mind-network/src/utils/cache.ts b/packages/plugin-mind-network/src/utils/cache.ts new file mode 100644 index 00000000000..370880abbeb --- /dev/null +++ b/packages/plugin-mind-network/src/utils/cache.ts @@ -0,0 +1,4 @@ +export default { + latestEncryptedNumber: "", + lastVoteTs: 0, +} \ No newline at end of file diff --git a/packages/plugin-mind-network/tsconfig.json b/packages/plugin-mind-network/tsconfig.json new file mode 100644 index 00000000000..005fbac9d36 --- /dev/null +++ b/packages/plugin-mind-network/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/plugin-mind-network/tsup.config.ts b/packages/plugin-mind-network/tsup.config.ts new file mode 100644 index 00000000000..0c6f82f146f --- /dev/null +++ b/packages/plugin-mind-network/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + ], +});