diff --git a/bot/account.py b/bot/account.py index 1c1aa26..0044a44 100644 --- a/bot/account.py +++ b/bot/account.py @@ -1,5 +1,5 @@ from typing import Optional, Union, Generator, Any -import requests, datetime, re, logging, os, json, ast, shutil +import requests, datetime, re, logging, os, json, ast, shutil, eth_abi import pandas as pd from PIL import Image from PIL.JpegImagePlugin import JpegImageFile @@ -25,6 +25,10 @@ class Account: "account": "accounts", "identity": "multiaccounts" } + __keccak256_selectors = { + "transfer": "a9059cbb" # keccak256("transfer(address,uint256)")[:4] + } + def __init__(self, domain: str = "localhost", port: int = 8080, is_secure: bool = False): """ Work with your own Status App account @@ -77,6 +81,7 @@ class Account: "create_backup": f"{self.__http_base_url}PerformLocalBackup", "load_backup": f"{self.__http_base_url}LoadLocalBackup", "rpc": f"{self.__http_base_url}CallRPC", + "transaction": f"{self.__http_base_url}SendTransactionV2" }, "socket": { "signals": f"{self.__ws_base_url}signals" @@ -995,6 +1000,63 @@ class Account: market_info = market_info.rename(columns=column_mapping)[list(column_mapping.values())] return market_info.copy() + def send_transaction(self, address: str, symbol: str, amount: float, chain_id: int = 1) -> Optional[str]: + """ + Send crypto to specified `address` + + Parameters: + - `address` - the wallet address of the receiver + - `symbol` - either a valid Status token symbol from `def get_tokens()` or its address + - `amount` - the amount that will be sent to the `address` + - `chain_id` - valid Chain from `self.chains` + + Output: + - Transaction hash that to monitor the transactions progress + """ + is_eth = symbol == "ETH" + is_address = symbol.startswith("0x") + symbol = symbol.upper() + tokens = self.get_tokens()[["chain_id", "address", "symbol", "decimals"]].drop_duplicates().reset_index(drop=True) + query = (tokens["address" if is_address else "symbol"] == symbol) & (tokens["chain_id"] == chain_id) + if query.sum() == 0: + raise Exception(f"Given {'address' if is_address else 'symbol'} {symbol} on chain ID {chain_id} does not exist...") + + token_info = tokens.loc[query].to_dict("records")[0] + + balance = self.balance + query = (balance["address"] == token_info["address"]) & (balance["chain_id"] == chain_id) + if query.sum() == 0: + raise Exception(f"Given {'address' if is_address else 'symbol'} {symbol} on chain ID {chain_id} was not found in your wallet ({self.info['wallet_address']})...") + + wallet_amount = balance.loc[query].reset_index(drop=True)["amount"].iloc[0] + if amount > wallet_amount: + raise Exception(f"Given {'address' if is_address else 'symbol'} {symbol} on chain ID {chain_id} has {wallet_amount} but you are trying to send {amount}...") + + raw_amount = int(amount * (10**token_info["decimals"])) + + tx = { + "version": 1, + "from": self.info["wallet_address"], + "to": address if is_eth else token_info["address"], + "value": hex(raw_amount) if is_eth else "0x0", + "fromChainID": chain_id, + "toChainID": chain_id, + } + if not is_eth: + encoded_args = eth_abi.encode(["address", "uint256"], [address, raw_amount]).hex() + tx["data"] = "0x" + self.__keccak256_selectors["transfer"] + encoded_args + + payload = { + "password": self.info["password"], + "txArgs": tx + } + response = requests.post(self.__urls["http"]["transaction"], json=payload) + transaction_hash: str = response.json().get("result") + + url = f"http://etherscan.io/tx/{transaction_hash}" + self.logger.info(f"Transaction: {url}") + return transaction_hash + def __start_messenger(self): """ Start the decentralized messaging service. diff --git a/docs/account.md b/docs/account.md index 70d2be3..35ed053 100644 --- a/docs/account.md +++ b/docs/account.md @@ -583,6 +583,127 @@ Returns `pd.DataFrame`. | `pct_change_24hr` | `float` | Percentage price change over the last 24 hours. | +#### `send_transaction(address, symbol, amount, chain_id=1)` + +Send crypto from the logged-in account's wallet to another wallet address on the same chain. This method supports both **ETH** and **ERC-20** tokens. The token can be identified either by its Status symbol (e.g. `ETH`, `SNT`, `USDT`) or by its contract address. Before broadcasting, the method validates that the token exists on the given chain and that the wallet holds enough balance for the requested `amount`. + +| Name | Type | Required | Description | +|-----|-----|-----|-------------| +| `address` | `str` | Yes | The wallet address of the receiver. | +| `symbol` | `str` | Yes | Either a valid Status token symbol from [`get_tokens`](./account.md#get_tokens) or the token's contract address (must start with `0x`). | +| `amount` | `float` | Yes | The amount of the token to send. Must be less than or equal to the wallet's current balance for that token. | +| `chain_id` | `int` | No | Chain ID where the transaction will be broadcast. Defaults to `1` (Ethereum mainnet). All available chain IDs can be obtained from the [`chains`](./account.md#chains) property. | + +Returns `str` representing the **transaction hash**. The hash can be appended to `https://etherscan.io/tx/` to monitor the transaction's progress. The transaction URL is also written to [`logger`](./account.md#logger) at `INFO` level. If the backend fails to broadcast and does not return a hash, `None` is returned instead. + +Send ETH: + +```python +from bot import Account + +account = Account() + +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP", + "infura_token" : "token from https://www.infura.io/", + "coingecko_api_key": "API key from https://www.coingecko.com/" +} +account.login(**params) + +vitalik_address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + +tx_hash = account.send_transaction( + address=vitalik_address, + symbol="ETH", + amount=0.01 +) +print(f"Transaction: https://etherscan.io/tx/{tx_hash}") +``` + +Send an ERC-20 token by symbol: + +```python +from bot import Account + +account = Account() + +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP", + "infura_token" : "token from https://www.infura.io/", + "coingecko_api_key": "API key from https://www.coingecko.com/" +} +account.login(**params) + +vitalik_address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + +tx_hash = account.send_transaction( + address=vitalik_address, + symbol="SNT", + amount=10 +) +``` + +Send an ERC-20 token by contract address: + +```python +from bot import Account + +account = Account() + +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP", + "infura_token" : "token from https://www.infura.io/", + "coingecko_api_key": "API key from https://www.coingecko.com/" +} +account.login(**params) + +vitalik_address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" +snt_address = "0x744d70fdbe2ba4cf95131626614a1763df805b9e" + +tx_hash = account.send_transaction( + address=vitalik_address, + symbol=snt_address, + amount=10 +) +``` + +Send on a different chain: + +```python +from bot import Account + +account = Account() + +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP", + "infura_token" : "token from https://www.infura.io/", + "coingecko_api_key": "API key from https://www.coingecko.com/" +} +account.login(**params) + +vitalik_address = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + +tx_hash = account.send_transaction( + address=vitalik_address, + symbol="ETH", + amount=0.01, + chain_id=10 # Optimism +) +``` + +**Note**: This is a wallet method, so it requires both `infura_token` and `coingecko_api_key` to be provided in [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennonecoingecko_api_keynone). If either is missing, an exception will be raised when this method is called. + +**Note**: The sender and receiver must be on the **same chain**. Cross-chain transfers are not supported by this method — set `chain_id` to the chain where the funds currently exist. + +**Note**: An **exception will be raised** when: +- the `symbol` (or contract address) does not exist on the given `chain_id` +- the token is not present in the logged-in wallet's balance +- the requested `amount` exceeds the current wallet balance + ## Properties ### `available_accounts` diff --git a/requirements.txt b/requirements.txt index 7ab5331..43cb841 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ websocket-client websockets pandas pillow +eth-abi