feat: use electrum rpc for bitcoin autoforward, refactoring and add deploy files

This commit is contained in:
Artur
2024-09-09 12:44:31 -03:00
parent 57cf7dce5e
commit e3125c1934
15 changed files with 493 additions and 155 deletions

View File

@@ -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
View 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
View File

@@ -1,2 +1,3 @@
.venv
.env
.env
.electrs-cookie

View File

@@ -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
View 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:

View 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"]

View 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 "$@"

View File

@@ -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
View File

@@ -0,0 +1,2 @@
requests
bip_utils

View File

@@ -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
View 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
View 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
View 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
View 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
View 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']