mirror of
https://github.com/MAGICGrants/autoforward-autoconvert.git
synced 2026-01-08 05:04:01 -05:00
feat: use electrum rpc for bitcoin autoforward, refactoring and add deploy files
This commit is contained in:
16
.env.example
16
.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=""
|
||||
KRAKEN_API_SECRET=""
|
||||
|
||||
42
.github/workflows/deploy.yml
vendored
Normal file
42
.github/workflows/deploy.yml
vendored
Normal file
@@ -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
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.venv
|
||||
.env
|
||||
.env
|
||||
.electrs-cookie
|
||||
111
autoforward.py
111
autoforward.py
@@ -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)
|
||||
85
docker-compose.yml
Normal file
85
docker-compose.yml
Normal file
@@ -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:
|
||||
41
electrum-client/Dockerfile
Normal file
41
electrum-client/Dockerfile
Normal file
@@ -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"]
|
||||
11
electrum-client/docker-entrypoint.sh
Executable file
11
electrum-client/docker-entrypoint.sh
Executable file
@@ -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 "$@"
|
||||
31
helpers.py
31
helpers.py
@@ -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']
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
requests
|
||||
bip_utils
|
||||
@@ -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)
|
||||
|
||||
149
src/autoforward.py
Normal file
149
src/autoforward.py
Normal file
@@ -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)
|
||||
|
||||
3
src/constants.py
Normal file
3
src/constants.py
Normal file
@@ -0,0 +1,3 @@
|
||||
MIN_BITCOIN_SEND_AMOUNT = 0.0001
|
||||
MAX_BITCOIN_FEE_PERCENT = 10
|
||||
MIN_MONERO_SEND_AMOUNT = 0.01
|
||||
15
src/env.py
Normal file
15
src/env.py
Normal file
@@ -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', '')
|
||||
36
src/seed-importer.py
Normal file
36
src/seed-importer.py
Normal file
@@ -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)
|
||||
86
src/util.py
Normal file
86
src/util.py
Normal file
@@ -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']
|
||||
Reference in New Issue
Block a user