La Bureautique?

docker
Boris 7 months ago
parent 634aca09ae
commit 16c1773669

@ -6,7 +6,7 @@ class Jaklis {
private $mode;
private $jaklisPath = __DIR__ . '/../vendors/jaklis/jaklis';
private $jaklisPath = __DIR__ . '/../vendors/jaklisse/jaklis';
private $nodes = [

1
vendors/jaklis vendored

@ -1 +0,0 @@
Subproject commit efed7354df7e5077f5721e8e92e429b60e0e6fb7

@ -0,0 +1,10 @@
# Chemin de la clé privé Ḡ1 de l'émetteur, au format PubSec
DUNIKEY=
# Noeud Duniter
NODE=https://g1.librelois.fr/gva
# Adresse du pod Cesium ou Gchange à utiliser
POD=https://g1.data.le-sou.org
#POD=https://g1.data.duniter.fr
#POD=https://data.gchange.fr

@ -0,0 +1,5 @@
.env
__pycache__
*.dunikey
not4U

@ -0,0 +1,3 @@
{
"python.pythonPath": "/usr/bin/python3.9"
}

@ -0,0 +1,82 @@
# Client CLI for Cesium+/Ḡchange pod
## Installation
Linux:
```
bash setup.sh
```
Autre:
```
Débrouillez-vous.
```
## Utilisation
*Python 3.6 minimum*
Renseignez optionnellement le fichier **.env** (Généré lors de la première tentative d'execution, ou à copier depuis .env.template).
```
./jaklis.py -h
```
```
usage: jaklis.py [-h] [-v] [-k KEY] [-n NODE] {read,send,delete,get,set,erase,stars,unstars,getoffer,setoffer,deleteoffer,pay,history,balance,id,idBalance} ...
Client CLI pour Cesium+ et Ḡchange
optional arguments:
-h, --help show this help message and exit
-v, --version Affiche la version actuelle du programme
-k KEY, --key KEY Chemin vers mon trousseau de clé (PubSec)
-n NODE, --node NODE Adresse du noeud Cesium+, Gchange ou Duniter à utiliser
Commandes de jaklis:
{read,send,delete,get,set,erase,stars,unstars,getoffer,setoffer,deleteoffer,pay,history,balance,id,idBalance}
read Lecture des messages
send Envoi d'un message
delete Supression d'un message
get Voir un profile Cesium+
set Configurer son profile Cesium+
erase Effacer son profile Cesium+
stars Voir les étoiles d'un profile / Noter un profile (option -s NOTE)
unstars Supprimer un star
getoffer Obtenir les informations d'une annonce gchange
setoffer Créer une annonce gchange
deleteoffer Supprimer une annonce gchange
pay Payer en Ḡ1
history Voir l'historique des transactions d'un compte Ḡ1
balance Voir le solde d'un compte Ḡ1
id Voir l'identité d'une clé publique/username
idBalance Voir l'identité d'une clé publique/username et son solde
```
Utilisez `./jaklis CMD -h``CMD` est la commande souhaité pour obtenir l'aide détaillé de cette commande.
### Exemples:
Lire les 10 derniers messages de mon compte indiqué dans le fichier `.env` (par defaut 3 messages):
```
./jaklis read -n10
```
Envoyer un message à la clé publique `Do99s6wQR2JLfhirPdpAERSjNbmjjECzGxHNJMiNKT3P` avec un fichier de trousseau particulier:
```
./jaklis.py -k /home/saucisse/mon_fichier_de_trousseau.dunikey send -d Do99s6wQR2JLfhirPdpAERSjNbmjjECzGxHNJMiNKT3P -t "Objet du message" -m "Corps de mon message"
```
Noter 4 étoiles le profile `S9EJbjbaGPnp26VuV6fKjR7raE1YkNhUGDgoydHvAJ1` sur gchange:
```
./jaklis.py -n https://data.gchange.fr like -p S9EJbjbaGPnp26VuV6fKjR7raE1YkNhUGDgoydHvAJ1 -s 4
```
Paramétrer mon profile Cesium+:
```
./jaklis.py set -n "Sylvain Durif" -v "Bugarach" -a "42 route de Vénus" -d "Christ cosmique" -pos 48.539927 2.6608169 -s https://www.creationmonetaire.info -A mon_avatar.png
```
Effacer mon profile Gchange:
```
./jaklis.py -n https://data.gchange.fr erase
```

@ -0,0 +1 @@
jaklis.py

@ -0,0 +1,273 @@
#!/usr/bin/env python3
import argparse, sys, os, getpass, string, random
from os.path import join, dirname
from shutil import copyfile
from dotenv import load_dotenv
from duniterpy.key import SigningKey
__version__ = "0.0.4"
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)
# Set global values (default parameters) , regarding variables environments
node = os.getenv('NODE')
if not node:
node="https://g1.librelois.fr/gva"
pod = os.getenv('POD')
if not pod:
pod="https://g1.data.le-sou.org"
destPubkey = False
# Parse arguments
parser = argparse.ArgumentParser(description="Client CLI pour Cesium+ et Ḡchange", epilog="current node: '" + node + "', current pod: '" + pod + "'.")
parser.add_argument('-v', '--version', action='store_true', help="Affiche la version actuelle du programme")
parser.add_argument('-k', '--key', help="Chemin vers mon trousseau de clé (PubSec)")
parser.add_argument('-n', '--node', help="Adresse du noeud Cesium+, Gchange ou Duniter à utiliser")
subparsers = parser.add_subparsers(title="Commandes de jaklis", dest="cmd")
read_cmd = subparsers.add_parser('read', help="Lecture des messages")
send_cmd = subparsers.add_parser('send', help="Envoi d'un message")
delete_cmd = subparsers.add_parser('delete', help="Supression d'un message")
getProfile_cmd = subparsers.add_parser('get', help="Voir un profile Cesium+")
setProfile_cmd = subparsers.add_parser('set', help="Configurer son profile Cesium+")
eraseProfile_cmd = subparsers.add_parser('erase', help="Effacer son profile Cesium+")
stars_cmd = subparsers.add_parser('stars', help="Voir les étoiles d'un profile / Noter un profile (option -s NOTE)")
unstars_cmd = subparsers.add_parser('unstars', help="Supprimer un star")
getoffer_cmd = subparsers.add_parser('getoffer', help="Obtenir les informations d'une annonce gchange")
setoffer_cmd = subparsers.add_parser('setoffer', help="Créer une annonce gchange")
deleteoffer_cmd = subparsers.add_parser('deleteoffer', help="Supprimer une annonce gchange")
pay_cmd = subparsers.add_parser('pay', help="Payer en Ḡ1")
history_cmd = subparsers.add_parser('history', help="Voir l'historique des transactions d'un compte Ḡ1")
balance_cmd = subparsers.add_parser('balance', help="Voir le solde d'un compte Ḡ1")
id_cmd = subparsers.add_parser('id', help="Voir l'identité d'une clé publique/username")
id_balance_cmd = subparsers.add_parser('idBalance', help="Voir l'identité d'une clé publique/username et son solde")
currentUd = subparsers.add_parser('currentUd', help="Affiche la montant actuel du dividende Universel")
listWallets = subparsers.add_parser('listWallets', help="Liste de toutes les portefeuilles G1")
# Messages management
read_cmd.add_argument('-n', '--number',type=int, default=3, help="Affiche les NUMBER derniers messages")
read_cmd.add_argument('-j', '--json', action='store_true', help="Sort au format JSON")
read_cmd.add_argument('-o', '--outbox', action='store_true', help="Lit les messages envoyés")
send_cmd.add_argument('-d', '--destinataire', required=True, help="Destinataire du message")
send_cmd.add_argument('-t', '--titre', help="Titre du message à envoyer")
send_cmd.add_argument('-m', '--message', help="Message à envoyer")
send_cmd.add_argument('-f', '--fichier', help="Envoyer le message contenu dans le fichier 'FICHIER'")
send_cmd.add_argument('-o', '--outbox', action='store_true', help="Envoi le message sur la boite d'envoi")
delete_cmd.add_argument('-i', '--id', action='append', nargs='+', required=True, help="ID(s) du/des message(s) à supprimer")
delete_cmd.add_argument('-o', '--outbox', action='store_true', help="Suppression d'un message envoyé")
# Profiles management
setProfile_cmd.add_argument('-n', '--name', help="Nom du profile")
setProfile_cmd.add_argument('-d', '--description', help="Description du profile")
setProfile_cmd.add_argument('-v', '--ville', help="Ville du profile")
setProfile_cmd.add_argument('-a', '--adresse', help="Adresse du profile")
setProfile_cmd.add_argument('-pos', '--position', nargs=2, help="Points géographiques (lat + lon)")
setProfile_cmd.add_argument('-s', '--site', help="Site web du profile")
setProfile_cmd.add_argument('-A', '--avatar', help="Chemin vers mon avatar en PNG")
getProfile_cmd.add_argument('-p', '--profile', help="Nom du profile")
getProfile_cmd.add_argument('-a', '--avatar', action='store_true', help="Récupérer également l'avatar au format raw base64")
# Likes management
stars_cmd.add_argument('-p', '--profile', help="Profile cible")
stars_cmd.add_argument('-n', '--number', type=int, help="Nombre d'étoile")
unstars_cmd.add_argument('-p', '--profile', help="Profile à dénoter")
# Offers management
getoffer_cmd.add_argument('-i', '--id', help="Annonce cible à récupérer")
setoffer_cmd.add_argument('-t', '--title', help="Titre de l'annonce à créer")
setoffer_cmd.add_argument('-d', '--description', help="Description de l'annonce à créer")
setoffer_cmd.add_argument('-c', '--category', help="Categorie de l'annonce à créer")
setoffer_cmd.add_argument('-l', '--localisation', nargs=2, help="Localisation de l'annonce à créer (lat + lon)")
setoffer_cmd.add_argument('-p', '--picture', help="Image de l'annonce à créer")
setoffer_cmd.add_argument('-ci', '--city', help="Ville de l'annonce à créer")
setoffer_cmd.add_argument('-pr', '--price', help="Prix de l'annonce à créer")
deleteoffer_cmd.add_argument('-i', '--id', help="Annonce cible à supprimer")
# GVA usage
pay_cmd.add_argument('-p', '--pubkey', help="Destinataire du paiement")
pay_cmd.add_argument('-a', '--amount', type=float, help="Montant de la transaction")
pay_cmd.add_argument('-c', '--comment', default="", help="Commentaire de la transaction")
pay_cmd.add_argument('-m', '--mempool', action='store_true', help="Utilise les sources en Mempool")
pay_cmd.add_argument('-v', '--verbose', action='store_true', help="Affiche le résultat JSON de la transaction")
history_cmd.add_argument('-p', '--pubkey', help="Clé publique du compte visé")
history_cmd.add_argument('-n', '--number',type=int, default=10, help="Affiche les NUMBER dernières transactions")
history_cmd.add_argument('-j', '--json', action='store_true', help="Affiche le résultat en format JSON")
history_cmd.add_argument('--nocolors', action='store_true', help="Affiche le résultat en noir et blanc")
balance_cmd.add_argument('-p', '--pubkey', help="Clé publique du compte visé")
balance_cmd.add_argument('-m', '--mempool', action='store_true', help="Utilise les sources en Mempool")
id_cmd.add_argument('-p', '--pubkey', help="Clé publique du compte visé")
id_cmd.add_argument('-u', '--username', help="Username du compte visé")
id_balance_cmd.add_argument('-p', '--pubkey', help="Pubkey du compte visé")
currentUd.add_argument('-p', '--pubkey', help="Pubkey du compte visé")
listWallets.add_argument('-b', '--balance', action='store_true', help="Affiche les soldes")
listWallets.add_argument('--mbr', action='store_true', help="Affiche la liste de pubkey membres brut")
listWallets.add_argument('--non_mbr', action='store_true', help="Affiche la liste de pubkey des identités non membres brut")
listWallets.add_argument('--larf', action='store_true', help="Affiche la liste des pubkey non membres brut")
listWallets.add_argument('--brut', action='store_true', help="Affiche la liste de toutes les pubkey brut")
listWallets.add_argument('-p', '--pubkey', help="useless but needed")
args = parser.parse_args()
cmd = args.cmd
if args.version:
print(__version__)
sys.exit(0)
if not cmd:
parser.print_help()
sys.exit(1)
def createTmpDunikey():
# Generate pseudo-random nonce
nonce=[]
for _ in range(32):
nonce.append(random.choice(string.ascii_letters + string.digits))
nonce = ''.join(nonce)
keyPath = "/tmp/secret.dunikey-" + nonce
# key = SigningKey.from_credentials(getpass.getpass("Identifiant: "), getpass.getpass("Mot de passe: "), None)
key = SigningKey.from_credentials("sgse547yhd54xv6541srdh", "sfdgwdrhpkxdawsbszqpof1sdg65xc", None)
key.save_pubsec_file(keyPath)
return keyPath
# Check if we need dunikey
try:
pubkey = args.pubkey
except:
pubkey = False
try:
profile = args.profile
except:
profile = False
# print(pubkey, profile)
if cmd in ('history','balance','get','id','idBalance','listWallets') and (pubkey or profile):
noNeedDunikey = True
keyPath = False
try:
dunikey = args.pubkey
except:
dunikey = args.profile
else:
noNeedDunikey = False
if args.key:
dunikey = args.key
keyPath = False
else:
dunikey = os.getenv('DUNIKEY')
if not dunikey:
keyPath = createTmpDunikey()
dunikey = keyPath
else:
keyPath = False
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)
# Construct CesiumPlus object
if cmd in ("read","send","delete","set","get","erase","stars","unstars","getoffer","setoffer","deleteoffer"):
from lib.cesium import CesiumPlus
if args.node:
pod = args.node
cesium = CesiumPlus(dunikey, pod, noNeedDunikey)
# Messaging
if cmd == "read":
cesium.read(args.number, args.outbox, args.json)
elif cmd == "send":
if args.fichier:
with open(args.fichier, 'r') as f:
msgT = f.read()
titre = msgT.splitlines(True)[0].replace('\n', '')
msg = ''.join(msgT.splitlines(True)[1:])
if args.titre:
titre = args.titre
msg = msgT
elif args.titre and args.message:
titre = args.titre
msg = args.message
else:
titre = input("Indiquez le titre du message: ")
msg = input("Indiquez le contenu du message: ")
cesium.send(titre, msg, args.destinataire, args.outbox)
elif cmd == "delete":
cesium.delete(args.id[0], args.outbox)
# Profiles
elif cmd == "set":
cesium.set(args.name, args.description, args.ville, args.adresse, args.position, args.site, args.avatar)
elif cmd == "get":
cesium.get(args.profile, args.avatar)
elif cmd == "erase":
cesium.erase()
# Stars
elif cmd == "stars":
if args.number or args.number == 0:
cesium.like(args.number, args.profile)
else:
cesium.readLikes(args.profile)
elif cmd == "unstars":
cesium.unLike(args.profile)
# Offers
elif cmd == "getoffer":
cesium.getOffer(args.id)
elif cmd == "setoffer":
cesium.setOffer(args.title, args.description, args.city, args.localisation, args.category, args.price, args.picture)
elif cmd == "deleteoffer":
cesium.deleteOffer(args.id)
# Construct GVA object
elif cmd in ("pay","history","balance","id","idBalance","currentUd","listWallets"):
from lib.gva import GvaApi
if args.node:
node = args.node
if args.pubkey:
destPubkey = args.pubkey
gva = GvaApi(dunikey, node, destPubkey, noNeedDunikey)
if cmd == "pay":
gva.pay(args.amount, args.comment, args.mempool, args.verbose)
elif cmd == "history":
gva.history(args.json, args.nocolors, args.number)
elif cmd == "balance":
gva.balance(args.mempool)
elif cmd == "id":
gva.id(args.pubkey, args.username)
elif cmd == "idBalance":
gva.idBalance(args.pubkey)
elif cmd == "currentUd":
gva.currentUd()
elif cmd == "listWallets":
gva.listWallets(args.brut, args.mbr, args.non_mbr, args.larf)
if keyPath:
os.remove(keyPath)

@ -0,0 +1,120 @@
import re, string, random, base64
from lib.cesiumCommon import CesiumCommon, PUBKEY_REGEX
from lib.messaging import ReadFromCesium, SendToCesium, DeleteFromCesium
from lib.profiles import Profiles
from lib.stars import ReadLikes, SendLikes, UnLikes
from lib.offers import Offers
class CesiumPlus(CesiumCommon):
#################### Messaging ####################
def read(self, nbrMsg, outbox, isJSON):
readCesium = ReadFromCesium(self.dunikey, self.pod)
jsonMsg = readCesium.sendDocument(nbrMsg, outbox)
if isJSON:
jsonFormat = readCesium.jsonMessages(jsonMsg, nbrMsg, outbox)
print(jsonFormat)
else:
readCesium.readMessages(jsonMsg, nbrMsg, outbox)
def send(self, title, msg, recipient, outbox):
sendCesium = SendToCesium(self.dunikey, self.pod)
sendCesium.recipient = recipient
# Generate pseudo-random nonce
nonce=[]
for _ in range(32):
nonce.append(random.choice(string.ascii_letters + string.digits))
sendCesium.nonce = base64.b64decode(''.join(nonce))
finalDoc = sendCesium.configDoc(sendCesium.encryptMsg(title), sendCesium.encryptMsg(msg)) # Configure JSON document to send
sendCesium.sendDocument(finalDoc, outbox) # Send final signed document
def delete(self, idsMsgList, outbox):
deleteCesium = DeleteFromCesium(self.dunikey, self.pod)
# deleteCesium.issuer = recipient
for idMsg in idsMsgList:
finalDoc = deleteCesium.configDoc(idMsg, outbox)
deleteCesium.sendDocument(finalDoc, idMsg)
#################### Profiles ####################
def set(self, name=None, description=None, ville=None, adresse=None, position=None, site=None, avatar=None):
setProfile = Profiles(self.dunikey, self.pod)
document = setProfile.configDocSet(name, description, ville, adresse, position, site, avatar)
result = setProfile.sendDocument(document,'set')
print(result)
return result
def get(self, profile=None, avatar=None):
getProfile = Profiles(self.dunikey, self.pod, self.noNeedDunikey)
if not profile:
profile = self.pubkey
if not re.match(PUBKEY_REGEX, profile) or len(profile) > 45:
scope = 'title'
else:
scope = '_id'
document = getProfile.configDocGet(profile, scope, avatar)
resultJSON = getProfile.sendDocument(document, 'get')
result = getProfile.parseJSON(resultJSON)
print(result)
def erase(self):
eraseProfile = Profiles(self.dunikey, self.pod)
document = eraseProfile.configDocErase()
result = eraseProfile.sendDocument(document,'erase')
print(result)
#################### Likes ####################
def readLikes(self, profile=False):
likes = ReadLikes(self.dunikey, self.pod, self.noNeedDunikey)
document = likes.configDoc(profile)
result = likes.sendDocument(document)
result = likes.parseResult(result)
print(result)
def like(self, stars, profile=False):
likes = SendLikes(self.dunikey, self.pod)
document = likes.configDoc(profile, stars)
if document:
likes.sendDocument(document, profile)
def unLike(self, pubkey, silent=False):
likes = UnLikes(self.dunikey, self.pod)
idLike = likes.checkLike(pubkey)
if idLike:
document = likes.configDoc(idLike)
likes.sendDocument(document, silent)
#################### Offer ####################
def setOffer(self, title=None, description=None, city=None, localisation=None, category=None, price=None, picture=None):
setOffer = Offers(self.dunikey, self.pod)
document = setOffer.configDocSet(title, description, city, localisation, category, price, picture)
result = setOffer.sendDocumentSet(document,'set')
# print(result)
return result
def getOffer(self, id, avatar=None):
getOffer = Offers(self.dunikey, self.pod, self.noNeedDunikey)
resultJSON = getOffer.sendDocumentGet(id, 'get')
# print(resultJSON)
result = getOffer.parseJSON(resultJSON)
print(result)
def deleteOffer(self, id):
eraseOffer = Offers(self.dunikey, self.pod)
document = eraseOffer.configDocErase(id)
result = eraseOffer.sendDocumentSet(document,'delete', id)
print(result)

@ -0,0 +1,51 @@
import sys, re, json
from hashlib import sha256
from lib.natools import fmt, sign, get_privkey
PUBKEY_REGEX = "(?![OIl])[1-9A-Za-z]{42,45}"
def pp_json(json_thing, sort=True, indents=4):
# Print beautifull JSON
if type(json_thing) is str:
print(json.dumps(json.loads(json_thing), sort_keys=sort, indent=indents))
else:
print(json.dumps(json_thing, sort_keys=sort, indent=indents))
return None
class CesiumCommon:
def __init__(self, dunikey, pod, noNeedDunikey=False):
self.pod = pod
self.noNeedDunikey = noNeedDunikey
# Get my pubkey from my private key
try:
self.dunikey = dunikey
if not dunikey:
raise ValueError("Dunikey is empty")
except:
sys.stderr.write("Please fill the path to your private key (PubSec)\n")
sys.exit(1)
if noNeedDunikey:
self.pubkey = self.dunikey
else:
self.pubkey = get_privkey(dunikey, "pubsec").pubkey
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)
def signDoc(self, document):
# Generate hash of document
hashDoc = sha256(document.encode()).hexdigest().upper()
# Generate signature of document
signature = fmt["64"](sign(hashDoc.encode(), get_privkey(self.dunikey, "pubsec"))[:-len(hashDoc.encode())]).decode()
# Build final document
data = {}
data['hash'] = hashDoc
data['signature'] = signature
signJSON = json.dumps(data)
finalJSON = {**json.loads(signJSON), **json.loads(document)}
return json.dumps(finalJSON)

@ -0,0 +1,31 @@
#!/usr/bin/env python3
import base64, base58, sys, string, random
from natools import get_privkey, box_decrypt, box_encrypt, fmt
def getargv(arg:str, default:str="", n:int=1, args:list=sys.argv) -> str:
if arg in args and len(args) > args.index(arg)+n:
return args[args.index(arg)+n]
else:
return default
cmd = sys.argv[1]
dunikey = getargv("-k", "private.dunikey")
msg = getargv("-m", "test")
pubkey = getargv("-p")
def decrypt(msg):
msg64 = base64.b64decode(msg)
return box_decrypt(msg64, get_privkey(dunikey, "pubsec"), pubkey).decode()
def encrypt(msg):
return fmt["64"](box_encrypt(msg.encode(), get_privkey(dunikey, "pubsec"), pubkey)).decode()
if cmd == 'decrypt':
clear = decrypt(msg)
print(clear)
elif cmd == 'encrypt':
clear = encrypt(msg)
print(clear)

@ -0,0 +1,40 @@
#!/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
class currentUd:
def __init__(self, node):
# Define Duniter GVA node
transport = AIOHTTPTransport(url=node)
self.client = Client(transport=transport, fetch_schema_from_transport=True)
def sendDoc(self):
# Build UD generation document
queryBuild = gql(
"""
query {
currentUd {
amount
}
}
"""
)
paramsBuild = {
}
# Send UD document
try:
udValue = 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 DU:\n" + message + "\n")
sys.exit(1)
udValueFinal = udValue['currentUd']['amount']
return udValueFinal

@ -0,0 +1,83 @@
from lib.currentUd import currentUd
from lib.gvaWallets import ListWallets
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
from lib.gvaID import Id
class GvaApi():
def __init__(self, dunikey, node, pubkey, noNeedDunikey=False):
self.noNeedDunikey = noNeedDunikey
self.dunikey = dunikey
self.node = node
if noNeedDunikey:
self.pubkey = self.dunikey
else:
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, number=10):
gva = History(self.dunikey, self.node, self.destPubkey)
gva.sendDoc(number)
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)
def id(self, pubkey, username):
gva = Id(self.dunikey, self.node, pubkey, username)
result = gva.sendDoc()
print(result)
def idBalance(self, pubkey):
gva = Id(self.dunikey, self.node, pubkey)
result = gva.sendDoc(True)
print(result)
def currentUd(self):
gva = currentUd(self.node)
result = gva.sendDoc()
print(result)
def listWallets(self, brut, brutMbr, brutNonMbr, brutLarf):
gva = ListWallets(self.node, brut, brutMbr, brutNonMbr, brutLarf)
result = gva.sendDoc()
print(result)

@ -0,0 +1,53 @@
#!/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: PkOrScriptGva!){
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)
if (balanceResult['balance'] == None): balanceValue = 'null'
else:
balanceValue = balanceResult['balance']['amount']/100
# print(balanceValue)
return balanceValue

@ -0,0 +1,272 @@
#!/usr/bin/env python3
import sys, re, os.path, json, ast, time, hashlib
from datetime import datetime
from duniterpy.key import base58
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, number):
# Build history generation document
queryBuild = gql(
"""
query ($pubkey: PubKeyGva!, $script: PkOrScriptGva!, $number: Int!){
txsHistoryBc(
script: $script
pagination: { pageSize: $number, ord: DESC }
) {
both {
pageInfo {
hasPreviousPage
hasNextPage
}
edges {
direction
node {
currency
issuers
outputs
comment
writtenTime
}
}
}
}
txsHistoryMp(pubkey: $pubkey) {
receiving {
currency
issuers
comment
outputs
receivedTime
}
receiving {
currency
issuers
comment
outputs
receivedTime
}
}
balance(script: $script) {
amount
base
}
node {
peer {
currency
}
}
currentUd {
amount
base
}
}
"""
)
paramsBuild = {
"pubkey": self.pubkey,
"number": number,
"script": f"SIG({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
# Parse transactions in blockchain
resBc = []
resBc = self.historyDoc['txsHistoryBc']['both']['edges']
for j, transaction in enumerate(resBc):
# print(transaction)
direction = resBc[j]['direction']
transaction = resBc[j]['node']
output = transaction['outputs'][0]
outPubkey = output.split("SIG(")[1].replace(')','')
# if direction == 'RECEIVED' or self.pubkey != outPubkey:
trans.append(i)
trans[i] = []
trans[i].append(direction)
trans[i].append(transaction['writtenTime'])
if direction == 'SENT':
trans[i].append(outPubkey)
amount = int('-' + output.split(':')[0])
else:
trans[i].append(transaction['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(transaction['comment'])
trans[i].append(base)
i += 1
# Parse transactions in mempool
for direction in self.historyDoc['txsHistoryMp']:
resBc = []
resBc = self.historyDoc['txsHistoryMp'][direction]
for j, transaction in enumerate(resBc):
# print(transaction)
transaction = resBc[j]
output = transaction['outputs'][0]
outPubkey = output.split("SIG(")[1].replace(')','')
# if direction == 'RECEIVING' or self.pubkey != outPubkey:
trans.append(i)
trans[i] = []
trans[i].append(direction)
trans[i].append(int(time.time()))
if direction == 'SENDING':
trans[i].append(outPubkey)
amount = int('-' + output.split(':')[0])
else:
trans[i].append(transaction['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(transaction['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
if (self.historyDoc['balance'] == None):
balance = balanceUD = 'null'
else:
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] == "SENT": color = "blue"
elif t[0] == "receiving": color = "yellow"
elif t[0] == "sending": color = "red"
else: color = None
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='')
checksum = self.gen_checksum(t[2])
shortPubkey = t[2][0:4] + '\u2026' + t[2][-4:] + ':' + checksum
if noColors:
print(" {: <18} | {: <12} | {: <7} | {: <7} | {: <30}".format(date, shortPubkey, t[3], t[4], comment))
else:
print(colored(" {: <18} | {: <12} | {: <7} | {: <7} | {: <30}".format(date, shortPubkey, 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 gen_checksum(self, pubkey):
"""
Returns the checksum of the input pubkey (encoded in b58)
thx Matograine
"""
pubkey_byte = base58.Base58Encoder.decode(str.encode(pubkey))
hash = hashlib.sha256(hashlib.sha256(pubkey_byte).digest()).digest()
return base58.Base58Encoder.encode(hash)[:3]
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,81 @@
#!/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 Id:
def __init__(self, dunikey, node, pubkey='', username=''):
self.dunikey = dunikey
self.pubkey = pubkey if pubkey else get_privkey(dunikey, "pubsec").pubkey
self.username = username
# 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, getBalance=False):
# Build balance generation document
if (getBalance):
queryBuild = gql(
"""
query ($pubkey: PubKeyGva!, $script: PkOrScriptGva!){
idty (pubkey: $pubkey) {
isMember
username
}
balance(script: $script) {
amount
}
}
"""
)
else:
queryBuild = gql(
"""
query ($pubkey: PubKeyGva!){
idty (pubkey: $pubkey) {
isMember
username
}
}
"""
)
paramsBuild = {
"pubkey": self.pubkey,
"script": f"SIG({self.pubkey})"
}
# Send balance document
try:
queryResult = self.client.execute(queryBuild, variable_values=paramsBuild)
except Exception as e:
sys.stderr.write("Echec de récupération du solde:\n" + str(e) + "\n")
sys.exit(1)
jsonBrut = queryResult
if (getBalance):
if (queryResult['balance'] == None):
jsonBrut['balance'] = {"amount": 0.0}
else:
jsonBrut['balance'] = queryResult['balance']['amount']/100
if (queryResult['idty'] == None):
username = 'Matiou'
isMember = False
else:
username = queryResult['idty']['username']
isMember = queryResult['idty']['isMember']
return json.dumps(jsonBrut, indent=2)

@ -0,0 +1,172 @@
#!/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])[0-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: PkOrScriptGva!, $issuer: PubKeyGva!, $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

@ -0,0 +1,77 @@
#!/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 ListWallets:
def __init__(self, node, brut, mbr, nonMbr, larf):
self.mbr = mbr
self.larf = larf
self.nonMbr = nonMbr
self.brut = brut
# Define Duniter GVA node
transport = AIOHTTPTransport(url=node)
self.client = Client(transport=transport, fetch_schema_from_transport=True)
def sendDoc(self):
# Build wallets generation document
queryBuild = gql(
"""
{
wallets(pagination: { cursor: null, ord: ASC, pageSize: 0 }) {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
script
balance {
amount
base
}
idty {
isMember
username
}
}
}
}
}
""")
# Send wallets document
try:
queryResult = self.client.execute(queryBuild)
except Exception as e:
sys.stderr.write("Echec de récupération de la liste:\n" + str(e) + "\n")
sys.exit(1)
jsonBrut = queryResult['wallets']['edges']
walletList = []
for i, trans in enumerate(jsonBrut):
dataWork = trans['node']
if (self.mbr and (dataWork['idty'] == None or dataWork['idty']['isMember'] == False)): continue
if (self.nonMbr and (dataWork['idty'] == None or dataWork['idty']['isMember'] == True)): continue
if (self.larf and (dataWork['idty'] != None)): continue
walletList.append({'pubkey': dataWork['script'],'balance': dataWork['balance']['amount']/100,'id': dataWork['idty']})
if (self.brut):
names = []
for dataWork in walletList:
if (self.mbr or self.nonMbr):
names.append(dataWork['pubkey'] + ' ' + dataWork['id']['username'])
else: