173 lines
6.2 KiB
Python
173 lines
6.2 KiB
Python
|
#!/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)
|
||
|
|
||
|
def send(self):
|
||
|
result = self.genDoc()
|
||
|
result = self.checkTXDoc()
|
||
|
result = self.signDoc()
|
||
|
result = self.sendTXDoc()
|
||
|
return result
|
||
|
|