6 changed files with 666 additions and 52 deletions
@ -0,0 +1,54 @@
|
||||
import sys, re |
||||
from lib.natools import get_privkey |
||||
from lib.gvaPay import Transaction, PUBKEY_REGEX |
||||
from lib.gvaHistory import History |
||||
from lib.gvaBalance import Balance |
||||
|
||||
class GvaApi(): |
||||
def __init__(self, dunikey, node, pubkey): |
||||
self.dunikey = dunikey |
||||
self.node = node |
||||
self.pubkey = get_privkey(dunikey, "pubsec").pubkey |
||||
if pubkey: |
||||
self.destPubkey = pubkey |
||||
else: |
||||
self.destPubkey = self.pubkey |
||||
|
||||
try: |
||||
if not re.match(PUBKEY_REGEX, self.pubkey) or len(self.pubkey) > 45: |
||||
raise ValueError("La clé publique n'est pas au bon format.") |
||||
except: |
||||
sys.stderr.write("La clé publique n'est pas au bon format.\n") |
||||
raise |
||||
|
||||
try: |
||||
if not re.match(PUBKEY_REGEX, self.destPubkey) or len(self.destPubkey) > 45: |
||||
raise ValueError("La clé publique n'est pas au bon format.") |
||||
except: |
||||
sys.stderr.write("La clé publique n'est pas au bon format.\n") |
||||
raise |
||||
|
||||
#################### Payments #################### |
||||
|
||||
def pay(self, amount, comment, mempool, verbose): |
||||
gva = Transaction(self.dunikey, self.node, self.destPubkey, amount, comment, mempool, verbose) |
||||
gva.genDoc() |
||||
gva.checkTXDoc() |
||||
gva.signDoc() |
||||
return gva.sendTXDoc() |
||||
|
||||
def history(self, isJSON=False, noColors=False): |
||||
gva = History(self.dunikey, self.node, self.destPubkey) |
||||
gva.sendDoc() |
||||
transList = gva.parseHistory() |
||||
|
||||
if isJSON: |
||||
transJson = gva.jsonHistory(transList) |
||||
print(transJson) |
||||
else: |
||||
gva.printHistory(transList, noColors) |
||||
|
||||
def balance(self, useMempool): |
||||
gva = Balance(self.dunikey, self.node, self.destPubkey, useMempool) |
||||
balanceValue = gva.sendDoc() |
||||
print(balanceValue) |
@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
import sys, re, os.path, json, ast |
||||
from termcolor import colored |
||||
from lib.natools import fmt, sign, get_privkey |
||||
from gql import gql, Client |
||||
from gql.transport.aiohttp import AIOHTTPTransport |
||||
|
||||
PUBKEY_REGEX = "(?![OIl])[1-9A-Za-z]{42,45}" |
||||
|
||||
class Balance: |
||||
|
||||
def __init__(self, dunikey, node, pubkey, useMempool=False): |
||||
self.dunikey = dunikey |
||||
self.pubkey = pubkey if pubkey else get_privkey(dunikey, "pubsec").pubkey |
||||
self.useMempool = useMempool |
||||
if not re.match(PUBKEY_REGEX, self.pubkey) or len(self.pubkey) > 45: |
||||
sys.stderr.write("La clé publique n'est pas au bon format.\n") |
||||
sys.exit(1) |
||||
|
||||
# Define Duniter GVA node |
||||
transport = AIOHTTPTransport(url=node) |
||||
self.client = Client(transport=transport, fetch_schema_from_transport=True) |
||||
|
||||
def sendDoc(self): |
||||
# Build balance generation document |
||||
queryBuild = gql( |
||||
""" |
||||
query ($pubkey: String!){ |
||||
balance(script: $pubkey) { |
||||
amount |
||||
} |
||||
} |
||||
""" |
||||
) |
||||
paramsBuild = { |
||||
"pubkey": self.pubkey |
||||
} |
||||
|
||||
# Send balance document |
||||
try: |
||||
balanceResult = self.client.execute(queryBuild, variable_values=paramsBuild) |
||||
except Exception as e: |
||||
message = ast.literal_eval(str(e))["message"] |
||||
sys.stderr.write("Echec de récupération du solde:\n" + message + "\n") |
||||
sys.exit(1) |
||||
|
||||
balanceValue = balanceResult['balance']['amount']/100 |
||||
# print(balanceValue) |
||||
return balanceValue |
@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
import sys, re, os.path, json, ast, time |
||||
from datetime import datetime |
||||
from termcolor import colored |
||||
from lib.natools import fmt, sign, get_privkey |
||||
from gql import gql, Client |
||||
from gql.transport.aiohttp import AIOHTTPTransport |
||||
|
||||
PUBKEY_REGEX = "(?![OIl])[1-9A-Za-z]{42,45}" |
||||
|
||||
class History: |
||||
|
||||
def __init__(self, dunikey, node, pubkey): |
||||
self.dunikey = dunikey |
||||
self.pubkey = pubkey if pubkey else get_privkey(dunikey, "pubsec").pubkey |
||||
self.node = node |
||||
if not re.match(PUBKEY_REGEX, self.pubkey) or len(self.pubkey) > 45: |
||||
sys.stderr.write("La clé publique n'est pas au bon format.\n") |
||||
sys.exit(1) |
||||
|
||||
# Define Duniter GVA node |
||||
transport = AIOHTTPTransport(url=node) |
||||
self.client = Client(transport=transport, fetch_schema_from_transport=True) |
||||
|
||||
def sendDoc(self): |
||||
# Build history generation document |
||||
queryBuild = gql( |
||||
""" |
||||
query ($pubkey: String!){ |
||||
transactionsHistory(pubkey: $pubkey) { |
||||
received { |
||||
writtenTime |
||||
issuers |
||||
outputs |
||||
comment |
||||
} |
||||
sent { |
||||
writtenTime |
||||
issuers |
||||
outputs |
||||
comment |
||||
} |
||||
receiving { |
||||
issuers |
||||
outputs |
||||
comment |
||||
} |
||||
sending { |
||||
issuers |
||||
outputs |
||||
comment |
||||
} |
||||
} |
||||
balance(script: $pubkey) { |
||||
amount |
||||
base |
||||
} |
||||
node { |
||||
peer { |
||||
currency |
||||
} |
||||
} |
||||
currentUd { |
||||
amount |
||||
base |
||||
} |
||||
} |
||||
""" |
||||
) |
||||
paramsBuild = { |
||||
"pubkey": self.pubkey |
||||
} |
||||
|
||||
# Send history document |
||||
try: |
||||
self.historyDoc = self.client.execute(queryBuild, variable_values=paramsBuild) |
||||
except Exception as e: |
||||
message = ast.literal_eval(str(e))["message"] |
||||
sys.stderr.write("Echec de récupération de l'historique:\n" + message + "\n") |
||||
sys.exit(1) |
||||
|
||||
|
||||
def parseHistory(self): |
||||
trans = [] |
||||
i = 0 |
||||
|
||||
currentBase = int(self.historyDoc['currentUd']['base']) |
||||
self.UD = self.historyDoc['currentUd']['amount']/100 |
||||
|
||||
for sens in 'received','sent','receiving','sending': |
||||
res = self.historyDoc['transactionsHistory'][sens] |
||||
for bloc in res: |
||||
output = bloc['outputs'][0] |
||||
outPubkey = output.split("SIG(")[1].replace(')','') |
||||
if sens in ('received','receiving') or self.pubkey != outPubkey: |
||||
trans.append(i) |
||||
trans[i] = [] |
||||
trans[i].append(sens) |
||||
if sens in ('receiving','sending'): |
||||
trans[i].append(int(time.time())) |
||||
else: |
||||
trans[i].append(bloc['writtenTime']) |
||||
if sens in ('sent','sending'): |
||||
trans[i].append(outPubkey) |
||||
amount = int('-' + output.split(':')[0]) |
||||
else: |
||||
trans[i].append(bloc['issuers'][0]) |
||||
amount = int(output.split(':')[0]) |
||||
base = int(output.split(':')[1]) |
||||
applyBase = base-currentBase |
||||
amount = round(amount*pow(10,applyBase)/100, 2) |
||||
# if referential == 'DU': amount = round(amount/UD, 2) |
||||
trans[i].append(amount) |
||||
trans[i].append(round(amount/self.UD, 2)) |
||||
trans[i].append(bloc['comment']) |
||||
trans[i].append(base) |
||||
i += 1 |
||||
|
||||
# Order transactions by date |
||||
trans.sort(key=lambda x: x[1]) |
||||
|
||||
# Keep only base if there is base change |
||||
lastBase = 0 |
||||
for i in trans: |
||||
if i[6] == lastBase: i[6] = None |
||||
else: lastBase = i[6] |
||||
|
||||
return trans |
||||
|
||||
def printHistory(self, trans, noColors): |
||||
# Get balance |
||||
balance = self.historyDoc['balance']['amount']/100 |
||||
balanceUD = round(balance/self.UD, 2) |
||||
|
||||
# Get currency |
||||
currency = self.historyDoc['node']['peer']['currency'] |
||||
if currency == 'g1': currency = 'Ḡ1' |
||||
elif currency == 'g1-test': currency = 'GT' |
||||
# if referential == 'DU': currency = 'DU/' + currency.lower() |
||||
|
||||
# Get terminal size |
||||
rows = int(os.popen('stty size', 'r').read().split()[1]) |
||||
|
||||
# Display history |
||||
print('+', end='') |
||||
print('-'.center(rows-1, '-')) |
||||
if noColors: isBold = isBoldEnd = '' |
||||
else: |
||||
isBold = '\033[1m' |
||||
isBoldEnd = '\033[0m' |
||||
print(isBold + "|{: <19} | {: <12} | {: <7} | {: <7} | {: <30}".format(" Date"," De / À"," {0}".format(currency)," DU/{0}".format(currency.lower()),"Commentaire") + isBoldEnd) |
||||
print('|', end='') |
||||
for t in trans: |
||||
if t[0] == "received": color = "green" |
||||
elif t[0] == "receiving": color = "yellow" |
||||
elif t[0] == "sending": color = "red" |
||||
else: color = "blue" |
||||
if noColors: |
||||
color = None |
||||
if t[0] in ('receiving','sending'): |
||||
comment = '(EN ATTENTE) ' + t[5] |
||||
else: |
||||
comment = t[5] |
||||
else: |
||||
comment = t[5] |
||||
|
||||
date = datetime.fromtimestamp(t[1]).strftime("%d/%m/%Y à %H:%M") |
||||
print('-'.center(rows-1, '-')) |
||||
if t[6]: |
||||
print('|', end='') |
||||
print(' Changement de base : {0} '.format(t[6]).center(rows-1, '#')) |
||||
print('|', end='') |
||||
print('-'.center(rows-1, '-')) |
||||
print('|', end='') |
||||
printKey = t[2][0:8] + '\u2026' + t[2][-3:] |
||||
if noColors: |
||||
print(" {: <18} | {: <12} | {: <7} | {: <7} | {: <30}".format(date, printKey, t[3], t[4], comment)) |
||||
else: |
||||
print(colored(" {: <18} | {: <12} | {: <7} | {: <7} | {: <30}".format(date, printKey, t[3], t[4], comment), color)) |
||||
print('|', end='') |
||||
print('-'.center(rows-1, '-')) |
||||
print('|', end='') |
||||
print(isBold + 'Solde du compte: {0} {1} ({2} DU/{3})'.format(balance, currency, balanceUD, currency.lower()).center(rows-1, ' ') + isBoldEnd) |
||||
print('+', end='') |
||||
print(''.center(rows-1, '-')) |
||||
if not noColors: |
||||
print(colored('Reçus', 'green'), '-', colored('En cours de réception', 'yellow'), '-', colored('Envoyé', 'blue'), '-', colored("En cours d'envoi", 'red')) |
||||
|
||||
return trans |
||||
|
||||
def jsonHistory(self, transList): |
||||
dailyJSON = [] |
||||
for i, trans in enumerate(transList): |
||||
dailyJSON.append(i) |
||||
dailyJSON[i] = {} |
||||
dailyJSON[i]['date'] = trans[1] |
||||
dailyJSON[i]['pubkey'] = trans[2] |
||||
dailyJSON[i]['amount'] = trans[3] |
||||
dailyJSON[i]['amountUD'] = trans[4] |
||||
dailyJSON[i]['comment'] = trans[5] |
||||
|
||||
dailyJSON = json.dumps(dailyJSON, indent=2) |
||||
# If we want to write JSON to a file |
||||
#jsonFile = open("history-{0}.json".format(self.pubkey[0:8]), "w") |
||||
#jsonFile.writelines(dailyJSON + '\n') |
||||
#jsonFile.close() |
||||
return dailyJSON |
||||
|
@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
import sys, re, os.path, json, ast |
||||
from termcolor import colored |
||||
from lib.natools import fmt, sign, get_privkey |
||||
from gql import gql, Client |
||||
from gql.transport.aiohttp import AIOHTTPTransport |
||||
|
||||
PUBKEY_REGEX = "(?![OIl])[1-9A-Za-z]{42,45}" |
||||
|
||||
class Transaction: |
||||
|
||||
def __init__(self, dunikey, node, recipient, amount, comment='', useMempool=False, verbose=False): |
||||
self.dunikey = dunikey |
||||
self.recipient = recipient |
||||
self.amount = int(amount*100) |
||||
self.comment = comment |
||||
self.issuer = get_privkey(dunikey, "pubsec").pubkey |
||||
self.useMempool = useMempool |
||||
self.verbose = verbose |
||||
self.node = node |
||||
self._isChange = False |
||||
|
||||
try: |
||||
if not re.match(PUBKEY_REGEX, recipient) or len(recipient) > 45: |
||||
raise ValueError("La clé publique n'est pas au bon format.") |
||||
except: |
||||
sys.stderr.write("La clé publique n'est pas au bon format.\n") |
||||
raise |
||||
|
||||
|
||||
try: |
||||
if recipient == self.issuer: |
||||
raise ValueError('Le destinataire ne peut pas être vous même.') |
||||
except: |
||||
sys.stderr.write("Le destinataire ne peut pas être vous même.\n") |
||||
raise |
||||
|
||||
|
||||
# Define Duniter GVA node |
||||
transport = AIOHTTPTransport(url=node) |
||||
self.client = Client(transport=transport, fetch_schema_from_transport=True) |
||||
|
||||
def genDoc(self): |
||||
# Build TX generation document |
||||
if self.verbose: print("useMempool:", str(self.useMempool)) |
||||
queryBuild = gql( |
||||
""" |
||||
query ($recipient: String!, $issuer: String!, $amount: Int!, $comment: String!, $useMempool: Boolean!){ genTx( |
||||
amount: $amount |
||||
comment: $comment |
||||
issuer: $issuer |
||||
recipient: $recipient |
||||
useMempoolSources: $useMempool |
||||
) |
||||
} |
||||
""" |
||||
) |
||||
paramsBuild = { |
||||
"recipient": self.recipient, |
||||
"issuer": self.issuer, |
||||
"amount": int(self.amount), |
||||
"comment": self.comment, |
||||
"useMempool": self.useMempool |
||||
} |
||||
|
||||
# Send TX document |
||||
try: |
||||
# self.txDoc = [] |
||||
self.txDoc = self.client.execute(queryBuild, variable_values=paramsBuild)['genTx'] |
||||
if self.verbose: print(self.txDoc[0]) |
||||
return self.txDoc |
||||
except Exception as e: |
||||
message = ast.literal_eval(str(e))["message"] |
||||
sys.stderr.write("Echec de la génération du document:\n" + message + "\n") |
||||
raise |
||||
|
||||
|
||||
# Check document |
||||
def checkTXDoc(self): |
||||
issuerRaw=[];outAmount=[];outPubkey=[];commentRaw=[] |
||||
for docs in self.txDoc: |
||||
docList = docs.splitlines() |
||||
for i, line in enumerate(docList): |
||||
if re.search("Issuers:", line): |
||||
issuerRaw.append(docList[(i + 1) % len(docList)]) |
||||
if re.search("Outputs:", line): |
||||
outputRaw = docList[(i + 1) % len(docList)].split(":") |
||||
outAmount.append(int(outputRaw[0])) |
||||
outPubkey.append(outputRaw[2].split("SIG(")[1].replace(')','')) |
||||
if re.search("Comment:", line): |
||||
commentRaw.append(line.split(': ', 1)[1]) |
||||
|
||||
# Check if it's only a change transaction |
||||
if all(i == self.issuer for i in outPubkey): |
||||
print("Le document contient une transaction de change") |
||||
self.isChange = True |
||||
# Check validity of the document |
||||
elif all(i != self.issuer for i in issuerRaw) or sum(outAmount) != self.amount or all(i != self.recipient for i in outPubkey) or all(i != self.comment for i in commentRaw): |
||||
sys.stderr.write(colored("Le document généré est corrompu !\nLe noeud " + self.node + "a peut être un dysfonctionnement.\n", 'red')) |
||||
sys.stderr.write(colored(issuerRaw[0] + " envoi " + str(outAmount[0]) + " vers " + outPubkey[0] + " with comment: " + commentRaw[0] + "\n", "yellow")) |
||||
raise ValueError('Le document généré est corrompu !') |
||||
else: |
||||
print("Le document généré est conforme.") |
||||
self.isChange = False |
||||
return self.txDoc |
||||
|
||||
def signDoc(self): |
||||
# Sign TX documents |
||||
signature=[] |
||||
self.signedDoc=[] |
||||
for i, docs in enumerate(self.txDoc): |
||||
signature.append(fmt["64"](sign(docs.encode(), get_privkey(self.dunikey, "pubsec"))[:-len(docs.encode())])) |
||||
self.signedDoc.append(docs + signature[i].decode()) |
||||
return self.signedDoc |
||||
|
||||
|
||||
def sendTXDoc(self): |
||||
# Build TX documents |
||||
txResult=[] |
||||
for docs in self.signedDoc: |
||||
querySign = gql( |
||||
""" |
||||
mutation ($signedDoc: String!){ tx( |
||||
rawTx: $signedDoc |
||||
) { |
||||
version |
||||
issuers |
||||
outputs |
||||
} |
||||
} |
||||
""" |
||||
) |
||||
paramsSign = { |
||||
"signedDoc": docs |
||||
} |
||||
|
||||
# Send TX Signed document |
||||
try: |
||||
txResult.append(str(self.client.execute(querySign, variable_values=paramsSign))) |
||||
except Exception as e: |
||||
message = ast.literal_eval(str(e))["message"] |
||||
sys.stderr.write("Echec de la transaction:\n" + message + "\n") |
||||
if self.verbose: |
||||
sys.stderr.write("Document final:\n" + docs) |
||||
raise ValueError(message) |
||||
else: |
||||
if self.isChange: |
||||
self.send() |
||||
else: |
||||
print(colored("Transaction effectué avec succès !", "green")) |
||||
if self.verbose: |
||||
print(docs) |
||||
break |
||||
|
||||
return txResult |
||||
|
||||
def _getIsChange(self): |
||||
return self._isChange |
||||
def _setIsChange(self, newChange): |
||||
if self.verbose: print("_setIsChange: ", str(newChange)) |
||||
self._isChange = newChange |
||||
if newChange: self.useMempool == True |
||||
isChange = property(_getIsChange, _setIsChange) |
@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3 |
||||
|
||||
""" |
||||
ZetCode Tkinter tutorial |
||||
|
||||
In this example, we use the pack |
||||
manager to create a review example. |
||||
|
||||
Author: Jan Bodnar |
||||
Website: www.zetcode.com |
||||
""" |
||||
|
||||
import PySimpleGUI as sg |
||||
from lib.gva import GvaApi |
||||
import sys, os, threading |
||||
from shutil import copyfile |
||||
from os.path import join, dirname |
||||
from dotenv import load_dotenv |
||||
from lib.natools import get_privkey |
||||
import requests |
||||
|
||||
class StdoutRedirector(object): |
||||
def __init__(self, text_widget): |
||||
self.text_widget = text_widget |
||||
|
||||
def write(self, s): |
||||
self.text_widget.insert('end', s) |
||||
self.text_widget.see('end') |
||||
|
||||
def flush(self): |
||||
pass |
||||
|
||||
|
||||
MY_PATH = os.path.realpath(os.path.dirname(sys.argv[0])) + '/' |
||||
|
||||
# Get variables environment |
||||
if not os.path.isfile(MY_PATH + '.env'): |
||||
copyfile(MY_PATH + ".env.template",MY_PATH + ".env") |
||||
dotenv_path = join(dirname(__file__),MY_PATH + '.env') |
||||
load_dotenv(dotenv_path) |
||||
|
||||
dunikey = os.getenv('DUNIKEY') |
||||
if not os.path.isfile(dunikey): |
||||
HOME = os.getenv("HOME") |
||||
dunikey = HOME + dunikey |
||||
if not os.path.isfile(dunikey): |
||||
sys.stderr.write('Le fichier de trousseau {0} est introuvable.\n'.format(dunikey)) |
||||
sys.exit(1) |
||||
node = os.getenv('NODE') |
||||
issuer = get_privkey(dunikey, "pubsec").pubkey |
||||
|
||||
|
||||
def ProceedPaiement(recipient, amount, comment): |
||||
if not recipient: |
||||
raise ValueError("Veuillez indiquer un destinataire de paiement") |
||||
elif not amount: |
||||
raise ValueError("Veuillez indiquer le montant de la transaction") |
||||
|
||||
amount = int(float(amount.replace(',','.'))*100) |
||||
print("Paiement en cours vers", recipient) |
||||
gva = GvaApi(dunikey, node, recipient) |
||||
gva.pay(amount, comment, False, False) |
||||
|
||||
recipient = amount = comment = None |
||||
|
||||
|
||||
sg.theme('DarkGrey2') |
||||
layout = [ [sg.Text('Noeud utilisé: ' + node)], |
||||
[sg.Text('Votre clé publique: ' + issuer)], |
||||
[sg.Text('')], |
||||
[sg.Text('Destinataire: '), sg.InputText(size=(55, None),default_text=issuer)], |
||||
[sg.Text('Montant: '), sg.InputText(size=(7, None)), sg.Text('Ḡ1')], |
||||
[sg.Text('Commentaire:'), sg.InputText(size=(55, None))], |
||||
[sg.Button('Envoyer')] ] |
||||
|
||||
# Create the Window |
||||
window = sg.Window('Paiement Ḡ1 - GVA', layout) |
||||
# availablePubkeys = requests.get('https://g1-stats.axiom-team.fr/data/wallets-g1.txt') |
||||
while True: |
||||
try: |
||||
event, values = window.read() |
||||
if event == sg.WIN_CLOSED: |
||||
break |
||||
if event == 'Envoyer': |
||||
ProceedPaiement(values[0], values[1], values[2]) |
||||
except Exception as e: |
||||
loc = window.CurrentLocation() |
||||
sg.popup(e, title="ERREUR", button_color=('black','red'), location=(loc)) |
||||
else: |
||||
loc = window.CurrentLocation() |
||||
sg.popup(f'Transaction effectué avec succès !', title="Envoyé", location=(loc)) |
||||
|
||||
|
||||
window.close() |
Loading…
Reference in new issue