Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • elmau/silkaj
  • Mr-Djez/silkaj
  • jbar/silkaj
  • clients/python/silkaj
  • Bernard/silkaj
  • cebash/silkaj
  • jytou/silkaj
  • c-geek/silkaj
  • vincentux/silkaj
  • jeanlucdonnadieu/silkaj
  • matograine/silkaj
  • zicmama/silkaj
  • manutopik/silkaj
  • atrax/silkaj
14 results
Select Git revision
Show changes
Showing
with 897 additions and 157 deletions
# inspiré de : https://forum.duniter.org/t/doppler-gitlab/3183/30
# GITHUB_URL_AND_KEY should look like https://duniter-gitlab:TOKEN@github.com/chemin/vers/ton/depot/github
mirror_to_github:
script:
- git remote add github $GITHUB_URL_AND_KEY
- git config --global user.email "contact@duniter.org"
- git config --global user.name "Duniter"
- git push --force --mirror github
npm:
script:
- echo TODO
only:
tags
# Licence Ǧ1 ([fr](license/license_g1-fr-FR.rst)) ([en](license/license_g1-en.rst))
Dernière version : (consulter la licence)
// TODO : import depuis npm, version npm publié automatiquement selon les tags
License Ğ1 - v0.2.5
===================
:date: 2017-08-21 16:59
:modified: 2018-01-24 19:20
**Money licensing and liability commitment.**
Any certification operation of a new member of Ğ1 must first be accompanied by the transmission of this license of the currency Ğ1 whose certifier must ensure that it has been studied, understood and accepted by the person who will be certified.
Money Ğ1
--------
Ğ1 occurs via a Universal Dividend (DU) for any human member, which is of the form:
* 1 DU per person per day
The amount of DU is identical each day until the next equinox, where the DU will then be reevaluated according to the formula:
* DU <sub>day</sub> (the following equinox) = DU <day>(equinox) + c² (M / N) (equinox) / (15778800 seconds)</day>
With as parameters:
* c = 4.88% / equinox
* UD (0) = 10.00 Ğ1
And as variables:
* _M_ the total monetary mass at the equinox
* _N_ the number of members at the equinox
Web of Trust Ğ1 (WoT Ğ1)
------------------------
**Warning:** Certifying is not just about making sure you've met the person, it's ensuring that the community Ğ1 knows the certified person well enough and Duplicate account made by a person certified by you, or other types of problems (disappearance ...), by cross-checking that will reveal the problem if necessary.
When you are a member of Ğ1 and you are about to certify a new account:
**You are assured:**
1°) The person who declares to manage this public key (new account) and to have personally checked with him that this is the public key is sufficiently well known (not only to know this person visually) that you are about to certify.
2a°) To meet her physically to make sure that it is this person you know who manages this public key.
2b°) Remotely verify the public person / key link by contacting the person via several different means of communication, such as social network + forum + mail + video conference + phone (acknowledge voice).
Because if you can hack an email account or a forum account, it will be much harder to imagine hacking four distinct means of communication, and mimic the appearance (video) as well as the voice of the person .
However, the 2 °) is preferable to 3 °, whereas the 1 °) is always indispensable in all cases.
3 °) To have verified with the person concerned that he has indeed generated his Duniter account revocation document, which will enable him, if necessary, to cancel his account (in case of account theft, ID, an incorrectly created account, etc.).
**Abbreviated WoT rules:**
Each member has a stock of 100 possible certifications, which can only be issued at the rate of 1 certification / 5 days.
Valid for 2 months, certification for a new member is definitively adopted only if the certified has at least 4 other certifications after these 2 months, otherwise the entry process will have to be relaunched.
To become a new member of WoT Ğ1 therefore 5 certifications must be obtained at a distance < 5 of 80% of the WoT sentinels.
A member of the TdC Ğ1 is sentinel when he has received and issued at least Y [N] certifications where N is the number of members of the TdC and Y [N] = ceiling N ^ (1/5). Examples:
* For 1024 < N ≤ 3125 we have Y [N] = 5
* For 7776 < N ≤ 16807 we have Y [N] = 7
* For 59049 < N ≤ 100 000 we have Y [N] = 10
Once the new member is part of the WoT Ğ1 his certifications remain valid for 2 years.
To remain a member, you must renew your agreement regularly with your private key (every 12 months) and make sure you have at least 5 certifications valid after 2 years.
Software Ğ1 and license Ğ1
--------------------------
The software Ğ1 allowing users to manage their use of Ğ1 must transmit this license with the software and all the technical parameters of the currency Ğ1 and TdC Ğ1 which are entered in block 0 of Ğ1.
For more details in the technical details it is possible to consult directly the code of Duniter which is a free software and also the data of the blockchain Ğ1 by retrieving it via a Duniter instance or node Ğ1.
More information on the Duniter Team website [https://www.duniter.org](https://www.duniter.org)
{
"name": "licence-g1",
"version": "1.0.0",
"description": "Licence de la Ǧ1",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.duniter.org:communication/licence-G1.git"
},
"keywords": [
"licence",
"crypto-currency",
"monnaie",
"libre",
"June",
"G1",
"Ǧ1"
],
"author": "Duniter community",
"license": "ISC"
}
Copyright 2016-2025 Maël Azimi <m.a@moul.re>
Silkaj is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Silkaj is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
site_name: Silkaj
site_url: https://silkaj.duniter.org
repo_url: https://git.duniter.org/clients/python/silkaj
copyright: "Copyright © 2016 2025 Maël Azimi"
repo_name: GitLab
remote_branch: "pages"
site_description: "Command line client for Ğ1 libre-currency powered by Duniter"
theme:
name: material
logo: images/silkaj_logo.png
favicon: images/silkaj_logo.png
features:
- navigation.tracking
- navigation.tabs
- navigation.tabs.sticky
- navigation.sections
- content.code.copy
- content.tabs.link
font: false
palette:
- media: "(prefers-color-scheme: dark)"
scheme: default
toggle:
icon: material/brightness-4
name: Switch to light mode
- media: "(prefers-color-scheme: light)"
scheme: slate
toggle:
icon: material/brightness-7
name: Switch to dark mode
markdown_extensions:
- attr_list
- mkdocs-click
- md_in_html
- pymdownx.emoji:
emoji_index: !!python/name:material.extensions.emoji.twemoji
emoji_generator: !!python/name:material.extensions.emoji.to_svg
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
- pymdownx.tabbed:
alternate_style: true
- toc:
permalink: true
nav:
- Home: index.md
- Installation: install.md
- Usage:
- usage/index.md
- usage/cli.md
- usage/account_storage.md
- usage/multi-recipients_transfers_and_automation.md
- DeathReaper: usage/deathreaper.md
- Blog:
- blog/index.md
- Changelog: changelog.md
- Contributing:
- contributing/index.md
- Poetry installation: contributing/install_poetry.md
- Container usage: contributing/container_usage.md
- Documentation: contributing/documentation.md
- Testing: contributing/testing.md
- Coverage report: coverage.md
- contributing/packaging.md
extra:
version:
provider: mike
social:
- icon: fontawesome/brands/gitlab
link: https://git.duniter.org/clients/python/silkaj
name: Git repository
- icon: fontawesome/brands/python
link: https://pypi.org/project/silkaj/
name: PyPI
- icon: simple/discourse
link: https://forum.duniter.org
name: Duniter forum
- icon: simple/xmpp
link: https://chat.duniter.org
name: Duniter XMPP chatroom
plugins:
- search
- blog:
pagination_per_page: 5
blog_toc: true
post_url_format: "{slug}"
archive_url_format: "{date}"
categories_url_format: "{slug}"
- coverage
- gitlab_links:
gitlab_url: "https://git.duniter.org/clients/python/silkaj"
[project]
name = "silkaj"
version = "0.20.0dev"
description = "Command line client for Ğ1 libre-currency powered by Duniter"
license = { text = "AGPL-3.0-or-later" }
readme = "README.md"
requires-python = ">=3.9,<4.0"
authors = [
{ name = "Moul, Maël Azimi", email = "mael.azimi@moul.re"},
{ name = "Matograine", email = "tom.ngr@zaclys.net"},
{ name = "Tortue", email = "ucoin@tednet.fr"},
{ name = "cebash, Sébastien DA ROCHA", email = "sebastien@da-rocha.net"},
]
maintainers = [{ name = "Moul, Maël Azimi", email = "mael.azimi@moul.re"}]
keywords = ["g1", "duniter", "cryptocurrency", "librecurrency", "RTM"]
dynamic = ["classifiers", "dependencies"]
[project.urls]
homepage = "https://silkaj.duniter.org"
repository = "https://git.duniter.org/clients/python/silkaj"
documentation = "https://silkaj.duniter.org/latest/"
"changelog" = "https://silkaj.duniter.org/latest/changelog/"
[project.scripts]
silkaj = "silkaj.cli:cli"
[tool.poetry]
requires-poetry = ">=2.0"
include = [{path="g1_monetary_license/g1_monetary_license_*.rst", format=["sdist", "wheel"]}]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Operating System :: Unix",
"Environment :: Console",
"Intended Audience :: End Users/Desktop",
"Natural Language :: English",
]
[project.optional-dependencies]
deathreaper = [ "pydiscourse (>=1.7.0,<2.0)" ]
[tool.poetry.dependencies]
duniterpy = "~1.2.1"
rich-click = "^1.8.8"
texttable = "^1.7.0"
pendulum = "^3.1.0"
[tool.poetry.group.test.dependencies]
pytest = "^8.3.5"
pytest-cov = "^6.0"
pytest-sugar = "^1.0.0"
pytest-clarity = "^1.0.1"
[tool.poetry.group.docs]
optional = true
[tool.poetry.group.docs.dependencies]
mkdocs-material = "^9.6.9"
mike = "^2.1.3"
mkdocs-coverage = "^1.1.0"
mkdocs-gitlab-plugin = "^0.1.4"
mkdocs-click = "0.8.1"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.mypy]
ignore_missing_imports = true
[tool.ruff]
target-version = "py39"
lint.select = ["F", "W", "I", "UP", "YTT", "B", "A", "DTZ", "T10",
"EXE", "ISC", "ICN", "G", "INP", "PIE", "PYI", "Q", "RSE", "SLF", "TID",
"PL", "C4", "PT", "RET", "SIM", "PTH", "PGH", "PL", "TRY", "RUF"]
lint.ignore = ["PLR2004", "PLR0913"]
......@@ -14,41 +14,21 @@ check_version_format() {
fi
}
check_branch() {
branch=`git rev-parse --abbrev-ref HEAD`
if [[ "$branch" != "master" ]]; then
error_message "Current branch should be 'master'"
fi
}
update_version() {
sed -i "s/SILKAJ_VERSION = \"silkaj.*\"/SILKAJ_VERSION = \"silkaj $VERSION\"/" src/constants.py
exec_installed poetry
sed -i "s/SILKAJ_VERSION = \".*\"/SILKAJ_VERSION = \"$VERSION\"/" silkaj/constants.py
poetry version "$VERSION"
git diff
}
commit_tag() {
git commit src/constants.py -m "v$VERSION"
git tag "v$VERSION" -a -m "$VERSION"
}
build() {
if [[ -z $VIRTUAL_ENV ]]; then
error_message "Activate silkaj-env"
fi
exec_installed pyinstaller
pyinstaller src/silkaj.py --hidden-import=_cffi_backend --hidden-import=_scrypt --onefile
}
checksum() {
# Generate sha256 checksum file
exec_installed sha256sum
cd dist
sha256sum silkaj > silkaj_sha256sum
git commit silkaj/constants.py pyproject.toml -m "v$VERSION"
git tag "v$VERSION" -a -m "v$VERSION"
}
exec_installed() {
if [[ ! `command -v $1` ]]; then
error_message "'$1' is not install on your machine"
error_message "'$1' is not installed on your machine"
fi
}
......@@ -59,9 +39,5 @@ error_message() {
check_argument_specified
check_version_format
check_branch
update_version
commit_tag
build
checksum
error_message "Build and checksum can be found in 'dist' folder"
src/silkaj.py
\ No newline at end of file
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
name = "silkaj"
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import rich_click as click
from silkaj.constants import SILKAJ_VERSION
@click.command("about", help="Display program information")
def about() -> None:
print(
"\
\n @@@@@@@@@@@@@\
\n @@@ @ @@@\
\n @@@ @@ @@@@@@ @@. Silkaj",
SILKAJ_VERSION,
"\
\n @@ @@@ @@@@@@@@@@@ @@,\
\n @@ @@@ &@@@@@@@@@@@@@ @@@ Command line client for Ğ1 libre currency powered by Duniter\
\n @@ @@@ @@@@@@@@@# @@@@ @@(\
\n @@ @@@@ @@@@@@@@@ @@@ @@ Built with Python for Duniter's currencies: \
Ğ1 and Ğ1-Test\
\n @@ @@@ @@@@@@@@ @ @@@ @@\
\n @@ @@@ @@@@@@ @@@@ @@ @@ Authors: see AUTHORS.md file\
\n @@ @@@@ @@@ @@@@@@@ @@ @@\
\n @@ @@@@* @@@@@@@@@ @# @@ Website: https://silkaj.duniter.org\
\n @@ @@@@@ @@@@@@@@@@ @ ,@@\
\n @@ @@@@@ @@@@@@@@@@ @ ,@@ Repository: \
https://git.duniter.org/clients/python/silkaj\
\n @@@ @@@@@@@@@@@@ @ @@*\
\n @@@ @@@@@@@@ @ @@@ License: GNU AGPLv3\
\n @@@@ @@ @@@,\
\n @@@@@@@@@@@@@@@\n",
)
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
from pathlib import Path
from silkaj import tools
from silkaj.blockchain import tools as bc_tools
class AccountStorage:
xdg_data_home = ".local/share"
program_name = "silkaj"
revocation_file_name = "revocation.txt"
authentication_v1_file_name = "authentication_file_ed25519.dewif"
authentication_v2_file_name = "authentication_file_sr25519.json"
def __init__(self) -> None:
self.account_name = tools.has_account_defined()
self.path = Path.home().joinpath(
self.xdg_data_home,
self.program_name,
bc_tools.get_currency(),
self.account_name,
)
self.path.mkdir(parents=True, exist_ok=True)
def authentication_file_path(self, check_exist: bool = True) -> Path:
auth_file_path = self.path.joinpath(self.authentication_v1_file_name)
if check_exist and not auth_file_path.is_file():
tools.click_fail(
f"{auth_file_path} not found for account name: {self.account_name}",
)
return auth_file_path
def revocation_path(self, check_exist: bool = True) -> Path:
revocation_path = self.path.joinpath(self.revocation_file_name)
if check_exist and not revocation_path.is_file():
tools.click_fail(
f"{revocation_path} not found for account name: {self.account_name}",
)
return revocation_path
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import re
from pathlib import Path
from typing import Optional
import rich_click as click
from duniterpy.key.scrypt_params import ScryptParams
from duniterpy.key.signing_key import SigningKey, SigningKeyException
from silkaj import tools
from silkaj.account_storage import AccountStorage
from silkaj.constants import PUBKEY_PATTERN
from silkaj.public_key import gen_pubkey_checksum
SEED_HEX_PATTERN = "^[0-9a-fA-F]{64}$"
PUBSEC_PUBKEY_PATTERN = f"pub: ({PUBKEY_PATTERN})"
PUBSEC_SIGNKEY_PATTERN = "sec: ([1-9A-HJ-NP-Za-km-z]{87,90})"
@click.pass_context
def auth_method(ctx: click.Context) -> SigningKey:
"""Account storage authentication"""
password = ctx.obj["PASSWORD"]
authfile = AccountStorage().authentication_file_path()
wif_content = authfile.read_text()
regex = re.compile("Type: ([a-zA-Z]+)", re.MULTILINE)
match = re.search(regex, wif_content)
if match and match.groups()[0] == "EWIF" and not password:
password = click.prompt("Encrypted WIF, enter your password", hide_input=True)
return auth_by_wif_file(authfile, password)
def auth_options(
auth_file: Path,
auth_seed: bool,
auth_wif: bool,
nrp: Optional[str] = None,
) -> SigningKey:
"""Authentication from CLI options"""
if auth_file:
return auth_by_auth_file(auth_file)
if auth_seed:
return auth_by_seed()
if auth_wif:
return auth_by_wif()
return auth_by_scrypt(nrp)
@click.command("authentication", help="Generate and store authentication file")
@click.option(
"--auth-scrypt",
"--scrypt",
is_flag=True,
help="Scrypt authentication. Default method",
cls=tools.MutuallyExclusiveOption,
mutually_exclusive=["auth_file", "auth_seed", "auth_wif"],
)
@click.option("--nrp", help='Scrypt parameters: defaults N,r,p: "4096,16,1"')
@click.option(
"--auth-file",
"-af",
type=click.Path(exists=True, dir_okay=False, path_type=Path),
help="Seed hexadecimal authentication from file path",
cls=tools.MutuallyExclusiveOption,
mutually_exclusive=["auth_scrypt", "auth_seed", "auth_wif"],
)
@click.option(
"--auth-seed",
"--seed",
is_flag=True,
help="Seed hexadecimal authentication",
cls=tools.MutuallyExclusiveOption,
mutually_exclusive=["auth_scrypt", "auth_file", "auth_wif"],
)
@click.option(
"--auth-wif",
"--wif",
is_flag=True,
help="WIF and EWIF authentication methods",
cls=tools.MutuallyExclusiveOption,
mutually_exclusive=["auth_scrypt", "auth_file", "auth_seed"],
)
@click.option(
"--password",
"-p",
help="EWIF encryption password for the destination file. \
If no password argument is passed, WIF format will be used. \
If you use this option prefix the command \
with a space so the password does not get saved in your shell history. \
Password input will be suggested via a prompt.",
)
@click.pass_context
def generate_auth_file(
ctx: click.Context,
auth_scrypt: bool,
nrp: Optional[str],
auth_file: Path,
auth_seed: bool,
auth_wif: bool,
password: Optional[str],
) -> None:
auth_file_path = AccountStorage().authentication_file_path(check_exist=False)
if not password and click.confirm(
"Would you like to encrypt the generated authentication file?",
):
password = click.prompt("Enter encryption password", hide_input=True)
if password:
password_confirmation = click.prompt(
"Enter encryption password confirmation",
hide_input=True,
)
if password != password_confirmation:
tools.click_fail("Entered passwords differ")
key = auth_options(auth_file, auth_seed, auth_wif, nrp)
pubkey_cksum = gen_pubkey_checksum(key.pubkey)
if auth_file_path.is_file():
message = (
f"Would you like to erase {auth_file_path} with an authentication file corresponding \
to following pubkey `{pubkey_cksum}`?"
)
click.confirm(message, abort=True)
if password:
key.save_ewif_file(auth_file_path, password)
else:
key.save_wif_file(auth_file_path)
print(
f"Authentication file '{auth_file_path}' generated and stored for public key: {pubkey_cksum}",
)
@click.pass_context
def auth_by_auth_file(ctx: click.Context, authfile: Path) -> SigningKey:
"""
Uses an authentication file to generate the key
Authfile can either be:
* A seed in hexadecimal encoding
* PubSec format with public and private key in base58 encoding
"""
filetxt = authfile.read_text(encoding="utf-8")
# two regural expressions for the PubSec format
regex_pubkey = re.compile(PUBSEC_PUBKEY_PATTERN, re.MULTILINE)
regex_signkey = re.compile(PUBSEC_SIGNKEY_PATTERN, re.MULTILINE)
# Seed hexadecimal format
if re.search(re.compile(SEED_HEX_PATTERN), filetxt):
return SigningKey.from_seedhex_file(authfile)
# PubSec format
if re.search(regex_pubkey, filetxt) and re.search(regex_signkey, filetxt):
return SigningKey.from_pubsec_file(authfile)
tools.click_fail("The format of the file is invalid")
return None
def auth_by_seed() -> SigningKey:
seedhex = click.prompt("Please enter your seed on hex format", hide_input=True)
try:
return SigningKey.from_seedhex(seedhex)
except SigningKeyException as error:
tools.click_fail(error)
@click.pass_context
def auth_by_scrypt(ctx: click.Context, nrp: Optional[str]) -> SigningKey:
salt = click.prompt(
"Please enter your Scrypt Salt (Secret identifier)",
hide_input=True,
default="",
)
password = click.prompt(
"Please enter your Scrypt password (masked)",
hide_input=True,
default="",
)
if nrp:
a, b, c = nrp.split(",")
if a.isnumeric() and b.isnumeric() and c.isnumeric():
n, r, p = int(a), int(b), int(c)
if n <= 0 or n > 65536 or r <= 0 or r > 512 or p <= 0 or p > 32:
tools.click_fail("The values of Scrypt parameters are not good")
scrypt_params = ScryptParams(n, r, p)
else:
tools.click_fail("one of n, r or p is not a number")
else:
scrypt_params = None
try:
return SigningKey.from_credentials(salt, password, scrypt_params)
except SigningKeyException as error:
tools.click_fail(error)
def auth_by_wif() -> SigningKey:
wif_hex = click.prompt(
"Enter your WIF or Encrypted WIF address (masked)",
hide_input=True,
)
password = click.prompt(
"(Leave empty in case WIF format) Enter the Encrypted WIF password (masked)",
hide_input=True,
)
try:
return SigningKey.from_wif_or_ewif_hex(wif_hex, password)
except SigningKeyException as error:
tools.click_fail(error)
def auth_by_wif_file(wif_file: Path, password: Optional[str] = None) -> SigningKey:
try:
return SigningKey.from_wif_or_ewif_file(wif_file, password)
except SigningKeyException as error:
tools.click_fail(error)
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import time
from operator import itemgetter
from urllib.error import HTTPError
import pendulum
import rich_click as click
from duniterpy.api import bma
from silkaj import tui
from silkaj.blockchain.tools import get_head_block
from silkaj.constants import ALL, BMA_SLEEP
from silkaj.network import client_instance
from silkaj.wot.tools import identity_of
@click.command("blocks", help="Display blocks: default: 0 for current window size")
@click.argument("number", default=0, type=click.IntRange(0, 5000))
@click.option(
"--detailed",
"-d",
is_flag=True,
help="Force detailed view. Compact view happen over 30 blocks",
)
def list_blocks(number: int, detailed: bool) -> None:
head_block = get_head_block()
current_nbr = head_block["number"]
if number == 0:
number = head_block["issuersFrame"]
client = client_instance()
blocks = client(bma.blockchain.blocks, number, current_nbr - number + 1)
issuers = []
issuers_dict = {}
for block in blocks:
issuer = {}
issuer["pubkey"] = block["issuer"]
if detailed or number <= 30:
issuer["block"] = block["number"]
issuer["gentime"] = pendulum.from_timestamp(
block["time"], tz="local"
).format(ALL)
issuer["mediantime"] = pendulum.from_timestamp(
block["medianTime"], tz="local"
).format(ALL)
issuer["hash"] = block["hash"][:10]
issuer["powMin"] = block["powMin"]
issuers_dict[issuer["pubkey"]] = issuer
issuers.append(issuer)
for pubkey in issuers_dict.items():
issuer = issuers_dict[pubkey[0]]
time.sleep(BMA_SLEEP)
try:
idty = identity_of(issuer["pubkey"])
except HTTPError:
idty = None
for issuer2 in issuers:
if (
issuer2.get("pubkey") is not None
and issuer.get("pubkey") is not None
and issuer2["pubkey"] == issuer["pubkey"]
):
issuer2["uid"] = idty["uid"] if idty else None
issuer2.pop("pubkey")
print_blocks_views(issuers, current_nbr, number, detailed)
def print_blocks_views(issuers, current_nbr, number, detailed):
header = (
f"Last {number} blocks from n°{current_nbr - number + 1} to n°{current_nbr}"
)
print(header, end=" ")
if detailed or number <= 30:
sorted_list = sorted(issuers, key=itemgetter("block"), reverse=True)
table = tui.Table(style="columns")
table.set_cols_align(["r", "r", "r", "r", "r", "l"])
table.set_cols_dtype(["i", "t", "t", "t", "i", "t"])
table.fill_from_dict_list(sorted_list)
table.set_cols_align(["r", "r", "r", "r", "r", "l"])
table.set_cols_dtype(["i", "t", "t", "t", "i", "t"])
print(f"\n{table.draw()}")
else:
list_issued = []
for issuer in issuers:
found = False
for issued in list_issued:
if issued.get("uid") is not None and issued["uid"] == issuer["uid"]:
issued["blocks"] += 1
found = True
break
if not found:
issued = {}
issued["uid"] = issuer["uid"]
issued["blocks"] = 1
list_issued.append(issued)
for issued in list_issued:
issued["percent"] = round(issued["blocks"] / number * 100)
sorted_list = sorted(list_issued, key=itemgetter("blocks"), reverse=True)
table = tui.Table(style="columns")
table.fill_from_dict_list(sorted_list)
table.set_cols_align(["l", "r", "r"])
table.set_cols_dtype(["t", "i", "i"])
print(f"from {len(list_issued)} issuers\n{table.draw()}")
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
from operator import itemgetter
from os import system
import jsonschema
import pendulum
import rich_click as click
from duniterpy.api import bma
from duniterpy.api.client import WSConnection
from websocket._exceptions import WebSocketConnectionClosedException
from silkaj import network, tui
from silkaj.constants import ALL
@click.command(
"difficulty",
help="Display current Proof-of-Work difficulty level to generate next block",
)
def difficulties() -> None:
client = network.client_instance()
try:
ws = client(bma.ws.block)
while True:
current = ws.receive_json()
jsonschema.validate(current, bma.ws.WS_BLOCK_SCHEMA)
diffi = client(bma.blockchain.difficulties)
display_diffi(current, diffi)
except (jsonschema.ValidationError, WebSocketConnectionClosedException) as e:
print(f"{e.__class__.__name__!s}: {e!s}")
def display_diffi(current: WSConnection, diffi: dict) -> None:
issuers = 0
sorted_diffi = sorted(diffi["levels"], key=itemgetter("level"), reverse=True)
for d in diffi["levels"]:
if d["level"] / 2 < current["powMin"]:
issuers += 1
d["match"] = match_pattern(d["level"])[0][:20]
d["Π diffi"] = compute_power(match_pattern(d["level"])[1])
d["Σ diffi"] = d.pop("level")
system("cls||clear")
block_gen = pendulum.from_timestamp(current["time"], tz="local").format(ALL)
match = match_pattern(int(current["powMin"]))[0]
table = tui.Table(style="columns").set_cols_dtype(["t", "t", "t", "i"])
table.fill_from_dict_list(sorted_diffi)
content = f"Current block: n°{current['number']}, generated on {block_gen}\n\
Generation of next block n°{diffi['block']} \
possible by at least {issuers}/{len(diffi['levels'])} members\n\
Common Proof-of-Work difficulty level: {current['powMin']}, hash starting with `{match}`\n\
{table.draw()}"
print(content)
def match_pattern(_pow: int, match: str = "", p: int = 1) -> tuple[str, int]:
while _pow > 0:
if _pow >= 16:
match += "0"
_pow -= 16
p *= 16
else:
match += f"[0-{hex(15 - _pow)[2:].upper()}]"
p *= _pow
_pow = 0
return f"{match}*", p
def compute_power(nbr: float, power: int = 0) -> str:
while nbr >= 10:
nbr /= 10
power += 1
return f"{nbr:.1f} x 10^{power}"
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import pendulum
import rich_click as click
from silkaj.blockchain.tools import get_head_block
from silkaj.constants import ALL
from silkaj.network import determine_endpoint
from silkaj.tools import get_currency_symbol
@click.command("info", help="Currency information")
def currency_info() -> None:
head_block = get_head_block()
ep = determine_endpoint()
current_time = pendulum.from_timestamp(head_block["time"], tz="local")
mediantime = pendulum.from_timestamp(head_block["medianTime"], tz="local")
print(
"Connected to node:",
ep.host,
ep.port,
"\nCurrent block number:",
head_block["number"],
"\nCurrency name:",
get_currency_symbol(),
"\nNumber of members:",
head_block["membersCount"],
"\nMinimal Proof-of-Work:",
head_block["powMin"],
"\nCurrent time:",
current_time.format(ALL),
"\nMedian time:",
mediantime.format(ALL),
"\nDifference time:",
current_time.diff_for_humans(mediantime, True),
)
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import functools
from duniterpy.api.bma import blockchain
from silkaj.network import client_instance
@functools.lru_cache(maxsize=1)
def get_blockchain_parameters() -> dict:
client = client_instance()
return client(blockchain.parameters)
@functools.lru_cache(maxsize=1)
def get_head_block() -> dict:
client = client_instance()
return client(blockchain.current)
@functools.lru_cache(maxsize=1)
def get_currency() -> str:
return get_head_block()["currency"]
# Copyright 2016-2025 Maël Azimi <m.a@moul.re>
#
# Silkaj is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Silkaj is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
import re
import rich_click as click
from silkaj import auth, tools
from silkaj.public_key import (
PUBKEY_CHECKSUM_PATTERN,
PUBKEY_DELIMITED_PATTERN,
gen_checksum,
gen_pubkey_checksum,
)
MESSAGE = "You should specify a pubkey or an authentication method"
@click.command(
"checksum",
help="Generate checksum out of a passed pubkey or an authentication method. \
Checks if the passed checksum is valid.",
)
@click.argument("pubkey_checksum", nargs=-1)
def checksum_command(pubkey_checksum: str) -> None:
if tools.has_account_defined(exit_error=False):
key = auth.auth_method()
click.echo(gen_pubkey_checksum(key.pubkey))
elif not pubkey_checksum:
tools.click_fail(MESSAGE)
elif re.search(re.compile(PUBKEY_DELIMITED_PATTERN), pubkey_checksum[0]):
click.echo(gen_pubkey_checksum(pubkey_checksum[0]))
elif re.search(re.compile(PUBKEY_CHECKSUM_PATTERN), pubkey_checksum[0]):
pubkey, checksum = pubkey_checksum[0].split(":")
if checksum == gen_checksum(pubkey):
click.echo("The checksum is valid")
else:
click.echo("The checksum is invalid")
else:
tools.click_fail("Wrong public key format")