mirror of
https://github.com/MAGICGrants/autoforward-autoconvert.git
synced 2026-01-09 21:48:04 -05:00
WIP: Support Litecoin
This commit is contained in:
22
.env.example
22
.env.example
@@ -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"
|
||||
15
README.md
15
README.md
@@ -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
41
btc-electrum/Dockerfile
Normal 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"]
|
||||
17
btc-electrum/docker-entrypoint.sh
Normal file
17
btc-electrum/docker-entrypoint.sh
Normal 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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
41
ltc-electrum/Dockerfile
Normal 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"]
|
||||
17
ltc-electrum/docker-entrypoint.sh
Normal file
17
ltc-electrum/docker-entrypoint.sh
Normal 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
13
src/env.py
13
src/env.py
@@ -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')
|
||||
@@ -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!')
|
||||
|
||||
35
src/util.py
35
src/util.py
@@ -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...')
|
||||
|
||||
|
||||
Reference in New Issue
Block a user