#
tokens: 22730/50000 11/11 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .github
│   └── workflows
│       └── python-publish.yml
├── armor_crypto_mcp
│   ├── __init__.py
│   ├── armor_client.py
│   └── armor_mcp.py
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── pyproject.toml
├── README_prompts.md
├── README.md
├── requirements.txt
└── smithery.yaml
```

# Files

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Armor Crypto MCP
  2 | *Alpha Test version 0.1.24*
  3 | 
  4 | A single source for integrating AI Agents with the Crypto ecosystem. This includes Wallet creation and management, swaps, transfers, event-based trades like DCA, stop loss and take profit, and much more. The Armor MCP supports Solana in Alpha and, when in beta, will support more than a dozen blockchains, including Ethereum. Base, Avalanche, Bitcoin, Sui, Berachain, megaETH, Optimism, Ton, BNB, and Arbitrum, among others. Using Armor's MCP you can bring all of crypto into your AI Agent with unified logic and a complete set of tools.
  5 |        
  6 | ![Armor MCP](https://armor-assets-repository.s3.nl-ams.scw.cloud/MCP_sm.png)
  7 | <br />
  8 | <br />
  9 | <br />
 10 | <br />
 11 | <br />
 12 | <br />
 13 | # Features
 14 | 
 15 | 🧠 AI Native
 16 | 
 17 | 📙 Wallet Management
 18 | 
 19 | 🔃 Swaps
 20 | 
 21 | 🌈 Specialized trades (DCA, Stop Loss etc.)
 22 | 
 23 | ⛓️ Multi-chain
 24 | 
 25 | ↔️ Cross-chain transations
 26 | 
 27 | 🥩 Staking
 28 | 
 29 | 🤖 Fast intergration to Agentic frameworks
 30 | 
 31 | 👫 Social Sentiment
 32 | 
 33 | 🔮 Prediction
 34 | <br />
 35 | <br />
 36 | ![Armor MCP Diagram](https://armor-assets-repository.s3.nl-ams.scw.cloud/amor_mcp_diagram.png)
 37 | <br />
 38 | <br />
 39 | <br />
 40 | <br />
 41 | <br />
 42 | <br />
 43 | 
 44 | # Requirements
 45 | 
 46 | ### 1. Make sure you have python installed
 47 | <br />
 48 | 
 49 | ### 2. Install `uv`
 50 | *Linux / Windows*
 51 | 
 52 | ```sh
 53 | pip install uv
 54 | ```
 55 | *Mac*
 56 | 
 57 | ```sh
 58 | brew install uv
 59 | ```
 60 | <br />
 61 | 
 62 | ### 3. Claude Desktop or your AI Agent will run the MCP
 63 | See [Usage & Configuration](#usage--configuration) for details.
 64 | <br />
 65 | <br />
 66 | <br />
 67 | <br />
 68 | <br />
 69 | 
 70 | # Alpha Testing
 71 | 
 72 | We are currently in pre-alpha, and we are testing the capabilities of various agents and agentic frameworks like Claude Desktop, Cline, Cursor, n8n, etc. 
 73 | 
 74 | ## Current Features & Tools
 75 | - Wallet Management
 76 |     - Grouping & Organization
 77 |     - Archiving
 78 | - Swap & Trades
 79 |     - Normal swap
 80 |     - DCA (place / list / cancel)
 81 |     - Scheduled Orders
 82 |     - Limit Orders (place / list / cancel)
 83 | - Staking and Unstaking
 84 | - Token Search and Trending Tokens
 85 | - Statistical Calculator for accurate Analysis
 86 | - Supports Solana blockchain
 87 | 
 88 | ## Coming Soon
 89 | - More Blockchain Support
 90 | - Minting
 91 | - Armor Agents as a Tool (or A2A)
 92 | 
 93 | ## MCP Setup
 94 | Currently you need to have the Armor NFT to get an API Key.
 95 | Get it [here](https://codex.armorwallet.ai/)
 96 | 
 97 | ## Usage & Configuration
 98 | To use the Armor MCP with your agent, you need the following configuration, replace `<PUT-YOUR-KEY-HERE>` with your API key:
 99 | ```json
100 | {
101 |   "mcpServers": {
102 |     "armor-crypto-mcp": {
103 |       "command": "uvx",
104 |       "args": ["armor-crypto-mcp@latest", "--version"],
105 |       "env": {
106 |         "ARMOR_API_KEY": "<PUT-YOUR-KEY-HERE>"
107 |       }
108 |     }
109 |   }
110 | }
111 | ```
112 | <br />
113 | <br />
114 | <br />
115 | <br />
116 | <br />
117 | <br />
118 | 
119 | # Use in Claude Desktop
120 | 1. Must have Developer Mode enabled
121 | 2. Open Claude Desktop's File Menu top left of the window.
122 | 3. Go to File > Settings
123 | 4. Under Developer, click Edit Configuration
124 | 5. In the config file, insert the `armor-wallet-mcp` section from above
125 | 6. Make sure to replace the placeholder with your API key
126 | 7. Save the file and start a new Chat in Claude Desktop
127 | 
128 | ## Use in Cline
129 | 1. Click on the `MCP Servers` button in the Cline tab in VSCode on the left panel
130 | 2. Scroll to the bottom of the left panel and click on `Configure MCP Servers`
131 | 3. In the config file, insert `armor-wallet-mcp` section from above
132 | 4. Make sure to replace the placeholder with your API key
133 | 5. Save the file, click `Done` under the `MCP Servers` tab and start chatting with Cline
134 | 
135 | ## Use in n8n
136 | 1. Open the n8n app
137 | 2. Bottom-left of screen click `...` next to your username and click `Settings`
138 | 3. On the left panel, click `Community nodes` and then `Install a Community Node` button
139 | 4. In the search field for `npm Package Name` type in *mcp*
140 | 5. Install `MCP Nodes`
141 | 6. Add any MCP node, for example: `List Tools`
142 | 7. In the MCP Client `Parameters` tab, click `Select Credential` and click `Create new credential`
143 | 8. Under `Command` enter `uvx`
144 | 9. Under `Arguments` enter `armor-crypto-mcp`
145 | 10. Under `Environments` enter `ARMOR_API_KEY=eyJhbGciOiJIUzI1NiIsIn...` paste the full API Key value after the `=`
146 | 11. Back in the `Parameters` tab you can choose the MCP `Operation` for that Node
147 | <br />
148 | <br />
149 | <br />
150 | <br />
151 | <br />
152 | <br />
153 | 
154 | # Using Armor MCP
155 | 
156 | Once you have setup the Armor MCP [here are some prompts you can use to get started](https://github.com/armorwallet/armor-crypto-mcp/blob/main/README_prompts.md)
157 | <br />
158 | <br />
159 | <br />
160 | 
```

--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Contributor Covenant Code of Conduct
  2 | 
  3 | ## Our Pledge
  4 | 
  5 | We as members, contributors, and leaders pledge to make participation in our
  6 | community a harassment-free experience for everyone, regardless of age, body
  7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
  8 | identity and expression, level of experience, education, socio-economic status,
  9 | nationality, personal appearance, race, caste, color, religion, or sexual
 10 | identity and orientation.
 11 | 
 12 | We pledge to act and interact in ways that contribute to an open, welcoming,
 13 | diverse, inclusive, and healthy community.
 14 | 
 15 | ## Our Standards
 16 | 
 17 | Examples of behavior that contributes to a positive environment for our
 18 | community include:
 19 | 
 20 | - Demonstrating empathy and kindness toward other people
 21 | - Being respectful of differing opinions, viewpoints, and experiences
 22 | - Giving and gracefully accepting constructive feedback
 23 | - Accepting responsibility and apologizing to those affected by our mistakes,
 24 |   and learning from the experience
 25 | - Focusing on what is best not just for us as individuals, but for the overall
 26 |   community
 27 | 
 28 | Examples of unacceptable behavior include:
 29 | 
 30 | - The use of sexualized language or imagery, and sexual attention or advances of
 31 |   any kind
 32 | - Trolling, insulting or derogatory comments, and personal or political attacks
 33 | - Public or private harassment
 34 | - Publishing others' private information, such as a physical or email address,
 35 |   without their explicit permission
 36 | - Other conduct which could reasonably be considered inappropriate in a
 37 |   professional setting
 38 | 
 39 | ## Enforcement Responsibilities
 40 | 
 41 | Community leaders are responsible for clarifying and enforcing our standards of
 42 | acceptable behavior and will take appropriate and fair corrective action in
 43 | response to any behavior that they deem inappropriate, threatening, offensive,
 44 | or harmful.
 45 | 
 46 | Community leaders have the right and responsibility to remove, edit, or reject
 47 | comments, commits, code, wiki edits, issues, and other contributions that are
 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
 49 | decisions when appropriate.
 50 | 
 51 | ## Scope
 52 | 
 53 | This Code of Conduct applies within all community spaces, and also applies when
 54 | an individual is officially representing the community in public spaces.
 55 | Examples of representing our community include using an official email address,
 56 | posting via an official social media account, or acting as an appointed
 57 | representative at an online or offline event.
 58 | 
 59 | ## Enforcement
 60 | 
 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
 62 | reported to the community leaders responsible for enforcement at
 63 | [email protected].
 64 | All complaints will be reviewed and investigated promptly and fairly.
 65 | 
 66 | All community leaders are obligated to respect the privacy and security of the
 67 | reporter of any incident.
 68 | 
 69 | ## Enforcement Guidelines
 70 | 
 71 | Community leaders will follow these Community Impact Guidelines in determining
 72 | the consequences for any action they deem in violation of this Code of Conduct:
 73 | 
 74 | ### 1. Correction
 75 | 
 76 | **Community Impact**: Use of inappropriate language or other behavior deemed
 77 | unprofessional or unwelcome in the community.
 78 | 
 79 | **Consequence**: A private, written warning from community leaders, providing
 80 | clarity around the nature of the violation and an explanation of why the
 81 | behavior was inappropriate. A public apology may be requested.
 82 | 
 83 | ### 2. Warning
 84 | 
 85 | **Community Impact**: A violation through a single incident or series of
 86 | actions.
 87 | 
 88 | **Consequence**: A warning with consequences for continued behavior. No
 89 | interaction with the people involved, including unsolicited interaction with
 90 | those enforcing the Code of Conduct, for a specified period of time. This
 91 | includes avoiding interactions in community spaces as well as external channels
 92 | like social media. Violating these terms may lead to a temporary or permanent
 93 | ban.
 94 | 
 95 | ### 3. Temporary Ban
 96 | 
 97 | **Community Impact**: A serious violation of community standards, including
 98 | sustained inappropriate behavior.
 99 | 
100 | **Consequence**: A temporary ban from any sort of interaction or public
101 | communication with the community for a specified period of time. No public or
102 | private interaction with the people involved, including unsolicited interaction
103 | with those enforcing the Code of Conduct, is allowed during this period.
104 | Violating these terms may lead to a permanent ban.
105 | 
106 | ### 4. Permanent Ban
107 | 
108 | **Community Impact**: Demonstrating a pattern of violation of community
109 | standards, including sustained inappropriate behavior, harassment of an
110 | individual, or aggression toward or disparagement of classes of individuals.
111 | 
112 | **Consequence**: A permanent ban from any sort of public interaction within the
113 | community.
114 | 
115 | ## Attribution
116 | 
117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118 | version 2.1, available at
119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
120 | 
121 | Community Impact Guidelines were inspired by
122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
123 | 
124 | For answers to common questions about this code of conduct, see the FAQ at
125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
126 | [https://www.contributor-covenant.org/translations][translations].
127 | 
128 | [homepage]: https://www.contributor-covenant.org
129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
130 | [Mozilla CoC]: https://github.com/mozilla/diversity
131 | [FAQ]: https://www.contributor-covenant.org/faq
132 | [translations]: https://www.contributor-covenant.org/translations
133 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
1 | python-dotenv>=1.0.0
2 | requests>=2.31.0
3 | mcp>=1.0.0
4 | uvicorn>=0.32.1
5 | httpx
```

--------------------------------------------------------------------------------
/armor_crypto_mcp/__init__.py:
--------------------------------------------------------------------------------

```python
 1 | """
 2 | Armor MCP Package
 3 | 
 4 | This package provides an agentic interface for interoperating with multiple blockchains,
 5 | staking, DeFi operations, swaps, bridging, wallet management, and for developing crypto
 6 | trading strategies through dynamic DCA.
 7 | """
 8 | 
 9 | __version__ = "0.2.1"
10 | 
11 | 
12 | 
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
 2 | FROM python:3.11-slim
 3 | 
 4 | WORKDIR /app
 5 | 
 6 | # Copy local code to the container. We assume the MCP base path contains the project files.
 7 | COPY . /app
 8 | 
 9 | # Install dependencies using pip. Use pip install . to install the package defined by pyproject.toml.
10 | RUN pip install --no-cache-dir .
11 | 
12 | # Expose port if necessary (optional)
13 | EXPOSE 8000
14 | 
15 | # Run the MCP server. The entry point is defined by the package's script, so we use the installed command.
16 | CMD ["armor-crypto-mcp"]
17 | 
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "armor-crypto-mcp"
 3 | version = "0.2.1"
 4 | description = "MCP to interface with multiple blockchains, staking, DeFi, swap, bridging, wallet management, DCA, Limit Orders, Coin Lookup, Tracking and more"
 5 | readme = "README.md"
 6 | requires-python = ">=3.11"
 7 | dependencies = [
 8 |  "mcp>=1.1.0",
 9 |  "python-dotenv>=1.0.0",
10 |  "requests>=2.31.0",
11 |  "uvicorn>=0.32.1",
12 |  "httpx"
13 | ]
14 | [[project.authors]]
15 | name = "Armor Wallet"
16 | email = "[email protected]"
17 | 
18 | [build-system]
19 | requires = [ "hatchling",]
20 | build-backend = "hatchling.build"
21 | 
22 | [project.scripts]
23 | armor-crypto-mcp = "armor_crypto_mcp.armor_mcp:main"
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - armorApiKey
10 |     properties:
11 |       armorApiKey:
12 |         type: string
13 |         description: API Key for Armor API authentication.
14 |       armorApiUrl:
15 |         type: string
16 |         default: https://app.armorwallet.ai/api/v1
17 |         description: The base URL for the Armor API.
18 |   commandFunction:
19 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
20 |     |-
21 |     (config) => ({
22 |       command: 'armor-crypto-mcp',
23 |       args: [],
24 |       env: {
25 |         ARMOR_API_KEY: config.armorApiKey,
26 |         ARMOR_API_URL: config.armorApiUrl || 'https://app.armorwallet.ai/api/v1'
27 |       }
28 |     })
29 |   exampleConfig:
30 |     armorApiKey: example_api_key
31 |     armorApiUrl: https://app.armorwallet.ai/api/v1
32 | 
```

--------------------------------------------------------------------------------
/README_prompts.md:
--------------------------------------------------------------------------------

```markdown
 1 | # Example Prompts for Armor MCP
 2 | 
 3 | ## How Armor Crypto MCP Might be Used
 4 | Here are some example prompts that we have been testing with our internal Armor Agents, but could use testing with other agents. Think of this MCP as the bridge to a large number of cryptocurrency ecosystems. Each tool we provide your agent can be combined with other tools to form powerful chains of action.
 5 | 
 6 | ### General Questions
 7 | ```
 8 | What can I do in Armor?
 9 | ```
10 | ```
11 | What tools does Armor make available?
12 | ```
13 | 
14 | ### Wallet Management
15 | - Creating wallets
16 | ```
17 | Create a wallet named test2 and transfer 0.2 SOL to it from test1
18 | ```
19 | - Wallet organization
20 | ```
21 | Put wallets test1 and test2 into a new group called testing
22 | ```
23 | ```
24 | List my wallet groups
25 | ```
26 | - Archiving wallets
27 | ```
28 | Move all of my assets from test3 to test1 and archive test3
29 | ```
30 | 
31 | ### DCA and Swaps
32 | - Simple DCA
33 | ```
34 | DCA into SOL from 20% of my USDc
35 | ```
36 | - Specific DCA
37 | ```
38 | Buy SOL with all of my USD in test1 wallet over a period of 3 months, place the orders at midnight every monday and thursday
39 | ```
40 | - Placing Orders
41 | ```
42 | Buy 0.12 BTC with my SOL at 10% below current market price
43 | ```
44 | ```
45 | Get out of SOL now!
46 | ```
47 | ```
48 | Put a stop loss on all my altcoin positions in test2 wallet
49 | ```
50 | - Cancelling Orders
51 | ```
52 | Cancel all my open orders
53 | ```
54 | ```
55 | Cancel all my buy orders below 5% of the current market price in SOL
56 | ```
57 | 
58 | ### Helpful Notes
59 | - The more specific you are, the more control you can have over whatever strategy you want.
60 | - It will help if you ask for the current state of your assets to better plan what to do.
61 | - All agents are not created equally, and won't use tools in the same way.
62 | - If your agent has Thinking mode or capability, try using that for a boost.
63 | - Talk to your agent about strategy before commanding it to do something.
64 | - None of this is financial advice.
65 | 
```

--------------------------------------------------------------------------------
/.github/workflows/python-publish.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # This workflow will upload a Python Package to PyPI when a release is created
 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
 3 | 
 4 | # This workflow uses actions that are not certified by GitHub.
 5 | # They are provided by a third-party and are governed by
 6 | # separate terms of service, privacy policy, and support
 7 | # documentation.
 8 | 
 9 | name: Upload Python Package
10 | 
11 | on:
12 |   release:
13 |     types: [published]
14 | 
15 | permissions:
16 |   contents: read
17 | 
18 | jobs:
19 |   release-build:
20 |     runs-on: ubuntu-latest
21 | 
22 |     steps:
23 |       - uses: actions/checkout@v4
24 | 
25 |       - uses: actions/setup-python@v5
26 |         with:
27 |           python-version: "3.x"
28 | 
29 |       - name: Build release distributions
30 |         run: |
31 |           # NOTE: put your own distribution build steps here.
32 |           python -m pip install build
33 |           python -m build
34 | 
35 |       - name: Upload distributions
36 |         uses: actions/upload-artifact@v4
37 |         with:
38 |           name: release-dists
39 |           path: dist/
40 | 
41 |   pypi-publish:
42 |     runs-on: ubuntu-latest
43 |     needs:
44 |       - release-build
45 |     permissions:
46 |       # IMPORTANT: this permission is mandatory for trusted publishing
47 |       id-token: write
48 | 
49 |     # Dedicated environments with protections for publishing are strongly recommended.
50 |     # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
51 |     environment:
52 |       name: pypi
53 |       # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
54 |       # url: https://pypi.org/p/YOURPROJECT
55 |       #
56 |       # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
57 |       # ALTERNATIVE: exactly, uncomment the following line instead:
58 |       url: https://pypi.org/project/armor-crypto-mcp/${{ github.event.release.name }}
59 | 
60 |     steps:
61 |       - name: Retrieve release distributions
62 |         uses: actions/download-artifact@v4
63 |         with:
64 |           name: release-dists
65 |           path: dist/
66 | 
67 |       - name: Publish release distributions to PyPI
68 |         uses: pypa/gh-action-pypi-publish@release/v1
69 |         with:
70 |           packages-dir: dist/
71 | 
```

--------------------------------------------------------------------------------
/armor_crypto_mcp/armor_mcp.py:
--------------------------------------------------------------------------------

```python
  1 | import os
  2 | import asyncio
  3 | from typing import List, Any, Dict
  4 | 
  5 | from dotenv import load_dotenv
  6 | from mcp.server.fastmcp import FastMCP, Context
  7 | 
  8 | 
  9 | # Import base models
 10 | from .armor_client import (
 11 |     ArmorWalletAPIClient,
 12 |     calculate,
 13 |     WalletTokenBalance,
 14 |     ConversionResponse,
 15 |     SwapQuoteResponse,
 16 |     SwapTransactionResponse,
 17 |     Wallet,
 18 |     TokenDetailsResponseContainer,
 19 |     GroupInfo,
 20 |     SingleGroupInfo,
 21 |     WalletInfo,
 22 |     WalletArchiveOrUnarchiveResponse,
 23 |     CreateGroupResponse,
 24 |     AddWalletToGroupResponse,
 25 |     GroupArchiveOrUnarchiveResponse,
 26 |     RemoveWalletFromGroupResponse,
 27 |     TransferTokenResponse,
 28 |     DCAOrderResponse,
 29 |     CancelDCAOrderResponse,
 30 |     ListSingleGroupRequest,
 31 |     TopTrendingTokensRequest,
 32 |     CandleStickRequest,
 33 |     StakeBalanceResponse,
 34 |     ListWalletsRequest,
 35 |     ListDCAOrderRequest,
 36 |     ListOrderRequest,
 37 |     PrivateKeyRequest,
 38 |     WalletTokenPairsContainer,
 39 |     ConversionRequestContainer,
 40 |     SwapQuoteRequestContainer,
 41 |     SwapTransactionRequestContainer,
 42 |     TokenDetailsRequestContainer,
 43 |     TokenSearchRequest,
 44 |     TokenSearchResponseContainer,
 45 |     TransferTokensRequestContainer,
 46 |     DCAOrderRequestContainer,
 47 |     CancelDCAOrderRequestContainer,
 48 |     CreateWalletRequestContainer,
 49 |     ArchiveWalletsRequestContainer,
 50 |     UnarchiveWalletRequestContainer,
 51 |     CreateGroupsRequestContainer,
 52 |     AddWalletToGroupRequestContainer,
 53 |     ArchiveWalletGroupRequestContainer,
 54 |     UnarchiveWalletGroupRequestContainer,
 55 |     RemoveWalletsFromGroupRequestContainer,
 56 |     CreateOrderRequestContainer,
 57 |     CancelOrderRequestContainer,
 58 |     CreateOrderResponseContainer,
 59 |     CancelOrderResponseContainer,
 60 |     StakeQuoteRequestContainer,
 61 |     UnstakeQuoteRequestContainer,
 62 |     StakeTransactionRequestContainer,
 63 |     UnstakeTransactionRequestContainer,
 64 |     RenameWalletRequestContainer,
 65 |     ListDCAOrderResponseContainer,
 66 |     ListOrderResponseContainer,
 67 | )
 68 | 
 69 | # Load environment variables (e.g. BASE_API_URL, etc.)
 70 | load_dotenv()
 71 | 
 72 | # Create an MCP server instance with FastMCP
 73 | mcp = FastMCP("Armor Crypto MCP")
 74 | 
 75 | # Global variable to hold the authenticated Armor API client
 76 | ACCESS_TOKEN = os.getenv('ARMOR_API_KEY') or os.getenv('ARMOR_ACCESS_TOKEN')
 77 | BASE_API_URL = os.getenv('ARMOR_API_URL') or 'https://app.armorwallet.ai/api/v1'
 78 | 
 79 | armor_client = ArmorWalletAPIClient(ACCESS_TOKEN, base_api_url=BASE_API_URL) #, log_path='armor_client.log')
 80 | 
 81 | # Include version endpoint
 82 | from armor_crypto_mcp import __version__
 83 | @mcp.tool()
 84 | async def get_armor_mcp_version():
 85 |     """Get the current Armor Wallet version"""
 86 |     return {'armor_version': __version__}
 87 | 
 88 | @mcp.tool()
 89 | async def wait_a_moment(seconds:float):
 90 |     """Wait for some short amount of time, no more than 10 seconds"""
 91 |     await asyncio.sleep(seconds)
 92 |     return {"waited": seconds}
 93 | 
 94 | from datetime import datetime, timezone
 95 | @mcp.tool()
 96 | async def get_current_time() -> Dict:
 97 |     """Gets the current time and date"""
 98 |     return {"current_time": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")}
 99 | 
100 | @mcp.tool()
101 | async def calculator(expression:str, variables:dict[str, Any]):
102 |     """
103 |     Safely evaluates a mathematical or statistical expression string using Python syntax.
104 | 
105 |     Supports arithmetic operations (+, -, *, /, **, %, //), list expressions, and a range of math and statistics functions: 
106 |     abs, round, min, max, len, sum, mean, median, stdev, variance, sin, cos, tan, sqrt, log, exp, floor, ceil, etc.
107 | 
108 |     Custom variables can be passed via the 'variables' dict, including lists for time series data.
109 |     """
110 |     return {'result': calculate(expression, variables)}
111 | 
112 | @mcp.tool()
113 | async def get_wallet_token_balance(wallet_token_pairs: WalletTokenPairsContainer) -> List[WalletTokenBalance]:
114 |     """
115 |     Get the balance for a list of wallet/token pairs.
116 |     
117 |     Expects a WalletTokenPairsContainer, returns a list of WalletTokenBalance.
118 |     """
119 |     if not armor_client:
120 |         return [{"error": "Not logged in"}]
121 |     try:
122 |         result: List[WalletTokenBalance] = await armor_client.get_wallet_token_balance(wallet_token_pairs)
123 |         return result
124 |     except Exception as e:
125 |         return [{"error": str(e)}]
126 | 
127 | 
128 | @mcp.tool()
129 | async def calculate_token_conversion(conversion_requests: ConversionRequestContainer) -> List[ConversionResponse]:
130 |     """
131 |     Perform token conversion quote between two tokens. Good for quickly calculating market prices.
132 |     
133 |     Expects a ConversionRequestContainer, returns a list of ConversionResponse.
134 |     """
135 |     if not armor_client:
136 |         return [{"error": "Not logged in"}]
137 |     try:
138 |         result: List[ConversionResponse] = await armor_client.conversion_api(conversion_requests)
139 |         return result
140 |     except Exception as e:
141 |         return [{"error": str(e)}]
142 | 
143 | 
144 | @mcp.tool()
145 | async def swap_quote(swap_quote_requests: SwapQuoteRequestContainer) -> List[SwapQuoteResponse]:
146 |     """
147 |     Retrieve a swap quote. Be sure to add slippage!
148 |     
149 |     Expects a SwapQuoteRequestContainer, returns a list of SwapQuoteResponse.
150 |     """
151 |     if not armor_client:
152 |         return [{"error": "Not logged in"}]
153 |     try:
154 |         result: List[SwapQuoteResponse] = await armor_client.swap_quote(swap_quote_requests)
155 |         return result
156 |     except Exception as e:
157 |         return [{"error": str(e)}]
158 | 
159 | 
160 | @mcp.tool()
161 | async def swap_transaction(swap_transaction_requests: SwapTransactionRequestContainer) -> List[SwapTransactionResponse]:
162 |     """
163 |     Execute a swap transaction.
164 |     
165 |     Expects a SwapTransactionRequestContainer, returns a list of SwapTransactionResponse.
166 |     """
167 |     if not armor_client:
168 |         return [{"error": "Not logged in"}]
169 |     try:
170 |         result: List[SwapTransactionResponse] = await armor_client.swap_transaction(swap_transaction_requests)
171 |         return result
172 |     except Exception as e:
173 |         return [{"error": str(e)}]
174 | 
175 | 
176 | @mcp.tool()
177 | async def get_all_wallets(get_all_wallets_requests: ListWalletsRequest) -> List[Wallet]:
178 |     """
179 |     Retrieve all wallets with balances.
180 |     
181 |     Returns a list of Wallets and asssets
182 |     """
183 |     if not armor_client:
184 |         return [{"error": "Not logged in"}]
185 |     try:
186 |         result: List[Wallet] = await armor_client.get_all_wallets(get_all_wallets_requests)
187 |         return result
188 |     except Exception as e:
189 |         return [{"error": str(e)}]
190 |     
191 | 
192 | @mcp.tool()
193 | async def get_all_orders(get_all_orders_requests: ListOrderRequest) -> ListOrderResponseContainer:
194 |     """
195 |     Retrieve all limit, take profit and stop loss orders.
196 |     
197 |     Returns a list of orders.
198 |     """
199 |     if not armor_client:
200 |         return [{"error": "Not logged in"}]
201 |     try:
202 |         result: ListOrderResponseContainer = await armor_client.list_orders(get_all_orders_requests)
203 |         return result
204 |     except Exception as e:
205 |         return [{"error": str(e)}]
206 |     
207 | 
208 | @mcp.tool()
209 | async def search_official_token_address(token_details_requests: TokenDetailsRequestContainer) -> TokenDetailsResponseContainer:
210 |     """
211 |     Get the official token address and symbol for a token symbol or token address.
212 |     Try to use this first to get address and symbol of coin. If not found, use search_token_details to get details.
213 | 
214 |     Expects a TokenDetailsRequestContainer, returns a TokenDetailsResponseContainer.
215 |     """
216 |     if not armor_client:
217 |         return [{"error": "Not logged in"}]
218 |     try:
219 |         result: TokenDetailsResponseContainer = await armor_client.get_official_token_address(token_details_requests)
220 |         return result
221 |     except Exception as e:
222 |         return [{"error": str(e)}]
223 | 
224 | 
225 | @mcp.tool()
226 | async def search_token_details(token_search_requests: TokenSearchRequest) -> TokenSearchResponseContainer:
227 |     """
228 |     Search and retrieve details about single token.
229 |     If only address or symbol is needed, use get_official_token_address first.
230 |     
231 |     Expects a TokenSearchRequest, returns a list of TokenDetailsResponse.
232 |     """
233 |     if not armor_client:
234 |         return [{"error": "Not logged in"}]
235 |     try:
236 |         result: TokenSearchResponseContainer = await armor_client.search_token(token_search_requests)
237 |         return result
238 |     except Exception as e:
239 |         return [{"error": str(e)}]
240 | 
241 | 
242 | @mcp.tool()
243 | async def list_groups() -> List[GroupInfo]:
244 |     """
245 |     List all wallet groups.
246 |     
247 |     Returns a list of GroupInfo.
248 |     """
249 |     if not armor_client:
250 |         return [{"error": "Not logged in"}]
251 |     try:
252 |         result: List[GroupInfo] = await armor_client.list_groups()
253 |         return result
254 |     except Exception as e:
255 |         return [{"error": str(e)}]
256 | 
257 | 
258 | @mcp.tool()
259 | async def list_single_group(list_single_group_requests: ListSingleGroupRequest) -> SingleGroupInfo:
260 |     """
261 |     Retrieve details for a single wallet group.
262 |     
263 |     Expects the group name as a parameter, returns SingleGroupInfo.
264 |     """
265 |     if not armor_client:
266 |         return {"error": "Not logged in"}
267 |     try:
268 |         result: SingleGroupInfo = await armor_client.list_single_group(list_single_group_requests)
269 |         return result
270 |     except Exception as e:
271 |         return {"error": str(e)}
272 | 
273 | 
274 | @mcp.tool()
275 | async def create_wallet(create_wallet_requests: CreateWalletRequestContainer) -> List[WalletInfo]:
276 |     """
277 |     Create new wallets.
278 |     
279 |     Expects a list of wallet names, returns a list of WalletInfo.
280 |     """
281 |     if not armor_client:
282 |         return [{"error": "Not logged in"}]
283 |     try:
284 |         result: List[WalletInfo] = await armor_client.create_wallet(create_wallet_requests)
285 |         return result
286 |     except Exception as e:
287 |         return [{"error": str(e)}]
288 | 
289 | 
290 | @mcp.tool()
291 | async def archive_wallets(archive_wallet_requests: ArchiveWalletsRequestContainer) -> List[WalletArchiveOrUnarchiveResponse]:
292 |     """
293 |     Archive wallets.
294 |     
295 |     Expects a list of wallet names, returns a list of WalletArchiveOrUnarchiveResponse.
296 |     """
297 |     if not armor_client:
298 |         return [{"error": "Not logged in"}]
299 |     try:
300 |         result: List[WalletArchiveOrUnarchiveResponse] = await armor_client.archive_wallets(archive_wallet_requests)
301 |         return result
302 |     except Exception as e:
303 |         return [{"error": str(e)}]
304 | 
305 | 
306 | @mcp.tool()
307 | async def unarchive_wallets(unarchive_wallet_requests: UnarchiveWalletRequestContainer) -> List[WalletArchiveOrUnarchiveResponse]:
308 |     """
309 |     Unarchive wallets.
310 |     
311 |     Expects a list of wallet names, returns a list of WalletArchiveOrUnarchiveResponse.
312 |     """
313 |     if not armor_client:
314 |         return [{"error": "Not logged in"}]
315 |     try:
316 |         result: List[WalletArchiveOrUnarchiveResponse] = await armor_client.unarchive_wallets(unarchive_wallet_requests)
317 |         return result
318 |     except Exception as e:
319 |         return [{"error": str(e)}]
320 | 
321 | 
322 | @mcp.tool()
323 | async def create_groups(create_groups_requests: CreateGroupsRequestContainer) -> List[CreateGroupResponse]:
324 |     """
325 |     Create new wallet groups.
326 |     
327 |     Expects a list of group names, returns a list of CreateGroupResponse.
328 |     """
329 |     if not armor_client:
330 |         return [{"error": "Not logged in"}]
331 |     try:
332 |         result: List[CreateGroupResponse] = await armor_client.create_groups(create_groups_requests)
333 |         return result
334 |     except Exception as e:
335 |         return [{"error": str(e)}]
336 | 
337 | 
338 | @mcp.tool()
339 | async def add_wallets_to_group(add_wallet_to_group_requests: AddWalletToGroupRequestContainer) -> List[AddWalletToGroupResponse]:
340 |     """
341 |     Add wallets to a specified group.
342 |     
343 |     Expects the group name and a list of wallet names, returns a list of AddWalletToGroupResponse.
344 |     """
345 |     if not armor_client:
346 |         return [{"error": "Not logged in"}]
347 |     try:
348 |         result: List[AddWalletToGroupResponse] = await armor_client.add_wallets_to_group(add_wallet_to_group_requests)
349 |         return result
350 |     except Exception as e:
351 |         return [{"error": str(e)}]
352 | 
353 | 
354 | @mcp.tool()
355 | async def archive_wallet_group(archive_wallet_group_requests: ArchiveWalletGroupRequestContainer) -> List[GroupArchiveOrUnarchiveResponse]:
356 |     """
357 |     Archive wallet groups.
358 |     
359 |     Expects a list of group names, returns a list of GroupArchiveOrUnarchiveResponse.
360 |     """
361 |     if not armor_client:
362 |         return [{"error": "Not logged in"}]
363 |     try:
364 |         result: List[GroupArchiveOrUnarchiveResponse] = await armor_client.archive_wallet_group(archive_wallet_group_requests)
365 |         return result
366 |     except Exception as e:
367 |         return [{"error": str(e)}]
368 | 
369 | 
370 | @mcp.tool()
371 | async def unarchive_wallet_group(unarchive_wallet_group_requests: UnarchiveWalletGroupRequestContainer) -> List[GroupArchiveOrUnarchiveResponse]:
372 |     """
373 |     Unarchive wallet groups.
374 |     
375 |     Expects a list of group names, returns a list of GroupArchiveOrUnarchiveResponse.
376 |     """
377 |     if not armor_client:
378 |         return [{"error": "Not logged in"}]
379 |     try:
380 |         result: List[GroupArchiveOrUnarchiveResponse] = await armor_client.unarchive_wallet_group(unarchive_wallet_group_requests)
381 |         return result
382 |     except Exception as e:
383 |         return [{"error": str(e)}]
384 | 
385 | 
386 | @mcp.tool()
387 | async def remove_wallets_from_group(remove_wallets_from_group_requests: RemoveWalletsFromGroupRequestContainer) -> List[RemoveWalletFromGroupResponse]:
388 |     """
389 |     Remove wallets from a specified group.
390 |     
391 |     Expects the group name and a list of wallet names, returns a list of RemoveWalletFromGroupResponse.
392 |     """
393 |     if not armor_client:
394 |         return [{"error": "Not logged in"}]
395 |     try:
396 |         result: List[RemoveWalletFromGroupResponse] = await armor_client.remove_wallets_from_group(remove_wallets_from_group_requests)
397 |         return result
398 |     except Exception as e:
399 |         return [{"error": str(e)}]
400 | 
401 | 
402 | @mcp.tool()
403 | async def transfer_tokens(transfer_tokens_requests: TransferTokensRequestContainer) -> List[TransferTokenResponse]:
404 |     """
405 |     Transfer tokens from one wallet to another.
406 |     
407 |     Expects a TransferTokensRequestContainer, returns a list of TransferTokenResponse.
408 |     """
409 |     if not armor_client:
410 |         return [{"error": "Not logged in"}]
411 |     try:
412 |         result: List[TransferTokenResponse] = await armor_client.transfer_tokens(transfer_tokens_requests)
413 |         return result
414 |     except Exception as e:
415 |         return [{"error": str(e)}]
416 | 
417 | 
418 | @mcp.tool()
419 | async def create_dca_order(dca_order_requests: DCAOrderRequestContainer) -> List[DCAOrderResponse]:
420 |     """
421 |     Create a DCA order.
422 |     
423 |     Expects a DCAOrderRequestContainer, returns a list of DCAOrderResponse.
424 |     """
425 |     if not armor_client:
426 |         return [{"error": "Not logged in"}]
427 |     try:
428 |         result: List[DCAOrderResponse] = await armor_client.create_dca_order(dca_order_requests)
429 |         return result
430 |     except Exception as e:
431 |         return [{"error": str(e)}]
432 | 
433 | 
434 | @mcp.tool()
435 | async def list_dca_orders(list_dca_order_requests: ListDCAOrderRequest) -> ListDCAOrderResponseContainer:
436 |     """
437 |     List all DCA orders.
438 |     
439 |     Returns a list of DCAOrderResponse.
440 |     """
441 |     if not armor_client:
442 |         return [{"error": "Not logged in"}]
443 |     try:
444 |         result: ListDCAOrderResponseContainer = await armor_client.list_dca_orders(list_dca_order_requests)
445 |         return result
446 |     except Exception as e:
447 |         return [{"error": str(e)}]
448 | 
449 | 
450 | @mcp.tool()
451 | async def cancel_dca_order(cancel_dca_order_requests: CancelDCAOrderRequestContainer) -> List[CancelDCAOrderResponse]:
452 |     """
453 |     Create a DCA order.
454 | 
455 |     Note: Make a single or multiple dca_order_requests 
456 |     """
457 |     if not armor_client:
458 |         return [{"error": "Not logged in"}]
459 |     try:
460 |         result: List[CancelDCAOrderResponse] = await armor_client.cancel_dca_order(cancel_dca_order_requests)
461 |         return result
462 |     except Exception as e:
463 |         return [{"error": str(e)}]
464 |     
465 | 
466 | @mcp.tool()
467 | async def create_order(create_order_requests: CreateOrderRequestContainer) -> CreateOrderResponseContainer:
468 |     """
469 |     Create a order. Can be a limit, take profit or stop loss order.
470 |     
471 |     Expects a CreateOrderRequestContainer, returns a CreateOrderResponseContainer.
472 |     """
473 |     if not armor_client:
474 |         return [{"error": "Not logged in"}]
475 |     try:
476 |         result: CreateOrderResponseContainer = await armor_client.create_order(create_order_requests)
477 |         return result
478 |     except Exception as e:
479 |         return [{"error": str(e)}]
480 |     
481 | 
482 | @mcp.tool()
483 | async def cancel_order(cancel_order_requests: CancelOrderRequestContainer) -> CancelOrderResponseContainer:
484 |     """
485 |     Cancel a limit, take profit or stop loss order.
486 |     
487 |     Expects a CancelOrderRequestContainer, returns a CancelOrderResponseContainer.
488 |     """
489 |     if not armor_client:
490 |         return [{"error": "Not logged in"}]
491 |     try:
492 |         result: CancelOrderResponseContainer = await armor_client.cancel_order(cancel_order_requests)
493 |         return result
494 |     except Exception as e:
495 |         return [{"error": str(e)}]
496 |     
497 | 
498 | @mcp.tool()
499 | async def stake_quote(stake_quote_requests: StakeQuoteRequestContainer) -> SwapQuoteRequestContainer:
500 |     """
501 |     Retrieve a stake quote.
502 |     
503 |     Expects a StakeQuoteRequestContainer, returns a SwapQuoteRequestContainer.
504 |     """
505 |     if not armor_client:
506 |         return [{"error": "Not logged in"}]
507 |     try:
508 |         result: StakeQuoteRequestContainer = await armor_client.stake_quote(stake_quote_requests)
509 |         return result
510 |     except Exception as e:
511 |         return [{"error": str(e)}]
512 | 
513 | 
514 | @mcp.tool()
515 | async def unstake_quote(unstake_quote_requests: UnstakeQuoteRequestContainer) -> SwapQuoteRequestContainer:
516 |     """
517 |     Retrieve an unstake quote.
518 | 
519 |     Expects a UnstakeQuoteRequestContainer, returns a SwapQuoteRequestContainer.
520 |     """
521 |     if not armor_client:
522 |         return [{"error": "Not logged in"}]
523 |     try:
524 |         result: UnstakeQuoteRequestContainer = await armor_client.unstake_quote(unstake_quote_requests)
525 |         return result
526 |     except Exception as e:
527 |         return [{"error": str(e)}]
528 | 
529 | 
530 | @mcp.tool()
531 | async def stake_transaction(stake_transaction_requests: StakeTransactionRequestContainer) -> SwapTransactionRequestContainer:
532 |     """
533 |     Execute a stake transaction.
534 |     
535 |     Expects a StakeTransactionRequestContainer, returns a SwapTransactionRequestContainer.
536 |     """
537 |     if not armor_client:
538 |         return [{"error": "Not logged in"}]
539 |     try:
540 |         result: SwapTransactionRequestContainer = await armor_client.stake_transaction(stake_transaction_requests)
541 |         return result
542 |     except Exception as e:
543 |         return [{"error": str(e)}]
544 | 
545 | 
546 | @mcp.tool()
547 | async def unstake_transaction(unstake_transaction_requests: UnstakeTransactionRequestContainer) -> SwapTransactionRequestContainer:
548 |     """
549 |     Execute an unstake transaction.
550 |     
551 |     Expects a UnstakeTransactionRequestContainer, returns a SwapTransactionRequestContainer.
552 |     """
553 |     if not armor_client:
554 |         return [{"error": "Not logged in"}]
555 |     try:
556 |         result: SwapTransactionRequestContainer = await armor_client.unstake_transaction(unstake_transaction_requests)
557 |         return result
558 |     except Exception as e:
559 |         return [{"error": str(e)}]
560 |     
561 | 
562 | @mcp.tool()
563 | async def get_top_trending_tokens(top_trending_tokens_requests: TopTrendingTokensRequest) -> List:
564 |     """
565 |     Get the top trending tokens in a particular time frame. Great for comparing market cap or volume.
566 |     
567 |     Expects a TopTrendingTokensRequest, returns a list of tokens with their details.
568 |     """
569 |     if not armor_client:
570 |         return [{"error": "Not logged in"}]
571 |     try:
572 |         result: List = await armor_client.top_trending_tokens(top_trending_tokens_requests)
573 |         return result
574 |     except Exception as e:
575 |         return [{"error": str(e)}]
576 |     
577 | 
578 | @mcp.tool()
579 | async def get_stake_balances() -> StakeBalanceResponse:
580 |     """
581 |     Get the balance of staked SOL (jupSOL).
582 |     
583 |     Returns a StakeBalanceResponse.
584 |     """
585 |     if not armor_client:
586 |         return [{"error": "Not logged in"}]
587 |     try:
588 |         result: StakeBalanceResponse = await armor_client.get_stake_balances()
589 |         return result
590 |     except Exception as e:
591 |         return [{"error": str(e)}]
592 |     
593 | 
594 | @mcp.tool()
595 | async def rename_wallets(rename_wallet_requests: RenameWalletRequestContainer) -> List:
596 |     """
597 |     Rename wallets.
598 |     
599 |     Expects a RenameWalletRequestContainer, returns a list.
600 |     """
601 |     if not armor_client:
602 |         return [{"error": "Not logged in"}]
603 |     try:
604 |         result: List = await armor_client.rename_wallet(rename_wallet_requests)
605 |         return result
606 |     except Exception as e:
607 |         return [{"error": str(e)}]
608 |     
609 | 
610 | @mcp.tool()
611 | async def get_token_candle_data(candle_stick_requests: CandleStickRequest) -> List:
612 |     """
613 |     Get candle data about any token for analysis.
614 | 
615 |     Expects a CandleStickRequest, returns a list of candle sticks.
616 |     """
617 |     if not armor_client:
618 |         return [{"error": "Not logged in"}]
619 |     try:
620 |         result: List = await armor_client.get_market_candle_data(candle_stick_requests)
621 |         return result
622 |     except Exception as e:
623 |         return [{"error": str(e)}]
624 | 
625 | 
626 | @mcp.prompt()
627 | def login_prompt(email: str) -> str:
628 |     """
629 |     A sample prompt to ask the user for their access token after providing an email.
630 |     """
631 |     return f"Please enter the Access token for your account {email}."
632 | 
633 | 
634 | @mcp.tool()
635 | async def send_key_to_telegram(private_key_request: PrivateKeyRequest) -> Dict:
636 |     """
637 |     Send the mnemonic or private key to telegram.
638 |     """
639 |     if not armor_client:
640 |         return [{"error": "Not logged in"}]
641 |     try:
642 |         result: Dict = await armor_client.send_key_to_telegram(private_key_request)
643 |         return result
644 |     except Exception as e:
645 |         return [{"error": str(e)}]
646 | 
647 | 
648 | def main():
649 |     mcp.run()
650 |     
651 | if __name__ == "__main__":
652 |     main()
653 | 
```

--------------------------------------------------------------------------------
/armor_crypto_mcp/armor_client.py:
--------------------------------------------------------------------------------

```python
  1 | import json
  2 | import os
  3 | from pydantic import BaseModel, Field
  4 | from typing_extensions import List, Optional, Literal, Dict
  5 | import httpx
  6 | from dotenv import load_dotenv
  7 | 
  8 | 
  9 | import ast
 10 | import operator
 11 | import math
 12 | import statistics
 13 | 
 14 | load_dotenv()
 15 | BASE_API_URL = os.getenv("BASE_API_URL")
 16 | 
 17 | # ------------------------------
 18 | # BaseModel Definitions
 19 | # ------------------------------
 20 | 
 21 | class WalletTokenPairs(BaseModel):
 22 |     wallet: str = Field(description="The name of wallet. To get wallet names use `get_user_wallets_and_groups_list`")
 23 |     token: str = Field(description="public address of token. To get the address from a token symbol use `get_token_details`")
 24 | 
 25 | 
 26 | class WalletTokenBalance(BaseModel):
 27 |     wallet: str = Field(description="name of wallet")
 28 |     token: str = Field(description="public address of token")
 29 |     balance: float = Field(description="balance of token")
 30 | 
 31 | 
 32 | class ConversionRequest(BaseModel):
 33 |     input_amount: float = Field(description="input amount to convert")
 34 |     input_token: str = Field(description="public address of input token")
 35 |     output_token: str = Field(description="public address of output token")
 36 | 
 37 | 
 38 | class ConversionResponse(BaseModel):
 39 |     input_amount: float = Field(description="input amount before conversion")
 40 |     input_token: str = Field(description="public address of input token")
 41 |     output_token: str = Field(description="public address of output token")
 42 |     output_amount: float = Field(description="output amount after conversion")
 43 | 
 44 | 
 45 | class SwapQuoteRequest(BaseModel):
 46 |     from_wallet: str = Field(description="The name of the wallet that input_token is in.")
 47 |     input_token: str = Field(description="public mint address of input token. To get the address from a token symbol use `get_token_details`")
 48 |     output_token: str = Field(description="public mint address of output token. To get the address from a token symbol use `get_token_details`")
 49 |     input_amount: float = Field(description="input amount to swap")
 50 |     slippage: float = Field("slippage percentage. To estimate slippage based on liquidity see `get_token_details` for the input_token_symbol. 1.0 for high liquidity and near 20.0 for lower liquidity.")
 51 | 
 52 | 
 53 | class StakeQuoteRequest(BaseModel):
 54 |     from_wallet: str = Field(description="The name of the wallet that input_token is in.")
 55 |     input_token: str = "So11111111111111111111111111111111111111112"  # Hardcoded SOL token address
 56 |     output_token: str = Field(description="the public mint address of the output liquid staking derivative token to stake.") # "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v"
 57 |     input_amount: float = Field(description="input amount to swap")
 58 | 
 59 | 
 60 | class UnstakeQuoteRequest(BaseModel):
 61 |     from_wallet: str = Field(description="The name of the wallet that input_token is in.")
 62 |     input_token: str = Field(description="the public mint address of the input liquid staking derivative token to unstake.") # "jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v"
 63 |     output_token: str = "So11111111111111111111111111111111111111112"
 64 |     input_amount: float = Field(description="input amount to swap")
 65 | 
 66 | 
 67 | class SwapQuoteResponse(BaseModel):
 68 |     id: str = Field(description="unique id of the generated swap quote")
 69 |     wallet_address: str = Field(description="public address of the wallet")
 70 |     input_token_symbol: str = Field(description="symbol of the input token")
 71 |     input_token_address: str = Field(description="public address of the input token")
 72 |     output_token_symbol: str = Field(description="symbol of the output token")
 73 |     output_token_address: str = Field(description="public address of the output token")
 74 |     input_amount: float = Field(description="input amount in input token")
 75 |     output_amount: float = Field(description="output amount in output token")
 76 |     slippage: float = Field(description="slippage percentage.")
 77 | 
 78 | 
 79 | class SwapTransactionRequest(BaseModel):
 80 |     transaction_id: str = Field(description="unique id of the generated swap quote")
 81 | 
 82 | 
 83 | class StakeTransactionRequest(BaseModel):
 84 |     transaction_id: str = Field(description="unique id of the generated stake quote")
 85 | 
 86 | 
 87 | class UnstakeTransactionRequest(BaseModel):
 88 |     transaction_id: str = Field(description="unique id of the generated unstake quote")
 89 | 
 90 | 
 91 | class SwapTransactionResponse(BaseModel):
 92 |     id: str = Field(description="unique id of the swap transaction")
 93 |     transaction_error: Optional[str] = Field(description="error message if the transaction fails")
 94 |     transaction_url: str = Field(description="public url of the transaction")
 95 |     input_amount: float = Field(description="input amount in input token")
 96 |     output_amount: float = Field(description="output amount in output token")
 97 |     status: str = Field(description="status of the transaction")
 98 | 
 99 | 
100 | class ListWalletsRequest(BaseModel):
101 |     is_archived: bool = Field(default=False, description="whether to include archived wallets")
102 | 
103 | 
104 | class WalletBalance(BaseModel):
105 |     mint_address: str = Field(description="public mint address of output token. To get the address from a token symbol use `get_token_details`")
106 |     name: str = Field(description="name of the token")
107 |     symbol: str = Field(description="symbol of the token")
108 |     decimals: int = Field(description="number of decimals of the token")
109 |     amount: float = Field(description="balance of the token")
110 |     usd_price: str = Field(description="price of the token in USD")
111 |     usd_amount: float = Field(description="balance of the token in USD")
112 | 
113 | 
114 | class WalletInfo(BaseModel):
115 |     id: str = Field(description="wallet id")
116 |     name: str = Field(description="wallet name")
117 |     is_archived: bool = Field(description="whether the wallet is archived")
118 |     public_address: str = Field(description="public address of the wallet")
119 | 
120 | 
121 | class Wallet(WalletInfo):
122 |     balances: List[WalletBalance] = Field(description="list of balances of the wallet")
123 | 
124 | 
125 | class TokenDetailsRequest(BaseModel):
126 |     query: str = Field(description="token symbol or address")
127 | 
128 | 
129 | class TokenDetailsResponse(BaseModel):
130 |     symbol: str = Field(description="symbol of the token")
131 |     mint_address: str = Field(description="mint address of the token")
132 | 
133 | 
134 | class TokenSearchRequest(BaseModel):
135 |     query: str = Field(description="token symbol or address")
136 |     sort_by: Optional[Literal['decimals', 'holders', 'jupiter', 'verified', 'liquidityUsd', 'marketCapUsd', 'priceUsd', 'totalBuys', 'totalSells', 'totalTransactions', 'volume_5m', 'volume', 'volume_15m', 'volume_30m', 'volume_1h', 'volume_6h', 'volume_12h', 'volume_24h']] = Field(description="Sort token data results by this field")
137 |     sort_order: Optional[Literal['asc', 'desc']] = Field(default='desc', description="The order of the sorted results")
138 |     limit: Optional[int] = Field(default=10, description="The number of results to return from the search. Use default unless specified. Should not be over 30 if looking up multiple tokens.")
139 | 
140 | class TokenSearchResponse(BaseModel):
141 |     name: str = Field(description="name of the token")
142 |     symbol: str = Field(description="symbol of the token")
143 |     mint_address: Optional[str] = Field(description="mint address of the token")
144 |     decimals: Optional[int] = Field(description="number of decimals of the token, returns only if include_details is True")
145 |     image: Optional[str] = Field(description="image url of the token, returns only if include_details is True")
146 |     holders: Optional[int] = Field(description="number of holders of the token, returns only if include_details is True")
147 |     jupiter: Optional[bool] = Field(description="whether the token is supported by Jupiter, returns only if include_details is True")
148 |     verified: Optional[bool] = Field(description="whether the token is verified, returns only if include_details is True")
149 |     liquidityUsd: Optional[float] = Field(description="liquidity of the token in USD, returns only if include_details is True")
150 |     marketCapUsd: Optional[float] = Field(description="market cap of the token in USD, returns only if include_details is True")
151 |     priceUsd: Optional[float] = Field(description="price of the token in USD, returns only if include_details is True")
152 |     lpBurn: Optional[float] = Field(description="lp burn of the token, returns only if include_details is True")
153 |     market: Optional[str] = Field(description="market of the token, returns only if include_details is True")
154 |     freezeAuthority: Optional[str] = Field(description="freeze authority of the token, returns only if include_details is True")
155 |     mintAuthority: Optional[str] = Field(description="mint authority of the token, returns only if include_details is True")
156 |     poolAddress: Optional[str] = Field(description="pool address of the token, returns only if include_details is True")
157 |     totalBuys: Optional[int] = Field(description="total number of buys of the token, returns only if include_details is True")
158 |     totalSells: Optional[int] = Field(description="total number of sells of the token, returns only if include_details is True")
159 |     totalTransactions: Optional[int] = Field(description="total number of transactions of the token, returns only if include_details is True")
160 |     volume: Optional[float] = Field(description="volume of the token, returns only if include_details is True")
161 |     volume_5m: Optional[float] = Field(description="volume of the token in the last 5 minutes, returns only if include_details is True")
162 |     volume_15m: Optional[float] = Field(description="volume of the token in the last 15 minutes, returns only if include_details is True")
163 |     volume_30m: Optional[float] = Field(description="volume of the token in the last 30 minutes, returns only if include_details is True")
164 |     volume_1h: Optional[float] = Field(description="volume of the token in the last 1 hour, returns only if include_details is True")
165 |     volume_6h: Optional[float] = Field(description="volume of the token in the last 6 hours, returns only if include_details is True")
166 |     volume_12h: Optional[float] = Field(description="volume of the token in the last 12 hours, returns only if include_details is True")
167 |     volume_24h: Optional[float] = Field(description="volume of the token in the last 24 hours, returns only if include_details is True")
168 | 
169 | 
170 | class GroupInfo(BaseModel):
171 |     id: str = Field(description="id of the group")
172 |     name: str = Field(description="name of the group")
173 |     is_archived: bool = Field(description="whether the group is archived")
174 | 
175 | 
176 | class SingleGroupInfo(GroupInfo):
177 |     wallets: List[WalletInfo] = Field(description="list of wallets in the group")
178 | 
179 | 
180 | class WalletArchiveOrUnarchiveResponse(BaseModel):
181 |     wallet_name: str = Field(description="name of the wallet")
182 |     message: str = Field(description="message of the operation showing if wallet was archived or unarchived")
183 | 
184 | 
185 | class CreateGroupResponse(BaseModel):
186 |     id: str = Field(description="id of the group")
187 |     name: str = Field(description="name of the group")
188 |     is_archived: bool = Field(description="whether the group is archived")
189 | 
190 | 
191 | class AddWalletToGroupResponse(BaseModel):
192 |     wallet_name: str = Field(description="name of the wallet to add to the group")
193 |     group_name: str = Field(description="name of the group to add the wallet to")
194 |     message: str = Field(description="message of the operation showing if wallet was added to the group")
195 | 
196 | 
197 | class GroupArchiveOrUnarchiveResponse(BaseModel):
198 |     group: str = Field(description="name of the group")
199 | 
200 | 
201 | class RemoveWalletFromGroupResponse(BaseModel):
202 |     wallet: str = Field(description="name of the wallet to remove from the group")
203 |     group: str = Field(description="name of the group to remove the wallet from")
204 | 
205 | 
206 | class UserWalletsAndGroupsResponse(BaseModel):
207 |     id: str = Field(description="id of the user")
208 |     email: str = Field(description="email of the user")
209 |     first_name: str = Field(description="first name of the user")
210 |     last_name: str = Field(description="last name of the user")
211 |     slippage: float = Field(description="slippage set by the user")
212 |     wallet_groups: List[GroupInfo] = Field(description="list of user's wallet groups")
213 |     wallets: List[WalletInfo] = Field(description="list of user's wallets")
214 | 
215 | 
216 | class TransferTokensRequest(BaseModel):
217 |     from_wallet: str = Field(description="name of the wallet to transfer tokens from")
218 |     to_wallet_address: str = Field(description="public address of the wallet to transfer tokens to. Use `get_user_wallets_and_group_list` if you only have a wallet name")
219 |     token: str = Field(description="public contract address of the token to transfer. To get the address from a token symbol use `get_token_details`")
220 |     amount: float = Field(description="amount of tokens to transfer")
221 | 
222 | 
223 | class TransferTokenResponse(BaseModel):
224 |     amount: float = Field(description="amount of tokens transferred")
225 |     from_wallet_address: str = Field(description="public address of the wallet tokens were transferred from")
226 |     to_wallet_address: str = Field(description="public address of the wallet tokens were transferred to")
227 |     token_address: str = Field(description="public address of the token transferred")
228 |     transaction_url: str = Field(description="public url of the transaction")
229 |     message: str = Field(description="message of the operation showing if tokens were transferred")
230 | 
231 | class ListDCAOrderRequest(BaseModel):
232 |     status: Optional[Literal["COMPLETED", "OPEN", "CANCELLED"]] = Field(description="status of the DCA orders, if specified filters the results.")
233 |     limit: Optional[int] = Field(default=30, description="number of mostrecent results to return")
234 | 
235 | class DCAOrderRequest(BaseModel):
236 |     wallet: str = Field(description="name of the wallet")
237 |     input_token: str = Field(description="public address of the input token. To get the address from a token symbol use `get_token_details`")
238 |     output_token: str = Field(description="public address of the output token. To get the address from a token symbol use `get_token_details`")
239 |     amount: float = Field(description="total amount of input token to invest")
240 |     cron_expression: str = Field(description="cron expression for the DCA worker execution frequency")
241 |     strategy_duration_unit: Literal["MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR"] = Field(description="unit of the duration of the DCA order")
242 |     strategy_duration: int = Field(description="Total running time of the DCA order given in strategy duration units, should be more than 0")
243 |     execution_type: Literal["MULTIPLE", "SINGLE"] = Field(description="set to SINGLE only if the user is asking for a single scheduled order, MULTIPLE if it is a true DCA")
244 |     token_address_watcher: Optional[str] = Field(description="If the DCA is conditional, public address of the token to watch.")
245 |     watch_field: Optional[Literal["liquidity", "marketCap", "price"]] = Field(description="If the DCA is conditional, field to watch for the condition")
246 |     delta_type: Optional[Literal["INCREASE", "DECREASE", "MOVE", "MOVE_DAILY", "AVERAGE_MOVE"]] = Field(description="If the DCA is conditional, the operator of the watch field in the conditional statement")
247 |     delta_percentage: Optional[float] = Field(description="If the DCA is conditional, percentage of the change to watch for given the delta_type")
248 |     time_zone: Optional[str] = Field(description="user's time zone. Defaults to UTC")
249 | 
250 | 
251 | class DCAWatcher(BaseModel):
252 |     watch_field: Literal["liquidity", "marketCap", "price"] = Field(description="field to watch for the DCA order")
253 |     delta_type: Literal["INCREASE", "DECREASE", "MOVE", "MOVE_DAILY", "AVERAGE_MOVE"] = Field(description="type of the delta")
254 |     initial_value: float = Field(description="initial value of the delta")
255 |     delta_percentage: float = Field(description="percentage of the delta")
256 | 
257 | 
258 | class TokenData(BaseModel):
259 |     name: str = Field(description="name of the token")
260 |     symbol: str = Field(description="symbol of the token")
261 |     mint_address: str = Field(description="mint address of the token")
262 | 
263 | 
264 | class DCAOrderResponse(BaseModel):
265 |     id: str = Field(description="id of the DCA order")
266 |     amount: float = Field(description="amount of tokens to invest")
267 |     investment_per_cycle: float = Field(description="amount of tokens to invest per cycle")
268 |     cycles_completed: int = Field(description="number of cycles completed")
269 |     total_cycles: int = Field(description="total number of cycles")
270 |     human_readable_expiry: str = Field(description="human readable expiry date of the DCA order")
271 |     status: str = Field(description="status of the DCA order")
272 |     input_token_data: TokenData = Field(description="details of the input token")
273 |     output_token_data: TokenData = Field(description="details of the output token")
274 |     wallet_name: str = Field(description="name of the wallet")
275 |     watchers: List[DCAWatcher] = Field(description="list of watchers for the DCA order")
276 |     dca_transactions: List[dict] = Field(description="list of DCA transactions")  # Can be further typed if structure is known
277 |     created: str = Field(description="Linux timestamp of the creation of the order")
278 | 
279 | 
280 | class ListOrderRequest(BaseModel):
281 |     status: Optional[Literal["OPEN", "CANCELLED", "EXPIRED", "COMPLETED", "FAILED", "IN_PROCESS"]] = Field(description="status of the orders, if specified filters results.")
282 |     limit: Optional[int] = Field(default=30, description="number of most recent results to return")
283 | 
284 | 
285 | class CreateOrderRequest(BaseModel):
286 |     wallet: str = Field(description="name of the wallet")
287 |     input_token: str = Field(description="public address of the input token")
288 |     output_token: str = Field(description="public address of the output token")
289 |     amount: float = Field(description="amount of input token to invest")
290 |     strategy_duration: int = Field(description="duration of the order")
291 |     strategy_duration_unit: Literal["MINUTE", "HOUR", "DAY", "WEEK", "MONTH", "YEAR"] = Field(description="unit of the duration of the order")
292 |     watch_field: Literal["liquidity", "marketCap", "price"] = Field(description="field to watch to execute the order. Can be price, marketCap or liquidity")
293 |     direction: Literal["ABOVE", "BELOW"] = Field(description="whether or not the order is above or below current market value")
294 |     token_address_watcher: str = Field(description="public address of the token to watch. should be output token for limit orders and input token for stop loss and take profit orders")
295 |     target_value: Optional[float] = Field(description="target value to execute the order. You must always specify a target value or delta percentage.")
296 |     delta_percentage: Optional[float] = Field(description="delta percentage to execute the order. You must always specify a target value or delta percentage.")
297 | 
298 | class OrderWatcher(BaseModel):
299 |     watch_field: Literal["liquidity", "marketCap", "price"] = Field(description="field being watched for a delta")
300 |     delta_type: Literal["INCREASE", "DECREASE", "MOVE", "MOVE_DAILY", "AVERAGE_MOVE"] = Field(description="type of delta change")
301 |     initial_value: float = Field(description="initial value when watcher was created")
302 |     delta_percentage: float = Field(description="percentage for delta change")
303 |     watcher_type: Literal["LIMIT", "STOP_LOSS"] = Field(description="type of watcher")
304 |     buying_price: Optional[float] = Field(description="price at which to buy", default=None)
305 | 
306 | 
307 | class OrderResponse(BaseModel):
308 |     id: str = Field(description="unique identifier of the order")
309 |     amount: float = Field(description="amount of tokens to invest")
310 |     status: str = Field(description="current status of the order")
311 |     input_token_data: TokenData = Field(description="details of the input token")
312 |     output_token_data: TokenData = Field(description="details of the output token")
313 |     wallet_name: str = Field(description="name of the wallet")
314 |     execution_type: Literal["LIMIT", "STOP_LOSS", "TAKE_PROFIT"] = Field(description="type of the order")
315 |     expiry_time: str = Field(description="expiry time of the order in ISO format")
316 |     watchers: List[OrderWatcher] = Field(description="list of watchers for the order")
317 |     transaction: Optional[dict] = Field(description="transaction details if any", default=None)
318 |     created: str = Field(description="ISO 8601 timestamp of the creation of the order")
319 | 
320 | 
321 | class CancelOrderRequest(BaseModel):
322 |     order_id: str = Field(description="id of the limit order")
323 | 
324 | 
325 | class CancelOrderResponse(BaseModel):
326 |     order_id: str = Field(description="id of the limit order")
327 |     status: str = Field(description="status of the limit order")
328 | 
329 | 
330 | class CancelDCAOrderRequest(BaseModel):
331 |     dca_order_id: str = Field(description="id of the DCA order")
332 | 
333 | 
334 | class CancelDCAOrderResponse(BaseModel):
335 |     dca_order_id: str = Field(description="id of the DCA order")
336 |     status: str = Field(description="status of the DCA order")
337 | 
338 | 
339 | class ListSingleGroupRequest(BaseModel):
340 |     group_name: str = Field(description="Name of the group to retrieve details for")
341 | 
342 | class CreateWalletRequest(BaseModel):
343 |     name: str = Field(description="Name of the wallet to create")
344 | 
345 | class ArchiveWalletsRequest(BaseModel):
346 |     wallet: str = Field(description="Name of the wallet to archive")
347 | 
348 | class UnarchiveWalletsRequest(BaseModel):
349 |     wallet: str = Field(description="Name of the wallet to unarchive")
350 | 
351 | class CreateGroupsRequest(BaseModel):
352 |     name: str = Field(description="Name of the group to create")
353 | 
354 | class AddWalletToGroupRequest(BaseModel):
355 |     group: str = Field(description="Name of the group to add wallets to")
356 |     wallet: str = Field(description="Name of the wallet to add to the group")
357 | 
358 | class ArchiveWalletGroupRequest(BaseModel):
359 |     group: str = Field(description="Name of the group to archive")
360 | 
361 | class UnarchiveWalletGroupRequest(BaseModel):
362 |     group: str = Field(description="Name of the group to unarchive")
363 | 
364 | class RemoveWalletsFromGroupRequest(BaseModel):
365 |     group: str = Field(description="Name of the group to remove wallets from")
366 |     wallet: str = Field(description="List of wallet names to remove from the group")
367 | 
368 | class TopTrendingTokensRequest(BaseModel):
369 |     time_frame: Literal["5m", "15m", "30m", "1h", "2h", "3h", "4h", "5h", "6h", "12h", "24h"] = Field(default="24h", description="Time frame to get the top trending tokens")
370 | 
371 | class StakeBalanceResponse(BaseModel):
372 |     total_stake_amount: float = Field(description="Total stake balance in jupSol")
373 |     total_stake_amount_in_usd: float = Field(description="Total stake balance in USD")
374 | 
375 | 
376 | class RenameWalletRequest(BaseModel):
377 |     wallet: str = Field(description="Name of the wallet to rename")
378 |     new_name: str = Field(description="New name of the wallet")
379 | 
380 | 
381 | class CandleStickRequest(BaseModel):
382 |     token_address: str = Field(description="Public mint address of the token. To get the address from a token symbol use `get_token_details`")
383 |     time_interval: Literal["1s", "5s", "15s", "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1mn"] = Field(default="1h", description="Time frame to get the candle sticks. Use larger candle time frames over larger time windows to keep returned candles minimal")
384 |     time_from: str = Field(description="The time from which to start the candle data in ISO 8601 format. Attempt to change this to keep number of candles returned under 64.")
385 |     time_to: Optional[str] = Field(default=None, description="The time to end the candle data in ISO 8601 format. Use only for historic analysis.")
386 |     market_cap: Optional[bool] = Field(default=False, description="Whether to return the marketcap of the token instead of the price")
387 |     
388 | class PrivateKeyRequest(BaseModel):
389 |     wallet: str = Field(description="Name of the wallet to get the mnemonic or private key for")
390 |     key_type: Literal['PRIVATE_KEY', 'MNEMONIC'] = Field(description="Whether to return the private or mnemonic key")
391 | 
392 | # ------------------------------
393 | # Container Models for List Inputs
394 | # ------------------------------
395 | 
396 | class RemoveWalletsFromGroupRequestContainer(BaseModel):
397 |     remove_wallets_from_group_requests: List[RemoveWalletsFromGroupRequest]
398 | 
399 | class AddWalletToGroupRequestContainer(BaseModel):
400 |     add_wallet_to_group_requests: List[AddWalletToGroupRequest]
401 | 
402 | class CreateWalletRequestContainer(BaseModel):
403 |     create_wallet_requests: List[CreateWalletRequest]
404 | 
405 | class ArchiveWalletsRequestContainer(BaseModel):
406 |     archive_wallet_requests: List[ArchiveWalletsRequest]
407 | 
408 | class UnarchiveWalletRequestContainer(BaseModel):
409 |     unarchive_wallet_requests: List[UnarchiveWalletsRequest]
410 | 
411 | class ArchiveWalletGroupRequestContainer(BaseModel):
412 |     archive_wallet_group_requests: List[ArchiveWalletGroupRequest]
413 | 
414 | class UnarchiveWalletGroupRequestContainer(BaseModel):
415 |     unarchive_wallet_group_requests: List[UnarchiveWalletGroupRequest]
416 | 
417 | class WalletTokenPairsContainer(BaseModel):
418 |     wallet_token_pairs: List[WalletTokenPairs]
419 | 
420 | 
421 | class CreateGroupsRequestContainer(BaseModel):
422 |     create_groups_requests: List[CreateGroupsRequest]    
423 | 
424 | 
425 | class ConversionRequestContainer(BaseModel):
426 |     conversion_requests: List[ConversionRequest]
427 | 
428 | 
429 | class SwapQuoteRequestContainer(BaseModel):
430 |     swap_quote_requests: List[SwapQuoteRequest]
431 | 
432 | 
433 | class StakeQuoteRequestContainer(BaseModel):
434 |     stake_quote_requests: List[StakeQuoteRequest]
435 | 
436 | 
437 | class UnstakeQuoteRequestContainer(BaseModel):
438 |     unstake_quote_requests: List[UnstakeQuoteRequest]
439 | 
440 | 
441 | class SwapTransactionRequestContainer(BaseModel):
442 |     swap_transaction_requests: List[SwapTransactionRequest]
443 | 
444 | 
445 | class StakeTransactionRequestContainer(BaseModel):
446 |     stake_transaction_requests: List[StakeTransactionRequest]
447 | 
448 | 
449 | class UnstakeTransactionRequestContainer(BaseModel):
450 |     unstake_transaction_requests: List[UnstakeTransactionRequest]
451 | 
452 | 
453 | class TokenSearchResponseContainer(BaseModel):
454 |     token_search_responses: List[TokenSearchResponse]
455 | 
456 | 
457 | class TokenDetailsRequestContainer(BaseModel):
458 |     token_details_requests: List[TokenDetailsRequest]
459 | 
460 | 
461 | class TokenDetailsResponseContainer(BaseModel):
462 |     token_details_responses: List[TokenDetailsResponse]
463 | 
464 | 
465 | class TransferTokensRequestContainer(BaseModel):
466 |     transfer_tokens_requests: List[TransferTokensRequest]
467 | 
468 | 
469 | class DCAOrderRequestContainer(BaseModel):
470 |     dca_order_requests: List[DCAOrderRequest]
471 | 
472 | 
473 | class CancelDCAOrderRequestContainer(BaseModel):
474 |     cancel_dca_order_requests: List[CancelDCAOrderRequest]
475 | 
476 | class CreateOrderRequestContainer(BaseModel):
477 |     create_order_requests: List[CreateOrderRequest]
478 | 
479 | 
480 | class CreateOrderResponseContainer(BaseModel):
481 |     create_order_responses: List[OrderResponse]
482 | 
483 | 
484 | class CancelOrderRequestContainer(BaseModel):
485 |     cancel_order_requests: List[CancelOrderRequest]
486 | 
487 | 
488 | class CancelOrderResponseContainer(BaseModel):
489 |     cancel_order_responses: List[CancelOrderResponse]
490 | 
491 | 
492 | class RenameWalletRequestContainer(BaseModel):
493 |     rename_wallet_requests: List[RenameWalletRequest]
494 | 
495 | class ListDCAOrderResponseContainer(BaseModel):
496 |     list_dca_order_responses: List[DCAOrderResponse]
497 | 
498 | class ListOrderResponseContainer(BaseModel):
499 |     list_order_responses: List[OrderResponse]
500 | 
501 | # ------------------------------
502 | # API Client
503 | # ------------------------------
504 | 
505 | # Setup logger for the module
506 | import logging
507 | import traceback
508 | 
509 | class ArmorWalletAPIClient:
510 |     def __init__(self, access_token: str, base_api_url: str = 'https://app.armorwallet.ai/api/v1', logger=None):
511 |         self.base_api_url = base_api_url
512 |         self.access_token = access_token
513 |         self.logger = logger
514 | 
515 |     async def _api_call(self, method: str, endpoint: str, payload: str = None) -> dict:
516 |         """Utility function for API calls to the wallet.
517 |            It sets common headers and raises errors on non-2xx responses.
518 |         """
519 |         url = f"{self.base_api_url}/{endpoint}"
520 |         payload = json.dumps(payload)
521 |         if self.logger is not None:
522 |             self.logger.debug(f"Request: {method} {url} Payload: {payload}")
523 |         headers = {
524 |             'Content-Type': 'application/json',
525 |             'Authorization': f'Bearer {self.access_token}'
526 |         }
527 |         try:
528 |             async with httpx.AsyncClient(timeout=30) as client:
529 |                 response = await client.request(method, url, headers=headers, data=payload, follow_redirects=False)
530 |                 
531 |                 if self.logger is not None:
532 |                     self.logger.debug(f"Response status: {response.status_code} Response: {response.text}")
533 |             if response.status_code >= 400:
534 |                 if self.logger is not None:
535 |                     self.logger.error(f"API Error {response.status_code}: {response.text}")
536 |                 raise Exception(f"API Error {response.status_code}: {response.text}")
537 |             try:
538 |                 return response.json()
539 |             except Exception:
540 |                 if self.logger is not None:
541 |                     self.logger.error(f"JSON Parsing: {response.text}")
542 |                 return {"text": response.text}
543 |         except Exception as e:
544 |             traceback.print_exc()
545 |             if self.logger is not None:
546 |                 self.logger.error(f"{e}")
547 |             return {"text": str(e)}
548 | 
549 |     async def get_wallet_token_balance(self, data: WalletTokenPairsContainer) -> List[WalletTokenBalance]:
550 |         """Get balances from a list of wallet and token pairs."""
551 |         # payload = [v.model_dump() for v in data.wallet_token_pairs]
552 |         payload = data.model_dump(exclude_none=True)['wallet_token_pairs']
553 |         return await self._api_call("POST", "tokens/wallet-token-balance/", payload)
554 | 
555 |     async def conversion_api(self, data: ConversionRequestContainer) -> List[ConversionResponse]:
556 |         """Perform a token conversion."""
557 |         # payload = [v.model_dump() for v in data.conversion_requests]
558 |         payload = data.model_dump(exclude_none=True)['conversion_requests']
559 |         return await self._api_call("POST", "tokens/token-price-conversion/", payload)
560 | 
561 |     async def swap_quote(self, data: SwapQuoteRequestContainer) -> List[SwapQuoteResponse]:
562 |         """Obtain a swap quote."""
563 |         # payload = [v.model_dump() for v in data.swap_quote_requests]
564 |         payload = data.model_dump(exclude_none=True)['swap_quote_requests']
565 |         return await self._api_call("POST", "transactions/quote/", payload)
566 | 
567 |     async def stake_quote(self, data: StakeQuoteRequestContainer) -> StakeQuoteRequestContainer:
568 |         """Obtain a stake quote."""
569 |         payload = data.model_dump(exclude_none=True)['stake_quote_requests']
570 |         return await self._api_call("POST", "transactions/quote/", payload)
571 |     
572 |     async def unstake_quote(self, data: UnstakeQuoteRequestContainer) -> UnstakeQuoteRequestContainer:
573 |         """Obtain an unstake quote."""
574 |         payload = data.model_dump(exclude_none=True)['unstake_quote_requests']
575 |         return await self._api_call("POST", "transactions/quote/", payload)
576 | 
577 |     async def swap_transaction(self, data: SwapTransactionRequestContainer) -> List[SwapTransactionResponse]:
578 |         """Execute the swap transactions."""
579 |         # payload = [v.model_dump() for v in data.swap_transaction_requests]
580 |         payload = data.model_dump(exclude_none=True)['swap_transaction_requests']
581 |         return await self._api_call("POST", "transactions/swap/", payload)
582 |     
583 |     async def stake_transaction(self, data: StakeTransactionRequestContainer) -> StakeTransactionRequestContainer:
584 |         """Execute the stake transactions."""
585 |         payload = data.model_dump(exclude_none=True)['stake_transaction_requests']
586 |         return await self._api_call("POST", "transactions/swap/", payload)
587 |     
588 |     async def unstake_transaction(self, data: UnstakeTransactionRequestContainer) -> UnstakeTransactionRequestContainer:
589 |         """Execute the unstake transactions."""
590 |         payload = data.model_dump(exclude_none=True)['unstake_transaction_requests']
591 |         return await self._api_call("POST", "transactions/swap/", payload)
592 | 
593 |     async def get_all_wallets(self, data: ListWalletsRequest) -> List[Wallet]:
594 |         """Return all wallets with balances."""
595 |         return await self._api_call("GET", f"wallets/?is_archived={data.is_archived}")
596 |     
597 |     async def search_token(self, data: TokenSearchRequest) -> TokenSearchResponseContainer:
598 |         """Get details of a token."""
599 |         payload = data.model_dump(exclude_none=True)
600 |         return await self._api_call("POST", "tokens/search-token/", payload)
601 | 
602 |     async def get_official_token_address(self, data: TokenDetailsRequestContainer) -> TokenDetailsResponseContainer:
603 |         """Retrieve the mint address of token."""
604 |         payload = data.model_dump(exclude_none=True)['token_details_requests']
605 |         return await self._api_call("POST", "tokens/official-token-detail/", payload)
606 | 
607 |     async def list_groups(self) -> List[GroupInfo]:
608 |         """Return a list of wallet groups."""
609 |         return await self._api_call("GET", "wallets/groups/")
610 | 
611 |     async def list_single_group(self, data: ListSingleGroupRequest) -> SingleGroupInfo:
612 |         """Return details for a single wallet group."""
613 |         return await self._api_call("GET", f"wallets/groups/{data.group_name}/")
614 | 
615 |     async def create_wallet(self, data: CreateWalletRequestContainer) -> List[WalletInfo]:
616 |         """Create new wallets given a list of wallet names."""
617 |         # payload = json.dumps([{"name": wallet_name} for wallet_name in data.wallet_names])
618 |         payload = data.model_dump(exclude_none=True)['create_wallet_requests']
619 |         return await self._api_call("POST", "wallets/", payload)
620 | 
621 |     async def archive_wallets(self, data: ArchiveWalletsRequestContainer) -> List[WalletArchiveOrUnarchiveResponse]:
622 |         """Archive the wallets specified in the list."""
623 |         # payload = json.dumps([{"wallet": wallet_name} for wallet_name in data.wallet_names])
624 |         payload = data.model_dump(exclude_none=True)['archive_wallet_requests']
625 |         return await self._api_call("POST", "wallets/archive/", payload)
626 | 
627 |     async def unarchive_wallets(self, data: UnarchiveWalletsRequest) -> List[WalletArchiveOrUnarchiveResponse]:
628 |         """Unarchive the wallets specified in the list."""
629 |         # payload = json.dumps([{"wallet": wallet_name} for wallet_name in data.wallet_names])
630 |         payload = data.model_dump(exclude_none=True)['unarchive_wallet_requests']
631 |         return await self._api_call("POST", "wallets/unarchive/", payload)
632 | 
633 |     async def create_groups(self, data: CreateGroupsRequest) -> List[CreateGroupResponse]:
634 |         """Create new wallet groups given a list of group names."""
635 |         # payload = json.dumps([{"name": group_name} for group_name in data.group_names])
636 |         payload = data.model_dump(exclude_none=True)['create_groups_requests']
637 |         return await self._api_call("POST", "wallets/groups/", payload)
638 | 
639 |     async def add_wallets_to_group(self, data: AddWalletToGroupRequestContainer) -> List[AddWalletToGroupResponse]:
640 |         """Add wallets to a specific group."""
641 |         # payload = json.dumps([{"wallet": wallet_name, "group": data.group_name} for wallet_name in data.wallet_names])
642 |         payload = data.model_dump(exclude_none=True)['add_wallet_to_group_requests']
643 |         return await self._api_call("POST", "wallets/add-wallet-to-group/", payload)
644 | 
645 |     async def archive_wallet_group(self, data: ArchiveWalletGroupRequestContainer) -> List[GroupArchiveOrUnarchiveResponse]:
646 |         """Archive the specified wallet groups."""
647 |         # payload = json.dumps([{"group": group_name} for group_name in data.group_names])
648 |         payload = data.model_dump(exclude_none=True)['archive_wallet_group_requests']
649 |         return await self._api_call("POST", "wallets/group-archive/", payload)
650 | 
651 |     async def unarchive_wallet_group(self, data: UnarchiveWalletGroupRequestContainer) -> List[GroupArchiveOrUnarchiveResponse]:
652 |         """Unarchive the specified wallet groups."""
653 |         # payload = json.dumps([{"group": group_name} for group_name in data.group_names])
654 |         payload = data.model_dump(exclude_none=True)['unarchive_wallet_group_requests']
655 |         return await self._api_call("POST", "wallets/group-unarchive/", payload)
656 | 
657 |     async def remove_wallets_from_group(self, data: RemoveWalletsFromGroupRequestContainer) -> List[RemoveWalletFromGroupResponse]:
658 |         """Remove wallets from a group."""
659 |         # payload = json.dumps([{"wallet": wallet_name, "group": data.group_name} for wallet_name in data.wallet_names])
660 |         payload = data.model_dump(exclude_none=True)['remove_wallets_from_group_requests']
661 |         return await self._api_call("POST", "wallets/remove-wallet-from-group/", payload)
662 | 
663 |     async def transfer_tokens(self, data: TransferTokensRequestContainer) -> List[TransferTokenResponse]:
664 |         """Transfer tokens from one wallet to another."""
665 |         # payload = [v.model_dump() for v in data.transfer_tokens_requests]
666 |         payload = data.model_dump(exclude_none=True)['transfer_tokens_requests']
667 |         return await self._api_call("POST", "transfers/transfer/", payload)
668 | 
669 |     async def create_dca_order(self, data: DCAOrderRequestContainer) -> List[DCAOrderResponse]:
670 |         """Create a DCA order."""
671 |         # payload = [v.model_dump() for v in data.dca_order_requests]
672 |         payload = data.model_dump(exclude_none=True)['dca_order_requests']
673 |         return await self._api_call("POST", "transactions/dca-order/create/", payload)
674 | 
675 |     async def list_dca_orders(self, data: ListDCAOrderRequest) -> ListDCAOrderResponseContainer:
676 |         """List all DCA orders."""
677 |         payload = data.model_dump(exclude_none=True)
678 |         return await self._api_call("POST", f"transactions/dca-order/", payload)
679 | 
680 |     async def cancel_dca_order(self, data: CancelDCAOrderRequestContainer) -> List[CancelDCAOrderResponse]:
681 |         """Cancel a DCA order."""
682 |         # payload = [v.model_dump() for v in data.cancel_dca_order_requests]
683 |         payload = data.model_dump(exclude_none=True)['cancel_dca_order_requests']
684 |         return await self._api_call("POST", "transactions/dca-order/cancel/", payload)
685 |     
686 |     async def create_order(self, data: CreateOrderRequestContainer) -> CreateOrderResponseContainer:
687 |         """Create a order."""
688 |         payload = data.model_dump(exclude_none=True)['create_order_requests']
689 |         return await self._api_call("POST", "transactions/order/create/", payload)
690 |     
691 |     async def list_orders(self, data: ListOrderRequest) -> ListOrderResponseContainer:
692 |         """List all orders."""
693 |         payload = data.model_dump(exclude_none=True)
694 |         return await self._api_call("POST", f"transactions/order/", payload)
695 |     
696 |     async def cancel_order(self, data: CancelOrderRequestContainer) -> CancelOrderResponseContainer:
697 |         """Cancel a order."""
698 |         payload = data.model_dump(exclude_none=True)['cancel_order_requests']
699 |         return await self._api_call("POST", "transactions/order/cancel/", payload) 
700 |     
701 |     async def top_trending_tokens(self, data: TopTrendingTokensRequest) -> List:
702 |         """Get the top trending tokens."""
703 |         payload = data.model_dump(exclude_none=True)
704 |         return await self._api_call("POST", f"tokens/trending/", payload)
705 |     
706 |     async def get_stake_balances(self) -> StakeBalanceResponse:
707 |         """Get the stake balances."""
708 |         return await self._api_call("GET", "frontend/wallets/stake/balance/")
709 |     
710 |     async def rename_wallet(self, data: RenameWalletRequestContainer) -> List:
711 |         """Rename a wallet."""
712 |         payload = data.model_dump(exclude_none=True)['rename_wallet_requests']
713 |         return await self._api_call("POST", "wallets/rename/", payload)
714 |     
715 |     async def get_market_candle_data(self, data: CandleStickRequest) -> Dict:
716 |         """Get the candle sticks."""
717 |         payload = data.model_dump(exclude_none=True)
718 |         return await self._api_call("POST", f"tokens/candles/", payload)
719 |     
720 |     async def send_key_to_telegram(self, data: PrivateKeyRequest) -> Dict:
721 |         """Send the mnemonic or private key to telegram."""
722 |         payload = data.model_dump(exclude_none=True)
723 |         return await self._api_call("POST", f"users/telegram/send-message/", payload)
724 | 
725 | # ------------------------------
726 | # Utility Functions
727 | # ------------------------------   
728 |     
729 | def calculate(expr: str, variables: dict = None) -> float:
730 |     """
731 |     Evaluate a math/stat expression with support for variables and common functions.
732 |     """
733 |     variables = variables or {}
734 |     # Allowed names from math and statistics
735 |     safe_names = {
736 |         k: v for k, v in vars(math).items() if not k.startswith("__")
737 |     }
738 |     safe_names.update({
739 |         'mean': statistics.mean,
740 |         'median': statistics.median,
741 |         'stdev': statistics.stdev,
742 |         'variance': statistics.variance,
743 |         'sum': sum,
744 |         'min': min,
745 |         'max': max,
746 |         'len': len,
747 |         'abs': abs,
748 |         'round': round
749 |     })
750 | 
751 |     # Safe operators
752 |     ops = {
753 |         ast.Add: operator.add,
754 |         ast.Sub: operator.sub,
755 |         ast.Mult: operator.mul,
756 |         ast.Div: operator.truediv,
757 |         ast.FloorDiv: operator.floordiv,
758 |         ast.Mod: operator.mod,
759 |         ast.Pow: operator.pow,
760 |         ast.USub: operator.neg
761 |     }
762 |     def _eval(node):
763 |         if isinstance(node, ast.Num):
764 |             return node.n
765 |         elif isinstance(node, ast.Constant):  # Python 3.8+
766 |             return node.value
767 |         elif isinstance(node, ast.BinOp):
768 |             return ops[type(node.op)](_eval(node.left), _eval(node.right))
769 |         elif isinstance(node, ast.UnaryOp):
770 |             return ops[type(node.op)](_eval(node.operand))
771 |         elif isinstance(node, ast.Name):
772 |             if node.id in variables:
773 |                 return variables[node.id]
774 |             elif node.id in safe_names:
775 |                 return safe_names[node.id]
776 |             else:
777 |                 raise NameError(f"Unknown variable or function: {node.id}")
778 |         elif isinstance(node, ast.Call):
779 |             func = _eval(node.func)
780 |             args = [_eval(arg) for arg in node.args]
781 |             return func(*args)
782 |         elif isinstance(node, ast.List):
783 |             return [_eval(elt) for elt in node.elts]
784 |         else:
785 |             raise TypeError(f"Unsupported expression type: {type(node)}")
786 | 
787 |     parsed = ast.parse(expr, mode="eval")
788 |     return _eval(parsed.body)
789 | 
```