WIP: Support Litecoin

This commit is contained in:
Justin Ehrenhofer
2025-02-19 16:52:02 -06:00
parent c826b62ce9
commit 0dc00cd342
16 changed files with 309 additions and 124 deletions

View File

@@ -1,12 +1,24 @@
ELECTRUM_SERVER_ADDRESS=""
MONERO_DAEMON_ADDRESS=""
BTC_ELECTRUM_RPC_URL=""
LTC_ELECTRUM_RPC_URL=""
ELECTRUM_RPC_USERNAME=""
ELECTRUM_RPC_PASSWORD=""
MONERO_RPC_PASSWORD=""
BITCOIN_WALLET_SEED=""
MONERO_RPC_URL=""
MONERO_RPC_USERNAME=""
MONERO_RPC_PASSWORD=""
MONERO_WALLET_SEED=""
MONERO_WALLET_PASSWORD=""
MONERO_WALLET_HEIGHT=""
MONERO_DAEMON_ADDRESS=""
KRAKEN_API_KEY=""
KRAKEN_API_SECRET=""
MAX_BITCOIN_FEE_PERCENT="10"
MAX_SLIPPAGE_PERCENT="1"
MAX_NETWORK_FEE_PERCENT="5"
MAX_SLIPPAGE_PERCENT="0.5"
BITCOIN_FEE_SOURCE="https://mempool.space/api/v1/fees/recommended"
BITCOIN_FEE_RATE="halfHourFee"
LITECOIN_FEE_SOURCE="https://litecoinspace.org/api/v1/fees/recommended"
LITECOIN_FEE_RATE="halfHourFee"

View File

@@ -15,18 +15,23 @@ Create a `.env` file as a copy of `.env.example` and set the values for the empt
| Variable name | Required | Default | Description |
| - | - | - | - |
| `BITCOIN_WALLET_SEED` | Yes | - | Your BIP39 Bitcoin mnemonic seed. |
| `BITCOIN_WALLET_SEED` | Yes | - | Your BIP39 Bitcoin mnemonic seed. Used for all Bitcoin-like assets. |
| `MONERO_WALLET_SEED` | Yes | - | Your 25 word Monero mnemonic seed. |
| `MONERO_WALLET_HEIGHT` | Yes | - | The restore height of your Monero wallet. |
| `ELECTRUM_RPC_PASSWORD` | Yes | - | A new strong password for your Electrum RPC. |
| `ELECTRUM_RPC_PASSWORD` | Yes | - | A new strong password for your Electrum RPCs. |
| `MONERO_RPC_PASSWORD` | Yes | - | A new strong password for your Monero RPC. |
| `MONERO_WALLET_PASSWORD` | Yes | - | A new strong password for your Monero Wallet. |
| `KRAKEN_API_KEY` | Yes | - | Your API key from Kraken. |
| `KRAKEN_API_SECRET` | Yes | - | Your API secret from Kraken. |
| `MONERO_DAEMON_ADDRESS` | Yes | - | The address of a Monero daemon you own or trust. |
| `ELECTRUM_SERVER_ADDRESS` | **No** | - | The address of an Electrum server you own or trust. E.g.: `localhost:50001:t` (no SSL) or `my.electrum.server:50001:s` (SSL). By leaving this blank you're letting Electrum select a random server for you, which may be a privacy concern. |
| `MAX_BITCOIN_FEE_PERCENT` | **No** | `10` | The maximum accepted bitcoin miner's fee percent when auto-forwarding. |
| `MAX_SLIPPAGE_PERCENT` | **No** | `1` | The maximum accepted slippage percent when auto-converting. |
| `BTC_ELECTRUM_SERVER_ADDRESS` | **No** | - | The address of a Bitcoin Electrum server you own or trust. E.g.: `localhost:50001:t` (no SSL) or `my.electrum.server:50001:s` (SSL). By leaving this blank you're letting Electrum select a random server for you, which may be a privacy concern. |
| `LTC_ELECTRUM_SERVER_ADDRESS` | **No** | - | The address of a Litecoin Electrum server you own or trust. E.g.: `localhost:50001:t` (no SSL) or `my.electrum.server:50001:s` (SSL). By leaving this blank you're letting Electrum select a random server for you, which may be a privacy concern. |
| `MAX_NETWORK_FEE_PERCENT` | **No** | `5` | The maximum accepted miner fee percent when auto-forwarding. Not applied to XMR. |
| `MAX_SLIPPAGE_PERCENT` | **No** | `0.5` | The maximum accepted slippage percent when auto-converting. |
| `BITCOIN_FEE_SOURCE` | **No** | `https://mempool.space/api/v1/fees/recommended` | The fee API source to use for Bitcoin transactions. |
| `BITCOIN_FEE_RATE` | **No** | `halfHourFee` | The fee rate to use in the Bitcoin fee source API response. |
| `LITECOIN_FEE_SOURCE` | **No** | `https://litecoinspace.org/api/v1/fees/recommended` | The fee API source to use for Litecoin transactions. |
| `LITECOIN_FEE_RATE` | **No** | `halfHourFee` | The fee rate to use in the Litecoin fee source API response. |
## Running

41
btc-electrum/Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
FROM python:3.12-alpine
ARG VERSION
ARG CHECKSUM_SHA512
ENV BTC_ELECTRUM_VERSION=$VERSION
ENV BTC_ELECTRUM_USER=btc-electrum
ENV BTC_ELECTRUM_HOME=/home/$BTC_ELECTRUM_USER
ENV BTC_ELECTRUM_NETWORK=mainnet
RUN adduser -D $BTC_ELECTRUM_USER
RUN mkdir -p /data ${BTC_ELECTRUM_HOME} && \
ln -sf /data ${BTC_ELECTRUM_HOME}/.electrum && \
chown ${BTC_ELECTRUM_USER} ${BTC_ELECTRUM_HOME}/.electrum /data
# IMPORTANT: always verify gpg signature before changing a hash here!
ENV BTC_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/${BTC_ELECTRUM_VERSION}/Electrum-${BTC_ELECTRUM_VERSION}.tar.gz
RUN [ "${BTC_ELECTRUM_CHECKSUM_SHA512} Electrum-${BTC_ELECTRUM_VERSION}.tar.gz" = "$(sha512sum Electrum-${BTC_ELECTRUM_VERSION}.tar.gz)" ]
RUN echo -e "**************************\n SHA 512 Checksum OK\n**************************"
RUN pip3 install cryptography Electrum-${BTC_ELECTRUM_VERSION}.tar.gz
RUN rm -f Electrum-${BTC_ELECTRUM_VERSION}.tar.gz
RUN mkdir -p /data \
${BTC_ELECTRUM_HOME}/.electrum/wallets/ \
${BTC_ELECTRUM_HOME}/.electrum/testnet/wallets/ \
${BTC_ELECTRUM_HOME}/.electrum/regtest/wallets/ \
${BTC_ELECTRUM_HOME}/.electrum/simnet/wallets/ && \
ln -sf ${BTC_ELECTRUM_HOME}/.electrum/ /data && \
chown -R ${BTC_ELECTRUM_USER} ${BTC_ELECTRUM_HOME}/.electrum /data
USER $BTC_ELECTRUM_USER
WORKDIR $BTC_ELECTRUM_HOME
VOLUME /data
EXPOSE 7000
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
set -ex
trap 'pkill -TERM -P1; electrum stop; exit 0' SIGTERM
rm -f .electrum/daemon
electrum --offline setconfig rpcuser ${BTC_ELECTRUM_USER}
electrum --offline setconfig rpcpassword ${BTC_ELECTRUM_PASSWORD}
electrum --offline setconfig rpchost 0.0.0.0
electrum --offline setconfig rpcport 7000
if [ -n "${BTC_ELECTRUM_SERVER_ADDRESS}" ]; then
electrum daemon -1 -s "${BTC_ELECTRUM_SERVER_ADDRESS}" "$@"
else
electrum daemon "$@"
fi

View File

@@ -1,16 +1,29 @@
services:
electrum-client:
btc-electrum:
build:
context: ./electrum-client
context: ./btc-electrum
args:
VERSION: "4.5.5"
CHECKSUM_SHA512: "3bdfce2187466fff20fd67736bdf257bf95d3517de47043be411ccda558a442b8fd81d6a8da094a39a1db39a7339dcd4282e73a7f00cf6bbd70473d7ce456b0b"
container_name: electrum-client
container_name: btc-electrum
restart: unless-stopped
environment:
- ELECTRUM_USER=${ELECTRUM_RPC_USERNAME}
- ELECTRUM_PASSWORD=${ELECTRUM_RPC_PASSWORD}
- ELECTRUM_SERVER_ADDRESS=${ELECTRUM_SERVER_ADDRESS}
- BTC_ELECTRUM_USER=${ELECTRUM_RPC_USERNAME}
- BTC_ELECTRUM_PASSWORD=${ELECTRUM_RPC_PASSWORD}
- BTC_ELECTRUM_SERVER_ADDRESS=${BTC_ELECTRUM_SERVER_ADDRESS}
ltc-electrum:
build:
context: ./ltc-electrum
args:
VERSION: "release-9"
CHECKSUM_SHA512: ""
container_name: ltc-electrum
restart: unless-stopped
environment:
- LTC_ELECTRUM_USER=${ELECTRUM_RPC_USERNAME}
- LTC_ELECTRUM_PASSWORD=${ELECTRUM_RPC_PASSWORD}
- LTC_ELECTRUM_SERVER_ADDRESS=${LTC_ELECTRUM_SERVER_ADDRESS}
monero-wallet-rpc:
image: ghcr.io/sethforprivacy/simple-monero-wallet-rpc:latest
@@ -32,7 +45,8 @@ services:
container_name: seed-importer
environment:
- PYTHONUNBUFFERED=1
- ELECTRUM_RPC_URL=http://electrum-client:7000
- BTC_ELECTRUM_SERVER_ADDRESS=http://btc-electrum:7000
- LTC_ELECTRUM_SERVER_ADDRESS=http://ltc-electrum:7001
- ELECTRUM_RPC_USERNAME=${ELECTRUM_RPC_USERNAME}
- ELECTRUM_RPC_PASSWORD=${ELECTRUM_RPC_PASSWORD}
- BITCOIN_WALLET_SEED=${BITCOIN_WALLET_SEED}
@@ -44,7 +58,8 @@ services:
- MONERO_WALLET_HEIGHT=${MONERO_WALLET_HEIGHT}
command: python ./src/seed-importer.py
depends_on:
- electrum-client
- btc-electrum
- ltc-electrum
- monero-wallet-rpc
autoforward:
@@ -54,7 +69,8 @@ services:
restart: unless-stopped
environment:
- PYTHONUNBUFFERED=1
- ELECTRUM_RPC_URL=http://electrum-client:7000
- BTC_ELECTRUM_SERVER_ADDRESS=http://btc-electrum:7000
- LTC_ELECTRUM_SERVER_ADDRESS=http://ltc-electrum:7001
- ELECTRUM_RPC_USERNAME=${ELECTRUM_RPC_USERNAME}
- ELECTRUM_RPC_PASSWORD=${ELECTRUM_RPC_PASSWORD}
- BITCOIN_WALLET_SEED=${BITCOIN_WALLET_SEED}

View File

@@ -1,41 +0,0 @@
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"]

View File

@@ -1,17 +0,0 @@
#!/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
if [ -n "${ELECTRUM_SERVER_ADDRESS}" ]; then
electrum daemon -1 -s "${ELECTRUM_SERVER_ADDRESS}" "$@"
else
electrum daemon "$@"
fi

41
ltc-electrum/Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
FROM python:3.12-alpine
ARG VERSION
ARG CHECKSUM_SHA512
ENV LTC_ELECTRUM_VERSION=$VERSION
ENV LTC_ELECTRUM_USER=ltc-electrum
ENV LTC_ELECTRUM_HOME=/home/$LTC_ELECTRUM_USER
ENV LTC_ELECTRUM_NETWORK=mainnet
RUN adduser -D $LTC_ELECTRUM_USER
RUN mkdir -p /data ${LTC_ELECTRUM_HOME} && \
ln -sf /data ${LTC_ELECTRUM_HOME}/.electrum && \
chown ${LTC_ELECTRUM_USER} ${LTC_ELECTRUM_HOME}/.electrum /data
# IMPORTANT: always verify gpg signature before changing a hash here!
ENV LTC_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/${LTC_ELECTRUM_VERSION}/Electrum-${LTC_ELECTRUM_VERSION}.tar.gz
RUN [ "${LTC_ELECTRUM_CHECKSUM_SHA512} Electrum-${LTC_ELECTRUM_VERSION}.tar.gz" = "$(sha512sum Electrum-${LTC_ELECTRUM_VERSION}.tar.gz)" ]
RUN echo -e "**************************\n SHA 512 Checksum OK\n**************************"
RUN pip3 install cryptography Electrum-${LTC_ELECTRUM_VERSION}.tar.gz
RUN rm -f Electrum-${LTC_ELECTRUM_VERSION}.tar.gz
RUN mkdir -p /data \
${LTC_ELECTRUM_HOME}/.electrum/wallets/ \
${LTC_ELECTRUM_HOME}/.electrum/testnet/wallets/ \
${LTC_ELECTRUM_HOME}/.electrum/regtest/wallets/ \
${LTC_ELECTRUM_HOME}/.electrum/simnet/wallets/ && \
ln -sf ${LTC_ELECTRUM_HOME}/.electrum/ /data && \
chown -R ${LTC_ELECTRUM_USER} ${LTC_ELECTRUM_HOME}/.electrum /data
USER $LTC_ELECTRUM_USER
WORKDIR $LTC_ELECTRUM_HOME
VOLUME /data
EXPOSE 7001
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
set -ex
trap 'pkill -TERM -P1; electrum stop; exit 0' SIGTERM
rm -f .electrum/daemon
electrum --offline setconfig rpcuser ${LTC_ELECTRUM_USER}
electrum --offline setconfig rpcpassword ${LTC_ELECTRUM_PASSWORD}
electrum --offline setconfig rpchost 0.0.0.0
electrum --offline setconfig rpcport 7001
if [ -n "${LTC_ELECTRUM_SERVER_ADDRESS}" ]; then
electrum daemon -1 -s "${LTC_ELECTRUM_SERVER_ADDRESS}" "$@"
else
electrum daemon "$@"
fi

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "magic-autoforward-autoconvert"
version = "0.1.0"
version = "0.2.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"

View File

@@ -6,12 +6,14 @@ import time
import util
import env
# https://support.kraken.com/hc/en-us/articles/205893708-Minimum-order-size-volume-for-trading
order_min = {
'XBT': 0.0001,
'LTC': 0.05,
'XMR': 0.03
}
def get_balance(asset: Literal['XBT', 'XMR']) -> str:
def get_balance(asset: Literal['XBT', 'LTC', 'XMR']) -> str:
balances = util.kraken_request('/0/private/Balance')
balance = '0'
@@ -20,10 +22,10 @@ def get_balance(asset: Literal['XBT', 'XMR']) -> str:
return balance
def get_bids(asset: Literal['XBT', 'XMR']):
def get_bids(asset: Literal['XBT', 'LTC', 'XMR']):
return util.kraken_request('/0/public/Depth', {'pair': f'{asset}USD'})[f'X{asset}ZUSD']['bids']
def attempt_sell(asset: Literal['XBT', 'XMR']):
def attempt_sell(asset: Literal['XBT', 'LTC', 'XMR']):
balance = float(get_balance(asset))
to_sell_amount = 0
@@ -65,9 +67,9 @@ def attempt_sell(asset: Literal['XBT', 'XMR']):
print(util.get_time(), f'Not selling {asset} due to high slippage.')
while 1:
for asset in ['XBT', 'XMR']:
for asset in ['XBT', 'LTC', 'XMR']:
try:
attempt_sell(cast(Literal['XBT', 'XMR'], asset))
attempt_sell(cast(Literal['XBT', 'LTC', 'XMR'], asset))
except Exception as e:
print(util.get_time(), f'Error attempting to sell {asset}:')
print(traceback.format_exc())

View File

@@ -8,27 +8,30 @@ from constants import 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 get_bitcoin_fee_rate(source: str, rate: str) -> int:
return requests.get(source).json()[rate]
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 set_bitcoin_fee_rate(coin: str, rate: int, dynamic: bool):
if dynamic: # Fall back to the Electrum rate if there is an issue
util.request_electrum_rpc(coin, 'setconfig', ['dynamic_fees', True])
else:
util.request_electrum_rpc(coin, 'setconfig', ['dynamic_fees', False])
util.request_electrum_rpc(coin, 'setconfig', ['fee_per_kb', rate * 1000])
def get_bitcoin_balance() -> float:
return float(util.request_electrum_rpc('getbalance')['confirmed'])
def get_bitcoin_balance(coin: str) -> float:
return float(util.request_electrum_rpc(coin, 'getbalance')['confirmed'])
def create_psbt(destination_address: str) -> str:
def create_psbt(coin: str, destination_address: str) -> str:
params = {
'destination': destination_address,
'amount': '!',
'unsigned': True # This way we can get the input amounts
}
return util.request_electrum_rpc('payto', params)
return util.request_electrum_rpc(coin, 'payto', params)
def get_psbt_data(psbt: str) -> dict:
return util.request_electrum_rpc('deserialize', [psbt])
def get_psbt_data(coin: str, psbt: str) -> dict:
return util.request_electrum_rpc(coin, 'deserialize', [psbt])
def get_total_psbt_fee(psbt_data: dict) -> float:
inputs_sum_sats = 0
@@ -41,14 +44,14 @@ def get_total_psbt_fee(psbt_data: dict) -> float:
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
total_fee = total_fee_sats / 100000000
return total_fee
def sign_psbt(psbt: str) -> str:
return cast(str, util.request_electrum_rpc('signtransaction', [psbt]))
def sign_psbt(coin: str, psbt: str) -> str:
return cast(str, util.request_electrum_rpc(coin, 'signtransaction', [psbt]))
def broadcast_bitcoin_tx(signed_tx: str):
util.request_electrum_rpc('broadcast', [signed_tx])
def broadcast_bitcoin_tx(coin: str, signed_tx: str):
util.request_electrum_rpc(coin, 'broadcast', [signed_tx])
def get_monero_balance() -> float:
params = {'account_index': 0}
@@ -86,18 +89,21 @@ def get_new_kraken_address(asset: Literal['XBT', 'XMR']) -> str:
raise Exception(f'Kraken did not return a new address: {json.dumps(result, indent=2)}')
def attempt_bitcoin_autoforward():
balance = get_bitcoin_balance()
balance = get_bitcoin_balance(env.BTC_ELECTRUM_RPC_URL)
if balance < MIN_BITCOIN_SEND_AMOUNT:
print(util.get_time(), f'Not enough Bitcoin balance to autoforward. (Balance: {balance}, Min Send: {MIN_BITCOIN_SEND_AMOUNT})')
return
fee_rate = get_bitcoin_fee_rate()
set_bitcoin_fee_rate(fee_rate)
try:
fee_rate = get_bitcoin_fee_rate(env.BITCOIN_FEE_SOURCE, env.BITCOIN_FEE_RATE)
set_bitcoin_fee_rate(env.BTC_ELECTRUM_RPC_URL, fee_rate, dynamic=False)
except:
set_bitcoin_fee_rate(env.BTC_ELECTRUM_RPC_URL, fee_rate=0, dynamic=True)
address = get_new_kraken_address('XBT')
try:
psbt = create_psbt(address)
psbt = create_psbt(env.BTC_ELECTRUM_RPC_URL, address)
except requests.exceptions.HTTPError as http_error:
response_json = cast(dict, http_error.response.json())
@@ -107,12 +113,50 @@ def attempt_bitcoin_autoforward():
raise http_error
psbt_data = get_psbt_data(psbt)
psbt_data = get_psbt_data(env.BTC_ELECTRUM_RPC_URL, psbt)
total_fee = get_total_psbt_fee(psbt_data)
amount = balance
if total_fee / amount * 100 > env.MAX_BITCOIN_FEE_PERCENT:
print(util.get_time(), f'Not autoforwarding due to high transaction fee.')
if total_fee / amount * 100 > env.MAX_NETWORK_FEE_PERCENT:
print(util.get_time(), f'Not autoforwarding due to high transaction fee {total_fee} BTC.')
return
signed_tx = sign_psbt(psbt)
broadcast_bitcoin_tx(signed_tx)
print(util.get_time(), f'Autoforwarded {amount} BTC to {address}!')
def attempt_litecoin_autoforward():
balance = get_bitcoin_balance(env.LTC_ELECTRUM_RPC_URL)
if balance < MIN_BITCOIN_SEND_AMOUNT:
print(util.get_time(), f'Not enough Litecoin balance to autoforward. (Balance: {balance}, Min Send: {MIN_BITCOIN_SEND_AMOUNT})')
return
try:
fee_rate = get_bitcoin_fee_rate(env.LITECOIN_FEE_SOURCE, env.LITECOIN_FEE_RATE)
set_bitcoin_fee_rate(env.LTC_ELECTRUM_RPC_URL, fee_rate, dynamic=False)
except:
set_bitcoin_fee_rate(env.LTC_ELECTRUM_RPC_URL, fee_rate=0, dynamic=True)
address = get_new_kraken_address('LTC')
try:
psbt = create_psbt(env.LTC_ELECTRUM_RPC_URL, 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
psbt_data = get_psbt_data(env.LTC_ELECTRUM_RPC_URL, psbt)
total_fee = get_total_psbt_fee(psbt_data)
amount = balance
if total_fee / amount * 100 > env.MAX_NETWORK_FEE_PERCENT:
print(util.get_time(), f'Not autoforwarding due to high transaction fee {total_fee} LTC.')
return
signed_tx = sign_psbt(psbt)
@@ -141,6 +185,12 @@ while 1:
print(util.get_time(), 'Error autoforwarding bitcoin:')
print(traceback.format_exc())
try:
attempt_litecoin_autoforward()
except Exception as e:
print(util.get_time(), 'Error autoforwarding litecoin:')
print(traceback.format_exc())
try:
attempt_monero_autoforward()
except Exception as e:

View File

@@ -1,2 +1,3 @@
MIN_BITCOIN_SEND_AMOUNT = 0.0001
MIN_MONERO_SEND_AMOUNT = 0.01
MIN_LITECOIN_SEND_AMOUNT = 0.05
MIN_MONERO_SEND_AMOUNT = 0.03

View File

@@ -1,6 +1,7 @@
import os
ELECTRUM_RPC_URL = os.getenv('ELECTRUM_RPC_URL', 'http://electrs:7000')
BTC_ELECTRUM_RPC_URL = os.getenv('ELECTRUM_RPC_URL', 'http://electrs:7000')
LTC_ELECTRUM_RPC_URL = os.getenv('ELECTRUM_RPC_URL', 'http://electrs:7001')
ELECTRUM_RPC_USERNAME = os.getenv('ELECTRUM_RPC_USERNAME', '')
ELECTRUM_RPC_PASSWORD = os.getenv('ELECTRUM_RPC_PASSWORD', '')
BITCOIN_WALLET_SEED = os.getenv('BITCOIN_WALLET_SEED', '')
@@ -11,9 +12,15 @@ 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', '')
MONERO_DAEMON_ADDRESS = os.getenv('MONERO_DAEMON_ADDRESS', '')
KRAKEN_API_KEY = os.getenv('KRAKEN_API_KEY', '')
KRAKEN_API_SECRET = os.getenv('KRAKEN_API_SECRET', '')
MAX_BITCOIN_FEE_PERCENT = float(os.getenv('MAX_BITCOIN_FEE_PERCENT', '10'))
MAX_SLIPPAGE_PERCENT = float(os.getenv('MAX_SLIPPAGE_PERCENT', '1'))
MAX_NETWORK_FEE_PERCENT = float(os.getenv('MAX_NETWORK_FEE_PERCENT', '5'))
MAX_SLIPPAGE_PERCENT = float(os.getenv('MAX_SLIPPAGE_PERCENT', '0.5'))
BITCOIN_FEE_SOURCE = os.getenv('BITCOIN_FEE_SOURCE', 'https://mempool.space/api/v1/fees/recommended')
BITCOIN_FEE_RATE = os.getenv('BITCOIN_FEE_RATE', 'halfHourFee')
LITECOIN_FEE_SOURCE = os.getenv('LITECOIN_FEE_SOURCE', 'https://litecoinspace.org/api/v1/fees/recommended')
LITECOIN_FEE_RATE = os.getenv('LITECOIN_FEE_RATE', 'halfHourFee')

View File

@@ -4,15 +4,19 @@ import traceback
import util
import env
def get_zprv_from_seed(mnemonic: str) -> str:
def get_zprv_from_seed(mnemonic: str, coin: str) -> str:
seed_bytes = Bip39SeedGenerator(mnemonic).Generate()
bip84_master_key = Bip84.FromSeed(seed_bytes, Bip84Coins.BITCOIN)
bip84_master_key = Bip84.FromSeed(seed_bytes, coin)
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])
zprv = get_zprv_from_seed(env.BITCOIN_WALLET_SEED, Bip84Coins.BITCOIN)
util.request_electrum_rpc(env.BTC_ELECTRUM_RPC_URL, 'restore', [zprv])
def import_litecoin_seed():
zprv = get_zprv_from_seed(env.BITCOIN_WALLET_SEED, Bip84Coins.LITECOIN)
util.request_electrum_rpc(env.LTC_ELECTRUM_RPC_URL, 'restore', [zprv])
def import_monero_seed():
params = {
@@ -30,13 +34,22 @@ util.wait_for_rpc()
try:
import_bitcoin_seed()
util.request_electrum_rpc('load_wallet')
util.request_electrum_rpc('changegaplimit', [1000, 'iknowhatimdoing'])
util.request_btc_electrum_rpc(env.BTC_ELECTRUM_RPC_URL, 'load_wallet')
util.request_btc_electrum_rpc(env.BTC_ELECTRUM_RPC_URL, 'changegaplimit', [1000, 'iknowhatimdoing'])
print('Bitcoin seed has successfully been imported!')
except Exception as e:
print(util.get_time(), 'Error importing bitcoin seed:')
print(traceback.format_exc())
try:
import_litecoin_seed()
util.request_electrum_rpc(env.LTC_ELECTRUM_RPC_URL, 'load_wallet')
util.request_electrum_rpc(env.LTC_ELECTRUM_RPC_URL, 'changegaplimit', [1000, 'iknowhatimdoing'])
print('Litecoin seed has successfully been imported!')
except Exception as e:
print(util.get_time(), 'Error importing litecoin seed:')
print(traceback.format_exc())
try:
import_monero_seed()
print('Monero seed has successfully been imported!')

View File

@@ -14,7 +14,7 @@ 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 = []):
def request_electrum_rpc(rpc_url: str, method: str, params: list | dict = []):
headers = {'content-type': 'application/json'}
data = {
@@ -25,7 +25,7 @@ def request_electrum_rpc(method: str, params: list | dict = []):
}
response = requests.post(
env.ELECTRUM_RPC_URL,
rpc_url,
headers=headers,
data=json.dumps(data),
auth=(env.ELECTRUM_RPC_USERNAME, env.ELECTRUM_RPC_PASSWORD)
@@ -67,18 +67,30 @@ def request_monero_rpc(method: str, params: dict = {}):
return response_json['result']
def open_bitcoin_wallet():
request_electrum_rpc('load_wallet')
request_electrum_rpc(env.BTC_ELECTRUM_RPC_URL, 'load_wallet')
def open_litecoin_wallet():
request_electrum_rpc(env.LTC_ELECTRUM_RPC_URL, 'load_wallet')
def open_monero_wallet() -> None:
params = {'filename': 'foo', 'password': env.MONERO_WALLET_PASSWORD}
request_monero_rpc('open_wallet', params)
def wait_for_rpc():
print('Waiting for Electrum RPC...')
print('Waiting for BTC Electrum RPC...')
while 1:
try:
request_electrum_rpc('getinfo')
request_electrum_rpc(env.BTC_ELECTRUM_RPC_URL, 'getinfo')
break
except:
time.sleep(10)
print('Waiting for LTC Electrum RPC...')
while 1:
try:
request_electrum_rpc(env.LTC_ELECTRUM_RPC_URL, 'getinfo')
break
except:
time.sleep(10)
@@ -87,13 +99,13 @@ def wait_for_rpc():
while 1:
try:
request_electrum_rpc('getinfo')
request_monero_rpc('getinfo')
break
except:
time.sleep(10)
def wait_for_wallets():
print('Waiting for Electrum wallet...')
print('Waiting for Bitcoin wallet...')
while 1:
try:
@@ -101,6 +113,15 @@ def wait_for_wallets():
break
except:
time.sleep(10)
print('Waiting for Litecoin wallet...')
while 1:
try:
open_litecoin_wallet()
break
except:
time.sleep(10)
print('Waiting for Monero wallet...')