diff --git a/.env.example b/.env.example index 7ead704..f096ed8 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,16 @@ -BITCOIN_RPC_URL="" -BITCOIN_RPC_USERNAME="" -BITCOIN_RPC_PASSWORD="" +ELECTRS_DAEMON_RPC_ADDR="" +ELECTRS_DAEMON_P2P_ADDR="" + +ELECTRUM_RPC_URL="" +ELECTRUM_RPC_PASSWORD="" +BITCOIN_WALLET_SEED="" + MONERO_RPC_URL="" MONERO_RPC_USERNAME="" MONERO_RPC_PASSWORD="" +MONERO_WALLET_SEED="" +MONERO_WALLET_PASSWORD="" +MONERO_WALLET_HEIGHT="" + KRAKEN_API_KEY="" -KRAKEN_API_SECRET="" \ No newline at end of file +KRAKEN_API_SECRET="" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..3fe0d7c --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy autoforward and autoconvert programs + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + environment: production + + steps: + - uses: actions/checkout@v4 + - uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - name: Deploy + run: | + ssh -o StrictHostKeyChecking=no ${{ secrets.VPS_USER }}@${{ secrets.VPS_IP }} << 'EOF' + export HISTFILE=/dev/null + cd autoforward-autoconvert + git checkout main + echo "Pulling changes..." + git pull + echo "Starting..." + + ELECTRS_DAEMON_RPC_ADDR=${{ secrets.ELECTRS_DAEMON_RPC_ADDR }} \ + ELECTRS_DAEMON_P2P_ADDR=${{ secrets.ELECTRS_DAEMON_P2P_ADDR }} \ + ELECTRUM_RPC_URL=${{ secrets.ELECTRUM_RPC_URL }} \ + ELECTRUM_RPC_PASSWORD=${{ secrets.ELECTRUM_RPC_PASSWORD }} \ + BITCOIN_WALLET_SEED=${{ secrets.BITCOIN_WALLET_SEED }} \ + MONERO_RPC_URL=${{ secrets.MONERO_RPC_URL }} \ + MONERO_RPC_USERNAME=${{ secrets.MONERO_RPC_USERNAME }} \ + MONERO_RPC_PASSWORD=${{ secrets.MONERO_RPC_PASSWORD }} \ + MONERO_WALLET_SEED=${{ secrets.MONERO_WALLET_SEED }} \ + MONERO_WALLET_PASSWORD=${{ secrets.MONERO_WALLET_PASSWORD }} \ + MONERO_WALLET_HEIGHT=${{ secrets.MONERO_WALLET_HEIGHT }} \ + KRAKEN_API_KEY=${{ secrets.KRAKEN_API_KEY }} \ + KRAKEN_API_SECRET=${{ secrets.KRAKEN_API_SECRET }} \ + docker compose up -d --build + EOF diff --git a/.gitignore b/.gitignore index c2eabec..74df079 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv -.env \ No newline at end of file +.env +.electrs-cookie \ No newline at end of file diff --git a/autoforward.py b/autoforward.py deleted file mode 100644 index 94abfca..0000000 --- a/autoforward.py +++ /dev/null @@ -1,111 +0,0 @@ -import requests -import json -from time import sleep -from datetime import datetime -from requests.auth import HTTPDigestAuth - -from helpers import * - -BITCOIN_RPC_URL = os.getenv('BITCOIN_RPC_URL') -BITCOIN_RPC_USERNAME = os.getenv('BITCOIN_RPC_USERNAME') -BITCOIN_RPC_PASSWORD = os.getenv('BITCOIN_RPC_PASSWORD') -MONERO_RPC_URL = os.getenv('MONERO_RPC_URL') -MONERO_RPC_USERNAME = os.getenv('MONERO_RPC_USERNAME') -MONERO_RPC_PASSWORD = os.getenv('MONERO_RPC_PASSWORD') - -def request_bitcoin_rpc(method: str, params: list[str] = []) -> any: - headers = {'content-type': 'application/json'} - - data = { - 'jsonrpc': '1.0', - 'id': 'python-bitcoin', - 'method': 'getbalance', - 'params': params, - } - - return requests.post( - BITCOIN_RPC_URL, - headers=headers, - data=json.dumps(data), - auth=(BITCOIN_RPC_USERNAME, BITCOIN_RPC_PASSWORD) - ).json()['result'] - -def request_monero_rpc(method: str, params: dict[str, any] = {}) -> any: - headers = {'content-type': 'application/json'} - - data = { - 'jsonrpc': '2.0', - 'id': '0', - 'method': method, - 'params': params - } - - return requests.post( - MONERO_RPC_URL, - headers=headers, - json=data, - auth=HTTPDigestAuth(MONERO_RPC_USERNAME, MONERO_RPC_PASSWORD) - ).json()['result'] - -def get_bitcoin_fee_rate() -> int: - return requests.get('https://mempool.space/api/v1/fees/recommended').json()['halfHourFee'] - -def get_bitcoin_balance() -> float: - return request_bitcoin_rpc('getbalance') - -def send_bitcoin(address: str, amount: float, fee_rate: int) -> None: - # https://bitcoincore.org/en/doc/24.0.0/rpc/wallet/sendtoaddress/ - params = [address, amount, null, null, true, true, null, null, null, fee_rate] - request_bitcoin_rpc('sendtoaddress', params) - -def get_monero_balance() -> float: - params = {'account_index': 0} - - return request_monero_rpc('get_balance', params)['balance'] - -def sweep_all_monero(address: str) -> None: - params = { - 'account_index': 0, - 'address': address, - } - - request_monero_rpc('sweep_all', params) - -def get_new_kraken_address(asset: 'XBT' | 'XMR') -> str: - payload = { - 'asset': asset, - 'method': 'Bitcoin' if asset == 'XBT' else 'Monero' - } - - result = kraken_request('/0/private/DepositAddresses', payload) - adddress = next((address['address'] for address in result['result'] if address['new']), None) - - return address - -while 1: - try: - balance = get_bitcoin_balance() - - if balance > 0: - fee_rate = get_bitcoin_fee_rate() - address = get_new_kraken_address('XBT') - amount = balance - send_bitcoin(address, amount, fee_rate) - print(get_time(), f'Sent {amount} BTC to {address}!') - else: - print(get_time(), 'No bitcoin balance to sweep.') - except Exception as e: - print(get_time(), 'Error autoforwarding Bitcoin:', e) - - try: - balance = get_monero_balance() - - if balance > 0: - address = get_new_kraken_address('XMR') - sweep_all_monero(address) - else: - print(get_time(), 'No bitcoin balance to sweep.') - except Exception as e: - print(get_time(), 'Error autoforwarding Monero:', e) - - sleep(60 * 5) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..54fe4c9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,85 @@ +services: + electrum-client: + build: + context: ./electrum-client + args: + VERSION: "4.5.5" + CHECKSUM_SHA512: "3bdfce2187466fff20fd67736bdf257bf95d3517de47043be411ccda558a442b8fd81d6a8da094a39a1db39a7339dcd4282e73a7f00cf6bbd70473d7ce456b0b" + container_name: electrum-client + restart: unless-stopped + environment: + - ELECTRUM_USER=user + - ELECTRUM_PASSWORD=${ELECTRUM_RPC_PASSWORD} + ports: + - 7000:7000 + + monero-wallet-rpc: + image: sethsimmons/simple-monero-wallet-rpc:latest + restart: unless-stopped + container_name: monero-wallet-rpc + volumes: + - monero-wallet-rpc-data:/home/monero + command: + - "--trusted-daemon" + - "--rpc-bind-port=18082" + - "--rpc-login=monero:monero" + - "--daemon-address=xmr.tcpcat.net:18089" + - "--wallet-dir=/home/monero/wallet" + - "--log-level=4" + + seed-importer: + image: curlimages/curl + container_name: seed-importer + environment: + - ELECTRUM_RPC_URL=${ELECTRUM_RPC_URL} + - ELECTRUM_RPC_PASSWORD=${ELECTRUM_RPC_PASSWORD} + - BITCOIN_WALLET_SEED=${BITCOIN_WALLET_SEED} + - MONERO_RPC_URL=${MONERO_RPC_URL} + - MONERO_RPC_USERNAME=${MONERO_RPC_USERNAME} + - MONERO_RPC_PASSWORD=${MONERO_RPC_PASSWORD} + - MONERO_WALLET_SEED=${MONERO_WALLET_SEED} + - MONERO_WALLET_PASSWORD=${MONERO_WALLET_PASSWORD} + - MONERO_WALLET_HEIGHT=${MONERO_WALLET_HEIGHT} + volumes: + - ./src/seed-importer.sh:/script/seed-importer.sh + command: /bin/sh /script/seed-importer.sh + depends_on: + - electrum_client + - monero-wallet-rpc + + autoforward: + image: python:3.12-alpine + container_name: autoforward + restart: unless-stopped + environment: + - ELECTRUM_RPC_URL=${ELECTRUM_RPC_URL} + - ELECTRUM_RPC_PASSWORD=${ELECTRUM_RPC_PASSWORD} + - BITCOIN_WALLET_SEED=${BITCOIN_WALLET_SEED} + - MONERO_RPC_URL=${MONERO_RPC_URL} + - MONERO_RPC_USERNAME=${MONERO_RPC_USERNAME} + - MONERO_RPC_PASSWORD=${MONERO_RPC_PASSWORD} + - MONERO_WALLET_SEED=${MONERO_WALLET_SEED} + - MONERO_WALLET_PASSWORD=${MONERO_WALLET_PASSWORD} + - MONERO_WALLET_HEIGHT=${MONERO_WALLET_HEIGHT} + - KRAKEN_API_KEY=${KRAKEN_API_KEY} + - KRAKEN_API_SECRET=${KRAKEN_API_SECRET} + volumes: + - ./requirements.txt:/app/requirements.txt + - ./src:/app/src + command: pip install -r /app/requirements.txt && python /app/src/autoforward.py + + autoconvert: + image: python:3.12-alpine + container_name: autoconvert + restart: unless-stopped + environment: + - KRAKEN_API_KEY=${KRAKEN_API_KEY} + - KRAKEN_API_SECRET=${KRAKEN_API_SECRET} + volumes: + - ./requirements.txt:/app/requirements.txt + - ./src:/app/src + command: pip install -r /app/requirements.txt && python /app/src/autoconvert.py + +volumes: + electrs_data: + monero-wallet-rpc-data: diff --git a/electrum-client/Dockerfile b/electrum-client/Dockerfile new file mode 100644 index 0000000..15b5f91 --- /dev/null +++ b/electrum-client/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.12-alpine + +ARG VERSION +ARG CHECKSUM_SHA512 + +ENV ELECTRUM_VERSION $VERSION +ENV ELECTRUM_USER electrum +ENV ELECTRUM_HOME /home/$ELECTRUM_USER +ENV ELECTRUM_NETWORK mainnet + +RUN adduser -D $ELECTRUM_USER + +RUN mkdir -p /data ${ELECTRUM_HOME} && \ + ln -sf /data ${ELECTRUM_HOME}/.electrum && \ + chown ${ELECTRUM_USER} ${ELECTRUM_HOME}/.electrum /data + +# IMPORTANT: always verify gpg signature before changing a hash here! +ENV ELECTRUM_CHECKSUM_SHA512 $CHECKSUM_SHA512 + +RUN apk --no-cache add --virtual build-dependencies gcc musl-dev libsecp256k1 libsecp256k1-dev libressl-dev +RUN wget https://download.electrum.org/${ELECTRUM_VERSION}/Electrum-${ELECTRUM_VERSION}.tar.gz +RUN [ "${ELECTRUM_CHECKSUM_SHA512} Electrum-${ELECTRUM_VERSION}.tar.gz" = "$(sha512sum Electrum-${ELECTRUM_VERSION}.tar.gz)" ] +RUN echo -e "**************************\n SHA 512 Checksum OK\n**************************" +RUN pip3 install cryptography Electrum-${ELECTRUM_VERSION}.tar.gz +RUN rm -f Electrum-${ELECTRUM_VERSION}.tar.gz + +RUN mkdir -p /data \ + ${ELECTRUM_HOME}/.electrum/wallets/ \ + ${ELECTRUM_HOME}/.electrum/testnet/wallets/ \ + ${ELECTRUM_HOME}/.electrum/regtest/wallets/ \ + ${ELECTRUM_HOME}/.electrum/simnet/wallets/ && \ + ln -sf ${ELECTRUM_HOME}/.electrum/ /data && \ + chown -R ${ELECTRUM_USER} ${ELECTRUM_HOME}/.electrum /data + +USER $ELECTRUM_USER +WORKDIR $ELECTRUM_HOME +VOLUME /data +EXPOSE 7000 + +COPY docker-entrypoint.sh /usr/local/bin/ +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/electrum-client/docker-entrypoint.sh b/electrum-client/docker-entrypoint.sh new file mode 100755 index 0000000..cbe3e67 --- /dev/null +++ b/electrum-client/docker-entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -ex + +trap 'pkill -TERM -P1; electrum stop; exit 0' SIGTERM + +rm -f .electrum/daemon +electrum --offline setconfig rpcuser ${ELECTRUM_USER} +electrum --offline setconfig rpcpassword ${ELECTRUM_PASSWORD} +electrum --offline setconfig rpchost 0.0.0.0 +electrum --offline setconfig rpcport 7000 +electrum daemon "$@" \ No newline at end of file diff --git a/helpers.py b/helpers.py deleted file mode 100644 index 5b4ecc0..0000000 --- a/helpers.py +++ /dev/null @@ -1,31 +0,0 @@ -import urllib -import hashlib -import hmac -import base64 -import requests - -KRAKEN_API_KEY = os.getenv('KRAKEN_API_KEY') -KRAKEN_API_SECRET = os.getenv('KRAKEN_API_SECRET') - -def get_time() -> str: - return f'[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}]' - -def get_kraken_signature(url: str, payload: dict): - postdata = urllib.parse.urlencode(payload) - encoded = (str(payload['nonce']) + postdata).encode() - message = url.encode() + hashlib.sha256(encoded).digest() - mac = hmac.new(base64.b64decode(KRAKEN_API_SECRET), message, hashlib.sha512) - sigdigest = base64.b64encode(mac.digest()) - return sigdigest.decode() - -def kraken_request(path: str, payload = {}) -> requests.Request: - payload['nonce'] = str(int(1000*time.time())) - headers = {} - headers['API-Key'] = KRAKEN_API_KEY - headers['API-Sign'] = get_kraken_signature(path, payload) - - return requests.post( - 'https://api.kraken.com' + path, - headers=headers, - data=payload - ).json()['result'] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a7be810 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +requests +bip_utils \ No newline at end of file diff --git a/autoconvert.py b/src/autoconvert.py similarity index 81% rename from autoconvert.py rename to src/autoconvert.py index b6560ec..00f02b2 100644 --- a/autoconvert.py +++ b/src/autoconvert.py @@ -1,7 +1,8 @@ import time -import os +import random +from typing import Literal, cast -from helpers import * +from util import * MAX_SLIPPAGE_PERCENT = 1 @@ -10,7 +11,7 @@ order_min = { 'XMR': 0.03 } -def get_balance(asset: 'XBT' | 'XMR') -> str: +def get_balance(asset: Literal['XBT', 'XMR']) -> str: balances = kraken_request('/0/private/Balance') balance = '0' @@ -19,10 +20,10 @@ def get_balance(asset: 'XBT' | 'XMR') -> str: return balance -def get_bids(asset: 'XBT' | 'XMR'): - return kraken_request('/0/public/Depth', {pair: f'{asset}USD'})[f'X{asset}ZUSD']['bids'] +def get_bids(asset: Literal['XBT', 'XMR']): + return kraken_request('/0/public/Depth', {'pair': f'{asset}USD'})[f'X{asset}ZUSD']['bids'] -def attempt_sell(asset: 'XBT' | 'XMR'): +def attempt_sell(asset: Literal['XBT', 'XMR']): balance = float(get_balance(asset)) to_sell_amount = 0 @@ -30,7 +31,7 @@ def attempt_sell(asset: 'XBT' | 'XMR'): print(get_time(), f'No enough {asset} balance to sell.') return - bids = get_bids() + bids = get_bids(asset) market_price = float(bids[0][0]) for bid in bids: @@ -66,7 +67,7 @@ def attempt_sell(asset: 'XBT' | 'XMR'): while 1: for asset in ['XBT', 'XMR']: try: - attempt_sell(asset) + attempt_sell(cast(Literal['XBT', 'XMR'], asset)) except Exception as e: print(get_time(), f'Error attempting to sell {asset}:', e) diff --git a/src/autoforward.py b/src/autoforward.py new file mode 100644 index 0000000..68cafe0 --- /dev/null +++ b/src/autoforward.py @@ -0,0 +1,149 @@ +from typing import Literal, cast +from time import sleep +import requests +import json + +from constants import MAX_BITCOIN_FEE_PERCENT, MIN_BITCOIN_SEND_AMOUNT, MIN_MONERO_SEND_AMOUNT +import util +import env + +def get_bitcoin_fee_rate() -> int: + return requests.get('https://mempool.space/api/v1/fees/recommended').json()['halfHourFee'] + +def open_bitcoin_wallet(): + util.request_electrum_rpc('open_wallet') + +def set_bitcoin_fee_rate(rate: int): + util.request_electrum_rpc('setconfig', ['dynamic_fees', False]) + util.request_electrum_rpc('setconfig', ['fee_per_kb', rate * 1000]) + +def get_bitcoin_balance() -> float: + return float(util.request_electrum_rpc('getbalance')['confirmed']) + +def create_psbt(destination_address: str) -> str: + params = { + 'destination': destination_address, + 'amount': '!', + 'unsigned': True # This way we can get the input amounts + } + + response = util.request_electrum_rpc('payto', params) + return response['result'] + +def get_psbt_data(psbt: str) -> dict: + return util.request_electrum_rpc('deserialize', [psbt]) + +def get_total_psbt_fee(psbt_data: dict) -> float: + inputs_sum_sats = 0 + outputs_sum_sats = 0 + + for _input in psbt_data['inputs']: + inputs_sum_sats += cast(int, _input['value_sats']) + + for _output in psbt_data['outputs']: + outputs_sum_sats += cast(int, _output['value_sats']) + + total_fee_sats = inputs_sum_sats - outputs_sum_sats + total_fee_btc = total_fee_sats / 100000000 + return total_fee_btc + +def sign_psbt(psbt: str) -> str: + return cast(str, util.request_electrum_rpc('signtransaction', [psbt])) + +def broadcast_bitcoin_tx(signed_tx: str): + util.request_electrum_rpc('broadcast', [signed_tx]) + +def open_monero_wallet() -> None: + params = {'filename': 'wallet', 'password': env.MONERO_WALLET_PASSWORD} + util.request_monero_rpc('open_wallet', params) + +def get_monero_balance() -> float: + params = {'account_index': 0} + return util.request_monero_rpc('get_balance', params)['balance'] / 1000000000000 + +def sweep_all_monero(address: str) -> None: + params = { + 'account_index': 0, + 'address': address, + } + + util.request_monero_rpc('sweep_all', params) + +def get_new_kraken_address(asset: Literal['XBT', 'XMR']) -> str: + payload = { + 'asset': asset, + 'method': 'Bitcoin' if asset == 'XBT' else 'Monero' + } + + result = util.kraken_request('/0/private/DepositAddresses', payload) + + for address in result['result']: + if address['new']: + return address['address'] + + raise Exception(f'Kraken did not return a new address: {json.dumps(result, indent=2)}') + +def attempt_bitcoin_autoforward(): + open_bitcoin_wallet() + balance = get_bitcoin_balance() + + if balance < MIN_BITCOIN_SEND_AMOUNT: + print(util.get_time(), 'No enough bitcoin balance to autoforward.') + return + + fee_rate = get_bitcoin_fee_rate() + set_bitcoin_fee_rate(fee_rate) + address = get_new_kraken_address('XBT') + + try: + psbt = create_psbt(address) + except requests.exceptions.HTTPError as http_error: + response_json = cast(dict, http_error.response.json()) + + if response_json.get('error', {}).get('data', {}).get('exception', '') == 'NotEnoughFunds()': + print(util.get_time(), f'Not autoforwarding due to high transaction fee.') + return + + raise http_error + + if psbt == None: + print(util.get_time(),f'Not autoforwarding due to high transaction fee.') + return + + psbt_data = get_psbt_data(psbt) + total_fee = get_total_psbt_fee(psbt_data) + amount = balance + + if total_fee / amount * 100 > MAX_BITCOIN_FEE_PERCENT: + print(util.get_time(),f'Not autoforwarding due to high transaction fee.') + return + + signed_tx = sign_psbt(psbt) + broadcast_bitcoin_tx(signed_tx) + + print(util.get_time(), f'Autoforwarded {amount} BTC to {address}!') + +def attempt_monero_autoforward(): + balance = get_monero_balance() + + if balance < MIN_MONERO_SEND_AMOUNT: + print(util.get_time(), 'No enough monero balance to autoforward.') + return + + address = get_new_kraken_address('XMR') + sweep_all_monero(address) + print(util.get_time(), f'Autoforwarded {balance} XMR to {address}!') + +while 1: + try: + attempt_bitcoin_autoforward() + except Exception as e: + print(util.get_time(), 'Error autoforwarding bitcoin:', e) + + try: + attempt_monero_autoforward() + except Exception as e: + print(util.get_time(), 'Error autoforwarding monero:', e) + + sleep(60 * 5) + diff --git a/src/constants.py b/src/constants.py new file mode 100644 index 0000000..09bb41d --- /dev/null +++ b/src/constants.py @@ -0,0 +1,3 @@ +MIN_BITCOIN_SEND_AMOUNT = 0.0001 +MAX_BITCOIN_FEE_PERCENT = 10 +MIN_MONERO_SEND_AMOUNT = 0.01 \ No newline at end of file diff --git a/src/env.py b/src/env.py new file mode 100644 index 0000000..ca45478 --- /dev/null +++ b/src/env.py @@ -0,0 +1,15 @@ +import os + +ELECTRUM_RPC_URL = os.getenv('ELECTRUM_RPC_URL', 'http://electrs:7000') +ELECTRUM_RPC_PASSWORD = os.getenv('ELECTRUM_RPC_PASSWORD', '') +BITCOIN_WALLET_SEED = os.getenv('BITCOIN_WALLET_SEED', '') + +MONERO_RPC_URL = os.getenv('MONERO_RPC_URL', 'http://monero-wallet-rpc:18082/json_rpc') +MONERO_RPC_USERNAME = os.getenv('MONERO_RPC_USERNAME', '') +MONERO_RPC_PASSWORD = os.getenv('MONERO_RPC_PASSWORD', '') +MONERO_WALLET_SEED = os.getenv('MONERO_WALLET_SEED', '') +MONERO_WALLET_PASSWORD = os.getenv('MONERO_WALLET_PASSWORD', '') +MONERO_WALLET_HEIGHT = os.getenv('MONERO_WALLET_HEIGHT', '') + +KRAKEN_API_KEY = os.getenv('KRAKEN_API_KEY', '') +KRAKEN_API_SECRET = os.getenv('KRAKEN_API_SECRET', '') diff --git a/src/seed-importer.py b/src/seed-importer.py new file mode 100644 index 0000000..186b5a6 --- /dev/null +++ b/src/seed-importer.py @@ -0,0 +1,36 @@ +from bip_utils import Bip39SeedGenerator, Bip84, Bip84Coins + +import util +import env + +def get_zprv_from_seed(mnemonic: str) -> str: + seed_bytes = Bip39SeedGenerator(mnemonic).Generate() + bip84_master_key = Bip84.FromSeed(seed_bytes, Bip84Coins.BITCOIN) + zprv = bip84_master_key.Purpose().Coin().Account(0).PrivateKey().ToExtended() + return zprv + +def import_bitcoin_seed(): + zprv = get_zprv_from_seed(env.BITCOIN_WALLET_SEED) + util.request_electrum_rpc('restore', [zprv]) + +def import_monero_seed(): + monero_params = { + 'filename': 'wallet', + 'seed': env.MONERO_WALLET_SEED, + 'password': env.MONERO_RPC_PASSWORD, + 'restore_height': env.MONERO_WALLET_HEIGHT, + 'language': 'english', + 'autosave_current': True + } + + util.request_monero_rpc('restore_deterministic_wallet', monero_params) + +try: + import_bitcoin_seed() +except Exception as e: + print(util.get_time(), 'Error importing bitcoin seed:', e) + +try: + import_monero_seed() +except Exception as e: + print(util.get_time(), 'Error importing monero seed:', e) \ No newline at end of file diff --git a/src/util.py b/src/util.py new file mode 100644 index 0000000..2415e7b --- /dev/null +++ b/src/util.py @@ -0,0 +1,86 @@ +from requests.auth import HTTPDigestAuth +from datetime import datetime +import urllib.parse +import requests +import hashlib +import base64 +import hmac +import json +import time + +import env + +def get_time() -> str: + return f'[{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}]' + +def request_electrum_rpc(method: str, params: list | dict = []) -> dict: + headers = {'content-type': 'application/json'} + + data = { + 'jsonrpc': '2.0', + 'id': '0', + 'method': method, + 'params': params, + } + + response = requests.post( + env.ELECTRUM_RPC_URL, + headers=headers, + data=json.dumps(data), + auth=('user', env.ELECTRUM_RPC_PASSWORD) + ) + + response_json = response.json() + + if 'error' in response_json: + response.status_code = 400 + + response.raise_for_status() + + return response_json['result'] + +def request_monero_rpc(method: str, params: dict = {}) -> dict: + headers = {'content-type': 'application/json'} + + data = { + 'jsonrpc': '2.0', + 'id': '0', + 'method': method, + 'params': params + } + + response = requests.post( + env.MONERO_RPC_URL, + headers=headers, + json=data, + auth=HTTPDigestAuth(env.MONERO_RPC_USERNAME, env.MONERO_RPC_PASSWORD) + ) + + response_json = response.json() + + if 'error' in response_json: + response.status_code = 400 + + response.raise_for_status() + + return response_json['result'] + +def get_kraken_signature(url: str, payload: dict): + postdata = urllib.parse.urlencode(payload) + encoded = (str(payload['nonce']) + postdata).encode() + message = url.encode() + hashlib.sha256(encoded).digest() + mac = hmac.new(base64.b64decode(env.KRAKEN_API_SECRET), message, hashlib.sha512) + sigdigest = base64.b64encode(mac.digest()) + return sigdigest.decode() + +def kraken_request(path: str, payload = {}) -> dict: + payload['nonce'] = str(int(1000*time.time())) + headers = {} + headers['API-Key'] = env.KRAKEN_API_KEY + headers['API-Sign'] = get_kraken_signature(path, payload) + + return requests.post( + 'https://api.kraken.com' + path, + headers=headers, + data=payload + ).json()['result'] \ No newline at end of file