commit acd2108381d2c852993344b10fec15e26cf8b662 Author: status404 Date: Tue Apr 23 13:56:26 2024 +0300 early concept v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..46f8403 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +data/ diff --git a/accept_deal.sh b/accept_deal.sh new file mode 100755 index 0000000..3152723 --- /dev/null +++ b/accept_deal.sh @@ -0,0 +1,86 @@ +#!/bin/bash + +set -xue + +. ./config.sh + +PREV_ID=$(cat "$DEALS_PREV_MESSAGE_ID_FOR_ACCEPT" || echo "") + +NEW_PREV_ID_FILE="${DEALS_PREV_MESSAGE_ID_FOR_ACCEPT}.new" +TEXT_FILE="${DEALS_BASE}/accept_message.txt" +SENDER_FILE="${DEALS_BASE}/sender.txt" + +python get_bm_message.py \ + --to-address "$DEALS_BM_TO_ACCEPT_DEALS" \ + --from-address "$SENDER_FILE" \ + --prev-id "$PREV_ID" \ + --output-id "$NEW_PREV_ID_FILE" \ + --output-text "$TEXT_FILE" + +if [ ! -f "$NEW_PREV_ID_FILE" ]; then + echo "No new messages" + exit 0 +fi + +# BM message id is used as deal ID. +DEAL_ID=$(cat "$NEW_PREV_ID_FILE") + +mv "$NEW_PREV_ID_FILE" "$DEALS_PREV_MESSAGE_ID_FOR_ACCEPT" + +mkdir -p "$DEALS_INPUTS_DB_ROOT" +INPUT_FILE="${DEALS_INPUTS_DB_ROOT}/${DEAL_ID}" + +mv "$TEXT_FILE" "$INPUT_FILE" + +PAYER_BTC=$(grep "payer_btc: " "$INPUT_FILE" | sed "s/^payer_btc: //" | sed 's/"//g' | sed "s/'//g") +PAYEE_BTC=$(grep "payee_btc: " "$INPUT_FILE" | sed "s/^payee_btc: //" | sed 's/"//g' | sed "s/'//g") +AMOUNT=$(grep "amount: " "$INPUT_FILE" | sed "s/^amount: //" | sed 's/"//g' | sed "s/'//g" | sed "s/ BTC$//" ) +MEMO="deposit for deal ${DEAL_ID}" + +mkdir -p "$DEALS_PAYER_BTC" +mkdir -p "$DEALS_PAYEE_BTC" +echo "$PAYER_BTC" > "${DEALS_PAYER_BTC}/${DEAL_ID}" +echo "$PAYEE_BTC" > "${DEALS_PAYEE_BTC}/${DEAL_ID}" + +NOW=$(date +"%s") +NOW_STR=$(date -u "-d@${NOW}") +EXPIRATION=$(($NOW + $DEALS_DEPOSIT_TIMEOUT)) +EXPIRATION_STR=$(date -u "-d@${EXPIRATION}") + +mkdir -p "$DEALS_REQUEST_DB_ROOT" +REQUEST_FILE="${DEALS_REQUEST_DB_ROOT}/${DEAL_ID}" + +if [ -f "$REQUEST_FILE" ] ; then + echo "File ${REQUEST_FILE} already exists" + exit 1 +fi + +$ELECTRUM addrequest "$AMOUNT" -m "$MEMO" --expiration "$DEALS_DEPOSIT_TIMEOUT" > "$REQUEST_FILE" + +DEPOSIT_ADDRESS=$(cat "$REQUEST_FILE" | grep '"address": ' | sed 's/"address": //' | sed 's/[ ",]//g') + +APPROVED_DEAL="${DEALS_BASE}/approved_deal.txt" +cp "$INPUT_FILE" "$APPROVED_DEAL" +echo '' >> "$APPROVED_DEAL" +echo "deal: ${DEAL_ID}" >> "$APPROVED_DEAL" +echo "to_bitcoin: ${DEPOSIT_ADDRESS}" >> "$APPROVED_DEAL" +echo "date: '${NOW_STR}'" >> "$APPROVED_DEAL" +echo "pay_until: '${EXPIRATION_STR}'" >> "$APPROVED_DEAL" + +mkdir -p "$DEALS_SIGNED_APPROVED_DEALS_DB_ROOT" +SIGNED_FILE="${DEALS_SIGNED_APPROVED_DEALS_DB_ROOT}/${DEAL_ID}" + +if [ -f "$SIGNED_FILE" ] ; then + echo "File ${SIGNED_FILE} already exists" + exit 1 +fi + +mkdir -p "$DEALS_DEAL_SENDERS" +cp "$SENDER_FILE" "${DEALS_DEAL_SENDERS}/${DEAL_ID}" + +gpg -o "$SIGNED_FILE" --sign-with="$DEALS_PGP_TO_ACK_DEAL" --clear-sign "$APPROVED_DEAL" +python send_bm_message.py \ + --from-address "$DEALS_BM_TO_ACCEPT_DEALS" \ + --to-address "$(cat "$SENDER_FILE" )" \ + --subject "" \ + --message-file "$SIGNED_FILE" diff --git a/accept_deal_example.yaml b/accept_deal_example.yaml new file mode 100644 index 0000000..81896bf --- /dev/null +++ b/accept_deal_example.yaml @@ -0,0 +1,6 @@ +date: "2018-03-20 17:09:00" +payer: mgP3agvYaFktZN1mGpERVZU2CMLysD3EWN +payer_btc: mgP3agvYaFktZN1mGpERVZU2CMLysD3EWN +payee: jane +payee_btc: mzLaUbq8pXtZHzvQrpMdsE269Ny2whcyjx +amount: "0.01 BTC" diff --git a/config.sh b/config.sh new file mode 100644 index 0000000..980e22d --- /dev/null +++ b/config.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +DEALS_BASE="data" +mkdir -p "$DEALS_BASE" + +DEALS_BM_FOR_REGISTRATIONS="BM-2cTfyagTwT8d7UeqQRnAcMkczyUmn9WHmS" +DEALS_PREV_MESSAGE_ID_FOR_REGISTRATIONS="${DEALS_BASE}/registrations_last_message_id.txt" +DEALS_REG_DB_ROOT="${DEALS_BASE}/users/" + +DEALS_BM_TO_ACCEPT_DEALS="BM-2cVtzJWsb2F7Me1Xhyf2UQXikacJW4uvsX" +DEALS_PREV_MESSAGE_ID_FOR_ACCEPT="${DEALS_BASE}/accept_last_message_id.txt" +DEALS_INPUTS_DB_ROOT="${DEALS_BASE}/deal_inputs/" +DEALS_REQUEST_DB_ROOT="${DEALS_BASE}/deal_requests/" +DEALS_DEAL_SENDERS="${DEALS_BASE}/deal_senders/" +DEALS_SIGNED_APPROVED_DEALS_DB_ROOT="${DEALS_BASE}/signed_approved_deals/" +DEALS_BM_TO_NOTIFY_BALANCE_UPDATES="BM-2cVtzJWsb2F7Me1Xhyf2UQXikacJW4uvsX" +DEALS_LAST_BALANCE="${DEALS_BASE}/request_last_balance/" +DEALS_FILLED="${DEALS_BASE}/request_filled/" + +DEALS_PAYER_BTC="${DEALS_BASE}/payer_btc/" +DEALS_PAYEE_BTC="${DEALS_BASE}/payee_btc/" + +DEALS_PGP_TO_ACK_DEAL="F451DDCB84456DAABDF72C6EB513FB92284223A4" +DEALS_BM_TO_SEND_DEAL_ACK="BM-2cVtzJWsb2F7Me1Xhyf2UQXikacJW4uvsX" + +DEALS_JUDGE_PGP="228EA88DCD8221723E6CC671A298E92820BE01C9" +DEALS_JUDGE_BM_DESTINATION="BM-2cWK3BTH4JBbUjGpb4zMsYwHEcxHtyhqxw" +DEALS_JUDGE_BM_SOURCE="BM-2cUDy5DDuunJgYgjdC1E5AvMY3dVYcSrQZ" +DEALS_PREV_MESSAGE_ID_FOR_JUDGE="${DEALS_BASE}/judge_last_message_id.txt" +DEALS_JUDGE_MESSAGES="${DEALS_BASE}/judge_messages/" +DEALS_FINISHED="${DEALS_BASE}/request_finished/" + +# Time to fill deposit address, in seconds. +DEALS_DEPOSIT_TIMEOUT=86400 + +ELECTRUM="electrum --testnet" diff --git a/get_bm_message.py b/get_bm_message.py new file mode 100644 index 0000000..7a61070 --- /dev/null +++ b/get_bm_message.py @@ -0,0 +1,38 @@ +import argparse +import base64 +import json +import xmlrpclib + +def get_next_message(api, to_address, prev_id): + messages = json.loads(api.getAllInboxMessages())['inboxMessages'] + messages = [m for m in messages if m['toAddress'] == to_address] + if len(messages) == 0: + return None + if not prev_id: + return messages[0] + for (prev, next) in zip(messages, messages[1:]): + if prev['msgid'] == prev_id: + return next + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--prev-id') + parser.add_argument('--api', default="http://username:password@127.0.0.1:8442/") + parser.add_argument('--to-address') + parser.add_argument('--from-address') + parser.add_argument('--output-id') + parser.add_argument('--output-text') + args = parser.parse_args() + api = xmlrpclib.ServerProxy(args.api) + next_message = get_next_message(api, args.to_address, args.prev_id) + if not next_message: + return + with open(args.output_text, 'w') as f: + f.write(base64.b64decode(next_message['message'])) + with open(args.output_id, 'w') as f: + f.write(next_message['msgid']) + with open(args.from_address, 'w') as f: + f.write(next_message['fromAddress']) + +if __name__ == '__main__': + main() diff --git a/judge.sh b/judge.sh new file mode 100755 index 0000000..0b4d025 --- /dev/null +++ b/judge.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +set -xue + +. ./config.sh + +PREV_ID=$(cat "$DEALS_PREV_MESSAGE_ID_FOR_JUDGE" || echo "") + +NEW_PREV_ID_FILE="${DEALS_PREV_MESSAGE_ID_FOR_JUDGE}.new" +TEXT_FILE="${DEALS_BASE}/judge_message.txt.asc" +SENDER_FILE="${DEALS_BASE}/judge_sender.txt" + +python get_bm_message.py \ + --to-address "$DEALS_JUDGE_BM_DESTINATION" \ + --from-address "$SENDER_FILE" \ + --prev-id "$PREV_ID" \ + --output-id "$NEW_PREV_ID_FILE" \ + --output-text "$TEXT_FILE" + +if [ ! -f "$NEW_PREV_ID_FILE" ]; then + echo "No new messages" + exit 0 +fi + +mv "$NEW_PREV_ID_FILE" "$DEALS_PREV_MESSAGE_ID_FOR_JUDGE" + +SENDER="$(cat "$SENDER_FILE" )" + +if [ "$SENDER" != "$DEALS_JUDGE_BM_SOURCE" ]; then + echo "Sender is not our judge" + exit 1 +fi + +DEAL=$(grep 'deal:' "$TEXT_FILE" | sed 's/deal: //') + +mkdir -p "$DEALS_JUDGE_MESSAGES" + +TEXT_FILE_CONTENT="${DEALS_JUDGE_MESSAGES}/${DEAL}" +gpg -o "$TEXT_FILE_CONTENT" "$TEXT_FILE" 2> "${DEALS_BASE}/gpg_stderr.txt" +if ! grep "$DEALS_JUDGE_PGP" "${DEALS_BASE}/gpg_stderr.txt" ; then + echo "GPG check failed" + exit 1 +fi + +mkdir -p "$DEALS_FINISHED" + +FINISHED_FILE="${DEALS_FINISHED}/${DEAL}" + +if [ -f "$FINISHED_FILE" ] ; then + echo "Deal ${DEAL} has been jusged already" + exit 0 +fi + +touch "$FINISHED_FILE" + +SEND_BTC=$(grep 'send_btc:' "$TEXT_FILE_CONTENT" | sed 's/send_btc: //') +BTC_ADDRESS_FILE="${DEALS_BASE}/${SEND_BTC}_btc/${DEAL}" + +if [ ! -f "$BTC_ADDRESS_FILE" ] ; then + echo "File ${BTC_ADDRESS_FILE} does not exist" + exit 1 +fi + +BTC_ADDRESS="$(cat "$BTC_ADDRESS_FILE")" + +INPUT_FILE="${DEALS_INPUTS_DB_ROOT}/${DEAL}" +AMOUNT=$(grep "amount: " "$INPUT_FILE" | sed "s/^amount: //" | sed 's/"//g' | sed "s/'//g" | sed "s/ BTC$//" ) + +$ELECTRUM payto "$BTC_ADDRESS" "$AMOUNT" | $ELECTRUM broadcast - diff --git a/judgement_example.txt.asc b/judgement_example.txt.asc new file mode 100644 index 0000000..d3f9bff --- /dev/null +++ b/judgement_example.txt.asc @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA256 + +deal: 9762456efae9abc483d8a25273e6d22ea1938c3a46f1e9893c14806dc4bf1d55 +send_btc: payee +-----BEGIN PGP SIGNATURE----- + +iQEzBAEBCAAdFiEEIo6ojc2CIXI+bMZxopjpKCC+AckFAlrekAgACgkQopjpKCC+ +Acl1Twf/dvkx81m13zfgPjaLzgdZ43erIiCtT2YTTcdszXTJc/fO+MH2sluNQwvs +oNeQhDnvRaZeG8MjepU1PXuOIdSEpdZGsh39tKSo25zBsQCIrlHcEtBHIDkP2q5I +4KIRaxdq0WERu7URRr1tUrvH13J15GaaabC9pbDa+aoXpXdro1vsU7C8IrndA1OS +OTdgJF/avlxLWqWPCHZdajpKVFEtwjCk+T+C89GiBbJh5O8823amCy8P9U6N5OJx +bkftghe6i15tCSJCHGNN53Ld0HyR2T1vSalyH2sk7ItWbvoclvJCm40hdPJfshN/ +rICKse8G+aCmQLiwtZNhjFPAkgziYg== +=oyDj +-----END PGP SIGNATURE----- diff --git a/read_registrations.sh b/read_registrations.sh new file mode 100755 index 0000000..c7e4a3c --- /dev/null +++ b/read_registrations.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -xue + +. ./config.sh + +PREV_ID=$(cat "$DEALS_PREV_MESSAGE_ID_FOR_REGISTRATIONS" || echo "") + +NEW_PREV_ID_FILE="${DEALS_PREV_MESSAGE_ID_FOR_REGISTRATIONS}.new" +TEXT_FILE="${DEALS_BASE}/message.txt" + +python get_bm_message.py \ + --to-address "$DEALS_BM_FOR_REGISTRATIONS" \ + --prev-id "$PREV_ID" \ + --output-id "$NEW_PREV_ID_FILE" \ + --output-text "$TEXT_FILE" \ + --from-address /dev/null + +if [ ! -f "$NEW_PREV_ID_FILE" ]; then + echo "No new messages" + exit 0 +fi + +NAME=$(grep "name: " "$TEXT_FILE" | sed "s/^name: //" || echo "") + +if [ "$NAME" == "" ]; then + echo "Failed to parse registration message" +else + if ! ( echo "$NAME" | egrep --quiet '^[a-zA-Z0-9_]{3,10}$' ) ; then + echo "Name ${NAME} is not acceptable" + else + mkdir -p "$DEALS_REG_DB_ROOT" + DB_FILE="${DEALS_REG_DB_ROOT}/${NAME}" + + if [ -f "$DB_FILE" ]; then + echo "User ${NAME} already exists" + else + echo "Registering user ${NAME}" + mv "$TEXT_FILE" "$DB_FILE" + fi + fi +fi + +mv "$NEW_PREV_ID_FILE" "$DEALS_PREV_MESSAGE_ID_FOR_REGISTRATIONS" diff --git a/registration_example.yaml b/registration_example.yaml new file mode 100644 index 0000000..7147aac --- /dev/null +++ b/registration_example.yaml @@ -0,0 +1,6 @@ +name: jane +registering_user: parent +bitcoin_address: 16sXwXeqJpAzh7T9mkTm352D6nXbJqQS4N +email: foo@bar.com +onion: blockchainbdgpzk.onion +note: arbitrary text here diff --git a/send_bm_message.py b/send_bm_message.py new file mode 100644 index 0000000..af3a743 --- /dev/null +++ b/send_bm_message.py @@ -0,0 +1,20 @@ +import argparse +import base64 +import json +import xmlrpclib + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--api', default="http://username:password@127.0.0.1:8442/") + parser.add_argument('--from-address') + parser.add_argument('--to-address') + parser.add_argument('--subject') + parser.add_argument('--message-file') + args = parser.parse_args() + api = xmlrpclib.ServerProxy(args.api) + with open(args.message_file) as f: + text = f.read() + api.sendMessage(args.to_address, args.from_address, base64.standard_b64encode(args.subject), base64.standard_b64encode(text), 2) + +if __name__ == '__main__': + main() diff --git a/setup.txt b/setup.txt new file mode 100644 index 0000000..dfe9205 --- /dev/null +++ b/setup.txt @@ -0,0 +1,33 @@ +Consigure the system by editing config.sh +You can change BM addresses, deal filling timeout (DEALS_DEPOSIT_TIMEOUT), +electrum command to use (ELECTRUM, by default it uses testnet), PGP keys +to use (DEALS_PGP_TO_ACK_DEAL to sign deals, DEALS_JUDGE_PGP to check +arbitration commands). You can change location of all files created bu the +system: DEALS_BASE. + +Add the following to ~/.config/PyBitmessage/keys.dat : + +apienabled = true +apiport = 8442 +apiinterface = 127.0.0.1 +apiusername = username +apipassword = password + +Run BitMessage. + +Setup electrum: + +. config.sh +$ELECTRUM setconfig rpcport 7777 +$ELECTRUM setconfig rpcuser "username" +$ELECTRUM setconfig rpcpassword "password" +$ELECTRUM create + +To start and stop Electrum use start_electrum.sh and stop_electrum.sh + +Then you run the following scripts in loop: + +read_registrations.sh (it reads new registration messages from BM) +accept_deal.sh (reads new deal creation messages from BM) +update_deals.sh (checks state of deal filling with money) +judge.sh (receives arbitration messages) diff --git a/start_electrum.sh b/start_electrum.sh new file mode 100755 index 0000000..d08a86e --- /dev/null +++ b/start_electrum.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -xue + +. ./config.sh + +$ELECTRUM daemon start +$ELECTRUM daemon load_wallet diff --git a/stop_electrum.sh b/stop_electrum.sh new file mode 100755 index 0000000..2095519 --- /dev/null +++ b/stop_electrum.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -xue + +. ./config.sh + +$ELECTRUM daemon stop diff --git a/update_deals.sh b/update_deals.sh new file mode 100755 index 0000000..9c2e837 --- /dev/null +++ b/update_deals.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -xue + +. ./config.sh + +mkdir -p "$DEALS_LAST_BALANCE" + +MESSAGE_TXT="${DEALS_BASE}/message.txt" +PAID_REQUESTS="${DEALS_BASE}/paid_requests.json" +PENDING_REQUESTS="${DEALS_BASE}/pending_requests.json" + +$ELECTRUM listrequests --pending > "$PENDING_REQUESTS" + +for deal in $(grep 'deposit for deal' "$PENDING_REQUESTS" | egrep -o '[0-9a-f]{64}'); do + DEPOSIT_ADDRESS=$(grep "$deal" -B 5 "$PENDING_REQUESTS" | grep '"address":' | sed 's/"address": //' | sed 's/[ ",]//g') + BALANCE=$($ELECTRUM getaddressbalance "$DEPOSIT_ADDRESS" | grep '"confirmed"' | sed 's/"confirmed": "//' | sed 's/[" ,]//g') + BALANCE_FILE="${DEALS_LAST_BALANCE}/${deal}" + PREV_BALANCE=$(cat "$BALANCE_FILE" || echo '') + if [ "$BALANCE" = "$PREV_BALANCE" ]; then + echo "No changes for balance of deal ${deal}, address ${DEPOSIT_ADDRESS}" + continue + fi + if [ "$BALANCE" = "0" ]; then + echo "Balance is 0 for deal ${deal}, address ${DEPOSIT_ADDRESS}" + continue + fi + echo "$BALANCE" > "$BALANCE_FILE" + SENDER_FILE="${DEALS_DEAL_SENDERS}/${deal}" + echo "Balance of deal ${deal} updated: ${BALANCE}" > "$MESSAGE_TXT" + python send_bm_message.py \ + --from-address "$DEALS_BM_TO_NOTIFY_BALANCE_UPDATES" \ + --to-address "$(cat "$SENDER_FILE" )" \ + --subject "" \ + --message-file "$MESSAGE_TXT" +done + +mkdir -p "$DEALS_FILLED" + +$ELECTRUM listrequests --paid > "$PAID_REQUESTS" + +for deal in $(grep 'deposit for deal' "$PAID_REQUESTS" | egrep -o '[0-9a-f]{64}'); do + FILLED_FILE="${DEALS_FILLED}/${deal}" + if [ -f "$FILLED_FILE" ] ; then + continue + fi + DEPOSIT_ADDRESS=$(grep "$deal" -B 6 "$PAID_REQUESTS" | grep '"address":' | sed 's/"address": //' | sed 's/[ ",]//g') + BALANCE=$($ELECTRUM getaddressbalance "$DEPOSIT_ADDRESS" | grep '"unconfirmed"' | sed 's/"unconfirmed": "//' | sed 's/[" ,]//g') + if [ "$BALANCE" != "0" ]; then + echo "There is unconfirmed money on deal ${deal}, address ${DEPOSIT_ADDRESS}" + continue + fi + touch "$FILLED_FILE" + SENDER_FILE="${DEALS_DEAL_SENDERS}/${deal}" + echo "Deal ${deal} is filled" > "$MESSAGE_TXT" + python send_bm_message.py \ + --from-address "$DEALS_BM_TO_NOTIFY_BALANCE_UPDATES" \ + --to-address "$(cat "$SENDER_FILE" )" \ + --subject "" \ + --message-file "$MESSAGE_TXT" +done