Skip to content
Snippets Groups Projects
Commit f8b74005 authored by vjrj's avatar vjrj
Browse files

Second round

parent 98743978
No related branches found
No related tags found
No related merge requests found
Showing
with 1100 additions and 493 deletions
# Ğ1nkgo
Ğ1nkgo (aka Ginkgo) is a lightweight Ĝ1 wallet for Duniter v1 written in Flutter. The app allows users to manage their Ĝ1 currency on their mobile device using just a browser.
![Ğ1nkgo logo](./assets/img/logo.png "Ğ1nkgo logo")
Ğ1nkgo (aka Ginkgo) is a lightweight Ğ1 wallet for Duniter v1 written in Flutter. The app allows
users to manage their Ğ1 currency on their mobile device using just a browser.
## Features
* Introduction for beginners
* Generation of Cesium wallet and persistence (if you refresh the page, it should display the same wallet address).
* A point-of-sale device that generates a QR code for the public address and other QR codes with amounts (which lightweight wallets will understand).
* Generation of Cesium wallet and persistence (if you refresh the page, it should display the same
wallet address).
* A point-of-sale device that generates a QR code for the public address and other QR codes with
amounts (which lightweight wallets will understand).
* Internationalization (i18n)
* Some contextual help (for example, by tapping on "Validity").
* Connectivity detection (to retry transactions)
......@@ -14,9 +19,10 @@
## Work in progress
* Send and receive Ĝ1 transactions
* View transaction history
* View Ĝ1 balance and currency conversion rate
* Automatic selection of nodes
* Send and receive Ğ1 transactions
* View transaction history
* View Ğ1 balance and currency conversion rate
## Demo
......@@ -34,8 +40,6 @@ git clone https://git.duniter.org/vjrj/ginkgo.git
cd ginkgo
```
You can remove the screenshots located in [assets/img/](./assets/img).
Get the dependencies.
```sh
......@@ -69,7 +73,8 @@ This repository makes use of the following pub packages:
#### Easy Localization
To add translations, add it to `assets/translations` and enable it in `main.dart`. Also go to [ios/Runner/Info.plist](./ios/Runner/Info.plist) and update the following code:
To add translations, add it to `assets/translations` and enable it in `main.dart`. Also go
to [ios/Runner/Info.plist](./ios/Runner/Info.plist) and update the following code:
```
<key>CFBundleLocalizations</key>
......@@ -78,6 +83,7 @@ To add translations, add it to `assets/translations` and enable it in `main.dart
<string>es</string>
</array>
```
``
## Screenshots
......@@ -88,9 +94,10 @@ To add translations, add it to `assets/translations` and enable it in `main.dart
## Credits
- Ĝ1 logo from duniter.org used in the card
- undraw intro images: https://undraw.co/license
- Chipcard https://commons.wikimedia.org/wiki/File:Chipcard.svg under the Creative Commons Attribution-Share Alike 3.0 Unported license.
- Ğ1 logo from duniter.org used in the card
- undraw intro images: https://undraw.co/license
- Chipcard https://commons.wikimedia.org/wiki/File:Chipcard.svg under the Creative Commons
Attribution-Share Alike 3.0 Unported license.
Thanks!
......
assets/img/favicon.ico

142 KiB | W: 256px | H: 256px

assets/img/favicon.ico

146 KiB | W: 256px | H: 256px

assets/img/favicon.ico
assets/img/favicon.ico
assets/img/favicon.ico
assets/img/favicon.ico
  • 2-up
  • Swipe
  • Onion skin
assets/img/favicon.png

7.48 KiB | W: | H:

assets/img/favicon.png

11.9 KiB | W: | H:

assets/img/favicon.png
assets/img/favicon.png
assets/img/favicon.png
assets/img/favicon.png
  • 2-up
  • Swipe
  • Onion skin
#!/bin/bash
optipng gbrevedot.png
optipng leaf.png
convert gbrevedot.png -resize 256x256 \
convert leaf.png -resize 256x256 \
-define icon:auto-resize="256,128,96,64,48,32,16" \
favicon.ico
convert gbrevedot.png -resize 256x256 favicon.png
convert leaf.png -resize 256x256 favicon.png
cp favicon.png ../../web/
for i in 192 512; do convert gbrevedot.png -resize $ix$i ../../web/icons/Icon-$i.png; done
for i in 192 512; do convert gbrevedot.png -resize $ix$i ../../web/icons/Icon-maskable-$i.png; done
for i in 192 512; do convert leaf.png -resize $ix$i ../../web/icons/Icon-$i.png; done
for i in 192 512; do convert leaf.png -resize $ix$i ../../web/icons/Icon-maskable-$i.png; done
assets/img/leaf.png

2.64 KiB

assets/img/logo.png

6.87 KiB

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="84.58181mm"
height="53.416752mm"
viewBox="0 0 84.581812 53.416751"
version="1.1"
id="svg5"
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
sodipodi:docname="logo.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="false"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="1"
inkscape:cx="-162.5"
inkscape:cy="203.5"
inkscape:window-width="3440"
inkscape:window-height="1370"
inkscape:window-x="0"
inkscape:window-y="33"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:showpageshadow="false"
inkscape:lockguides="false" />
<defs
id="defs2">
<rect
x="61.754852"
y="201.36572"
width="452.37518"
height="165.14932"
id="rect957" />
<rect
x="61.754852"
y="201.36572"
width="1928.033"
height="774.1853"
id="rect957-4" />
<rect
x="61.754852"
y="201.36572"
width="1928.033"
height="774.1853"
id="rect957-4-1" />
<rect
x="61.754852"
y="201.36572"
width="452.37518"
height="165.14932"
id="rect957-1" />
<rect
x="61.754852"
y="201.36572"
width="452.37518"
height="165.14932"
id="rect957-3" />
</defs>
<g
inkscape:label="Capa 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-74.527161,-114.07086)">
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,57.400707,86.714039)"
id="text955-1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:96px;line-height:1.25;font-family:'Tilt Warp';-inkscape-font-specification:'Tilt Warp, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;white-space:pre;shape-inside:url(#rect957-3);fill:#445500;fill-opacity:1;stroke:none"
inkscape:export-filename="/home/vjrj/dev/ginkgo/assets/img/logo.png"
inkscape:export-xdpi="70"
inkscape:export-ydpi="70"><tspan
x="61.753906"
y="288.67427"
id="tspan2320">Ğ1nkgo</tspan></text>
<path
id="path82264-3"
style="fill:#aad400;stroke:none;stroke-width:0.279393"
d="m 108.09099,115.01624 c -1.59852,1.47475 -0.52338,3.68473 -0.15819,4.59287 0.54281,1.32029 0.43573,2.68372 1.07573,3.9691 -1.25394,-0.96575 -1.44,-2.95185 -2.22273,-4.27643 -0.78273,-1.32458 -2.55066,-2.61055 -4.13321,-2.41125 -2.61765,0.32965 -4.243773,2.94399 -5.634295,5.55891 -1.390521,2.61493 -1.47491,5.58192 -1.4771,9.46391 0.25808,1.61908 2.246796,1.56416 3.503536,1.36285 2.067679,-0.21544 4.130989,-0.60839 6.213799,-0.62888 1.97994,0.0416 4.25656,0.48639 5.35704,2.32683 1.46632,2.20022 1.82978,4.88724 2.13105,7.45207 -0.24635,1.32525 1.89259,2.11646 1.36594,0.35383 -0.0407,-3.20181 -2.41878,-5.96792 -2.04335,-9.20883 0.42116,-2.02556 2.30346,-3.26024 4.09562,-3.98561 2.003,-1.08618 4.21275,-1.6672 6.38229,-2.3005 1.90912,-0.43653 3.07227,-2.59338 2.13084,-4.37519 -0.68061,-1.53552 -1.63574,-3.2019 -3.80832,-5.15075 -2.17259,-1.94885 -9.11778,-5.34806 -12.77865,-2.74293 z"
sodipodi:nodetypes="ccczazcccccccccczc"
inkscape:export-filename="/home/vjrj/dev/ginkgo/assets/img/logo.png"
inkscape:export-xdpi="70"
inkscape:export-ydpi="70" />
</g>
</svg>
......@@ -8,9 +8,9 @@
"bottom_nav_trd": "Contacts",
"bottom_nav_frd": "Balance",
"bottom_nav_fifth": "Info",
"title_first": "Send Ĝ1",
"title_first": "Send Ğ1",
"g1_amount": "Amount to send",
"g1_amount_hint": "Amount to send in Ĝ1",
"g1_amount_hint": "Amount to send in Ğ1",
"g1_form_pay_send": "Send",
"search_user_title": "User to pay",
"search_user": "Search (user or public key)",
......@@ -39,7 +39,15 @@
"keys-tooltip": "Public and private keys in Ğ1 and Duniter are like a lock and key system, where the public key acts as the lock that can be opened by anyone with the corresponding private key, providing a secure way to authenticate and verify transactions",
"card-validity": "Validity",
"card-validity-tooltip": "Please note that this wallet is only accessible while using this specific browser and device. If you delete or reset the browser, you will lose access to this wallet and the funds stored in it.",
"demo-title": "This is a demo",
"demo-desc": "Please refrain from using this with real transactions for now.",
"connected-to": "We are connected to node:"
"connected-to": "We are connected to node:",
"export-key": "Export your wallet",
"import-key": "Import your wallet",
"copy-your-key": "Copy your public key",
"key-copied-to-clipboard": "Your public key has been copied to the clipboard",
"key_tools_title": "Keys and Tools",
"transactions": "Transactions",
"balance": "Balance",
"transaction_from_to": "From {from} to {to}",
"your_wallet": "your wallet"
}
......@@ -8,9 +8,9 @@
"bottom_nav_trd": "Contactos",
"bottom_nav_frd": "Saldo",
"bottom_nav_fifth": "Información",
"title_first": "Enviar Ĝ1",
"title_first": "Enviar Ğ1",
"g1_amount": "Monto a enviar",
"g1_amount_hint": "Monto a enviar en Ĝ1",
"g1_amount_hint": "Monto a enviar en Ğ1",
"g1_form_pay_send": "Enviar",
"search_user_title": "Usuario a pagar",
"search_user": "Buscar (usuari@ o clave pública)",
......@@ -39,7 +39,15 @@
"keys-tooltip": "Las claves públicas y privadas en Ğ1 y Duniter son como un sistema de cerradura y llave, donde la clave pública actúa como la cerradura que puede ser abierta por cualquiera que tenga la clave privada correspondiente, proporcionando una forma segura de autenticar y verificar transacciones.",
"card-validity": "Validez",
"card-validity-tooltip": "Tenga en cuenta que este monedero solo es accesible mientras utiliza este navegador y este dispositivo específico. Si borra o restablece el navegador, perderá el acceso a este monedero y los fondos almacenados en el.",
"demo-title": "Esto es una demostración",
"demo-desc": "Por favor, no utilice esto aún para transacciones reales.",
"connected-to": "Estamos conectados al nodo:"
"connected-to": "Estamos conectados al nodo:",
"export-key": "Exporta tu monedero",
"import-key": "Importa tu monedero",
"copy-your-key": "Copia tu clave pública",
"key-copied-to-clipboard": "Tu clave pública se ha copiado al portapapeles",
"key_tools_title": "Llaves y Herramientas",
"transactions": "Transacciones",
"balance": "Balance",
"transaction_from_to": "Desde {from} a {to}",
"your_wallet": "tu monedero"
}
......@@ -8,9 +8,9 @@
"bottom_nav_trd": "Contacts",
"bottom_nav_frd": "Solde",
"bottom_nav_fifth": "Info",
"title_first": "Envoyer des Ĝ1",
"title_first": "Envoyer des Ğ1",
"g1_amount": "Montant à envoyer",
"g1_amount_hint": "Montant à envoyer en Ĝ1",
"g1_amount_hint": "Montant à envoyer en Ğ1",
"g1_form_pay_send": "Envoyer",
"search_user_title": "Utilisateur à payer",
"search_user": "Rechercher (utilisateur ou clé publique)",
......@@ -39,7 +39,15 @@
"keys-tooltip": "Les clés publiques et privées en Ğ1 et Duniter fonctionnent comme un système de verrou et de clé, où la clé publique agit comme le verrou qui peut être ouvert par n'importe qui ayant la clé privée correspondante, offrant ainsi un moyen sécurisé d'authentifier et de vérifier les transactions",
"card-validity": "Validité",
"card-validity-tooltip": "Veuillez noter que ce portefeuille n'est accessible que lors de l'utilisation de ce navigateur et de cet appareil spécifiques. Si vous supprimez ou réinitialisez le navigateur, vous perdrez l'accès à ce portefeuille et aux fonds qu'il contient.",
"demo-title": "Ceci est une démonstration",
"demo-desc": "Veuillez vous abstenir d'utiliser ceci avec de vraies transactions pour le moment.",
"connected-to": "Nous sommes connectés au nœud:"
"connected-to": "Nous sommes connectés au nœud:",
"export-key": "Exporter votre portefeuille",
"import-key": "Importer votre portefeuille",
"copy-your-key": "Copier votre clé publique",
"key-copied-to-clipboard": "Votre clé publique a été copiée dans le presse-papiers",
"key_tools_title": "Clés et outils",
"transactions": "Transactions",
"balance": "Solde",
"transaction_from_to": "De {from} à {to}",
"your_wallet": "votre portefeuille"
}
{
"currency": "g1",
"pubkey": "9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG",
"history": {
"sending": [],
"received": [
{
"version": 10,
"locktime": 0,
"blockstamp": "596811-00000010212BED3B897DF62268149929C349BBD4B1766305485222E9884CB1D2",
"blockstampTime": 1674898161,
"issuers": [
"A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB"
],
"inputs": [
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:518825",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:519111",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:519375",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:519668",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:519955",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:520225",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:520510",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:520798",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:521073",
"1051:0:D:A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB:521341"
],
"outputs": [
"10000:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)",
"510:0:SIG(A1Fc1VoCLKHyPYmXimYECSmjmsceqwRSZcTBXfgG9JaB)"
],
"unlocks": [
"0:SIG(0)",
"1:SIG(0)",
"2:SIG(0)",
"3:SIG(0)",
"4:SIG(0)",
"5:SIG(0)",
"6:SIG(0)",
"7:SIG(0)",
"8:SIG(0)",
"9:SIG(0)"
],
"signatures": [
"v5smlhrIXl5wQc2yvsqtJQTWd06YS9D4NJimnnoC0oIwF1f0lKQ078addg85YmEA7bEEph8O2qOn92GZqDjAAw=="
],
"comment": "Bienvenido Vicente :-)",
"hash": "A1C8830A779045C8D8D5D3BC439179666779D5020B7B1D3096C2B3C8B3733420",
"time": 1674899170,
"block_number": 596813,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "602685-0000000CFB8507F5DFFFDAA69D4617334C0C0EE542435A6149B6ED5A8D733BD1",
"blockstampTime": 1676718990,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"10000:0:T:A1C8830A779045C8D8D5D3BC439179666779D5020B7B1D3096C2B3C8B3733420:0"
],
"outputs": [
"1200:0:SIG(EDB7chzCBdtUCnqFZquVeto4a65FjeRkPrqcV8NwVbTx)",
"8800:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"o6WUC+GoyEHgvy9oY3krR/MY7yp6jwqi+39PYTrm6PB6sHJ0ISZpyH98P4FwTiMTWDgD4gWs7vFx1akjnQzwDw=="
],
"comment": "",
"hash": "345530C67C4B4B582432742185311D52AB4FB00C956B41CAEBE45132564EDB30",
"time": 1676720148,
"block_number": 602688,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "602695-000000035E81EA7C15D3C7B287C705374B7E2F63FFA7DD62C713D1E35BE11BF0",
"blockstampTime": 1676723001,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"8800:0:T:345530C67C4B4B582432742185311D52AB4FB00C956B41CAEBE45132564EDB30:1"
],
"outputs": [
"600:0:SIG(84MdshpTmoP1diH6GjhcJ97jxFNGhf5Z8RxMmwRgLCaT)",
"8200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"GD5U15d413K+8OYfDINzmwEZQXL/5t8ZzNFqluMWlzwPfg2L0NTGnh+BTa//hSuIG5YVglsm+HSH0XwUcI5fCw=="
],
"comment": "",
"hash": "77504288DD776AD5573ABE56D2ACB7F4AAC5160E56E29A8D768C37E4C3BC0114",
"time": 1676723896,
"block_number": 602697,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "602748-000000082186C2A605755E08B0457930BF9BB46A2D7670B8F2BCD218E7AB5CC1",
"blockstampTime": 1676741623,
"issuers": [
"ARErWXr3bhKYh8FqX9axMXxxRPXMuoZW4s73P1zBHUTY"
],
"inputs": [
"2500:0:T:48BD39C86D2E11C9B7D0579FBA7BA12146C81D8CCD93E1AD6240BAED2EC94036:0"
],
"outputs": [
"1100:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)",
"1400:0:SIG(ARErWXr3bhKYh8FqX9axMXxxRPXMuoZW4s73P1zBHUTY)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"0qGfZG/a/shiFiH3pEBbl6EOuR15YG/2xCgjXn1ejyozh5YRrL0uOl3HRwo6zD3jfxpFxvzYfC4EDsYhpuLcAw=="
],
"comment": "Devol 3 mercado Feleches",
"hash": "45B4F8020CAF6B0CD8C42CE1A2F5B1DDA3571A48DCF243BE929ACB73EE72FDE6",
"time": 1676742139,
"block_number": 602750,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606584-00000003E02D29358704298395A97F1698136BCF178A23CB76E427396157A3AD",
"blockstampTime": 1677927446,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"1100:0:T:45B4F8020CAF6B0CD8C42CE1A2F5B1DDA3571A48DCF243BE929ACB73EE72FDE6:0"
],
"outputs": [
"900:0:SIG(EDB7chzCBdtUCnqFZquVeto4a65FjeRkPrqcV8NwVbTx)",
"200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"6uZA9JfHusWZ3uo8qVgTvLICCUzaMllSzEj22nEqxvKIntJhEvV+yBf5y4IE/jNHvm0thgt/i/7Pv7jvyYPeDg=="
],
"comment": "",
"hash": "9ADE23150CCB2727FFCE7AC4F9D3040B9E098AB9E502184C38DD047C3D439E25",
"time": 1677927915,
"block_number": 606586,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606587-00000021331ECE824F07DF74D22637D45D54B2C4A16C70C8842630545ED9E2A5",
"blockstampTime": 1677928124,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"8200:0:T:77504288DD776AD5573ABE56D2ACB7F4AAC5160E56E29A8D768C37E4C3BC0114:1"
],
"outputs": [
"300:0:SIG(AFv1D5xA7FCdHCTA1bqfQ3PWvwEM16Gw67QJ37obGnsv)",
"7900:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"PSNsEgyGwM9UCJ19C8mohat0xUpDSWrHBToIqq1i1Ajfa2py5jdiHoUr88k2HOyCraQYb8yOsHujNmo2l6B9BA=="
],
"comment": "Moneda",
"hash": "5579C1ECC29FF8989DFFD9D51F68F85CCC41F121DCA2E3E54868CB2F94E9DA0C",
"time": 1677928643,
"block_number": 606589,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606587-00000021331ECE824F07DF74D22637D45D54B2C4A16C70C8842630545ED9E2A5",
"blockstampTime": 1677928124,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"200:0:T:9ADE23150CCB2727FFCE7AC4F9D3040B9E098AB9E502184C38DD047C3D439E25:1",
"7900:0:T:5579C1ECC29FF8989DFFD9D51F68F85CCC41F121DCA2E3E54868CB2F94E9DA0C:1"
],
"outputs": [
"1400:0:SIG(Dgawofin5qT9W4oQSXEKMkkuAkk3rAqrekcxo4qVTcPJ)",
"6700:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)",
"1:SIG(0)"
],
"signatures": [
"W1QhJJP+jVxOToiQUkOM5u0Dhs0W46ja7Q9F2jNLz+cFdQ4KwvRWYjlScHRhNXkEnr38Qzk3T3Ku+8SuiddfAQ=="
],
"comment": "Plantas de melisa y tal",
"hash": "B7B8E7BABDA683FCCA9242C497635E7036CBE1706552080640883997DD4433C1",
"time": 1677928643,
"block_number": 606589,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606591-0000000646D1648737BAF1FA78BCCD49EA22B10B9F0254BF9C34A10E0E76B01D",
"blockstampTime": 1677929202,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"6700:0:T:B7B8E7BABDA683FCCA9242C497635E7036CBE1706552080640883997DD4433C1:1"
],
"outputs": [
"1500:0:SIG(HB2ngqUzJZaBpZqMWHUoEreCGYY7ZfzNysmcwvzNx925)",
"5200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"3gahEdwb804JYjj76GaQKYslpoMaU+g1x3+EQCmbU86hpuqDjDHPSyWj2gEUOGfaOBd5BjS1KWsr9L97l7SzAw=="
],
"comment": "",
"hash": "BF9C41EC56177D0F2D051D3BC712AEFA66330E589E4A74A675233963574CF717",
"time": 1677929791,
"block_number": 606593,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606601-0000000753A22FFCFBC9E99CD9EE5ACB35D8B99E053EC1218D78BC7B0E414BBC",
"blockstampTime": 1677932450,
"issuers": [
"2NZfLBXH8675kEYQ7GY2pkmgrSBMN9KiyDXiaGSghQsq"
],
"inputs": [
"1059:0:D:2NZfLBXH8675kEYQ7GY2pkmgrSBMN9KiyDXiaGSghQsq:592376"
],
"outputs": [
"200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)",
"859:0:SIG(2NZfLBXH8675kEYQ7GY2pkmgrSBMN9KiyDXiaGSghQsq)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"SV5YAwat8spdt6Y3o7Hj5ZDGjwRApMJvjGMafo89vOHKu9DiDZGosZAQogQKG+ODcbOelCxTrdo1LgOQCl7NAw=="
],
"comment": "Beso",
"hash": "B99D1E7BDDCA8BEDA54236949C2203A2DBEA272B82C4531C7E2DFA0556DF5572",
"time": 1677933150,
"block_number": 606603,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "607268-0000000A7D4537107B201071C7DA4B09264D0BFA00F30AAD587A79D9B81DFFB3",
"blockstampTime": 1678137803,
"issuers": [
"ARErWXr3bhKYh8FqX9axMXxxRPXMuoZW4s73P1zBHUTY"
],
"inputs": [
"10000:0:T:3E7B9DFD4D6C1F47306D99F545317A4B04D002E2D0E73596D066C836E5FB4EE7:0"
],
"outputs": [
"1300:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)",
"8700:0:SIG(ARErWXr3bhKYh8FqX9axMXxxRPXMuoZW4s73P1zBHUTY)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"Seb65CV62DwLV8aQAgrE5HI2lV5OJbaMCwpO02hC2u1OdzQ11j+85lMRy/2bQRCEAs8uhTOv7Opz0R7XbkuuCg=="
],
"comment": "6 Merkao feleches",
"hash": "0EAE460AF31884FCBA3A5A8668AFE666F44F83B2B75282295CEA5DA3EC889462",
"time": 1678138352,
"block_number": 607270,
"received": null
}
],
"receiving": [],
"sent": [
{
"version": 10,
"locktime": 0,
"blockstamp": "602685-0000000CFB8507F5DFFFDAA69D4617334C0C0EE542435A6149B6ED5A8D733BD1",
"blockstampTime": 1676718990,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"10000:0:T:A1C8830A779045C8D8D5D3BC439179666779D5020B7B1D3096C2B3C8B3733420:0"
],
"outputs": [
"1200:0:SIG(EDB7chzCBdtUCnqFZquVeto4a65FjeRkPrqcV8NwVbTx)",
"8800:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"o6WUC+GoyEHgvy9oY3krR/MY7yp6jwqi+39PYTrm6PB6sHJ0ISZpyH98P4FwTiMTWDgD4gWs7vFx1akjnQzwDw=="
],
"comment": "",
"hash": "345530C67C4B4B582432742185311D52AB4FB00C956B41CAEBE45132564EDB30",
"time": 1676720148,
"block_number": 602688,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "602695-000000035E81EA7C15D3C7B287C705374B7E2F63FFA7DD62C713D1E35BE11BF0",
"blockstampTime": 1676723001,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"8800:0:T:345530C67C4B4B582432742185311D52AB4FB00C956B41CAEBE45132564EDB30:1"
],
"outputs": [
"600:0:SIG(84MdshpTmoP1diH6GjhcJ97jxFNGhf5Z8RxMmwRgLCaT)",
"8200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"GD5U15d413K+8OYfDINzmwEZQXL/5t8ZzNFqluMWlzwPfg2L0NTGnh+BTa//hSuIG5YVglsm+HSH0XwUcI5fCw=="
],
"comment": "",
"hash": "77504288DD776AD5573ABE56D2ACB7F4AAC5160E56E29A8D768C37E4C3BC0114",
"time": 1676723896,
"block_number": 602697,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606584-00000003E02D29358704298395A97F1698136BCF178A23CB76E427396157A3AD",
"blockstampTime": 1677927446,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"1100:0:T:45B4F8020CAF6B0CD8C42CE1A2F5B1DDA3571A48DCF243BE929ACB73EE72FDE6:0"
],
"outputs": [
"900:0:SIG(EDB7chzCBdtUCnqFZquVeto4a65FjeRkPrqcV8NwVbTx)",
"200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"6uZA9JfHusWZ3uo8qVgTvLICCUzaMllSzEj22nEqxvKIntJhEvV+yBf5y4IE/jNHvm0thgt/i/7Pv7jvyYPeDg=="
],
"comment": "",
"hash": "9ADE23150CCB2727FFCE7AC4F9D3040B9E098AB9E502184C38DD047C3D439E25",
"time": 1677927915,
"block_number": 606586,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606587-00000021331ECE824F07DF74D22637D45D54B2C4A16C70C8842630545ED9E2A5",
"blockstampTime": 1677928124,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"8200:0:T:77504288DD776AD5573ABE56D2ACB7F4AAC5160E56E29A8D768C37E4C3BC0114:1"
],
"outputs": [
"300:0:SIG(AFv1D5xA7FCdHCTA1bqfQ3PWvwEM16Gw67QJ37obGnsv)",
"7900:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"PSNsEgyGwM9UCJ19C8mohat0xUpDSWrHBToIqq1i1Ajfa2py5jdiHoUr88k2HOyCraQYb8yOsHujNmo2l6B9BA=="
],
"comment": "Moneda",
"hash": "5579C1ECC29FF8989DFFD9D51F68F85CCC41F121DCA2E3E54868CB2F94E9DA0C",
"time": 1677928643,
"block_number": 606589,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606587-00000021331ECE824F07DF74D22637D45D54B2C4A16C70C8842630545ED9E2A5",
"blockstampTime": 1677928124,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"200:0:T:9ADE23150CCB2727FFCE7AC4F9D3040B9E098AB9E502184C38DD047C3D439E25:1",
"7900:0:T:5579C1ECC29FF8989DFFD9D51F68F85CCC41F121DCA2E3E54868CB2F94E9DA0C:1"
],
"outputs": [
"1400:0:SIG(Dgawofin5qT9W4oQSXEKMkkuAkk3rAqrekcxo4qVTcPJ)",
"6700:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)",
"1:SIG(0)"
],
"signatures": [
"W1QhJJP+jVxOToiQUkOM5u0Dhs0W46ja7Q9F2jNLz+cFdQ4KwvRWYjlScHRhNXkEnr38Qzk3T3Ku+8SuiddfAQ=="
],
"comment": "Plantas de melisa y tal",
"hash": "B7B8E7BABDA683FCCA9242C497635E7036CBE1706552080640883997DD4433C1",
"time": 1677928643,
"block_number": 606589,
"received": null
},
{
"version": 10,
"locktime": 0,
"blockstamp": "606591-0000000646D1648737BAF1FA78BCCD49EA22B10B9F0254BF9C34A10E0E76B01D",
"blockstampTime": 1677929202,
"issuers": [
"9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG"
],
"inputs": [
"6700:0:T:B7B8E7BABDA683FCCA9242C497635E7036CBE1706552080640883997DD4433C1:1"
],
"outputs": [
"1500:0:SIG(HB2ngqUzJZaBpZqMWHUoEreCGYY7ZfzNysmcwvzNx925)",
"5200:0:SIG(9Bcx5JV3swCQBEeH3PcuNcBVperLscWtN78hjFVx1yzG)"
],
"unlocks": [
"0:SIG(0)"
],
"signatures": [
"3gahEdwb804JYjj76GaQKYslpoMaU+g1x3+EQCmbU86hpuqDjDHPSyWj2gEUOGfaOBd5BjS1KWsr9L97l7SzAw=="
],
"comment": "",
"hash": "BF9C41EC56177D0F2D051D3BC712AEFA66330E589E4A74A675233963574CF717",
"time": 1677929791,
"block_number": 606593,
"received": null
}
],
"pending": []
}
}
import 'package:bloc/bloc.dart';
import 'main.dart';
class AppBlocObserver extends BlocObserver {
@override
void onCreate(BlocBase<dynamic> bloc) {
super.onCreate(bloc);
logger('onCreate -- ${bloc.runtimeType}');
}
@override
void onEvent(Bloc<dynamic, dynamic> bloc, Object? event) {
super.onEvent(bloc, event);
logger('onEvent -- $event');
}
@override
void onTransition(Bloc<dynamic, dynamic> bloc, Transition transition) {
super.onTransition(bloc, transition);
logger('onTransition -- $transition');
}
@override
void onError(BlocBase<dynamic> bloc, Object error, StackTrace stackTrace) {
super.onError(bloc, error, stackTrace);
logger('onError -- $error');
}
@override
void onClose(BlocBase<dynamic> bloc) {
super.onClose(bloc);
logger('onClose -- ${bloc.runtimeType}');
}
}
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../g1/duniter_node_manager.dart';
String get duniterNet {
return DuniterNodeManager().fastestNode;
}
String get duniterLookupUrl {
return '${duniterNet}/wot/lookup/';
}
String get duniterNetworkPeers {
return '${duniterNet}/network/peers';
}
String duniterAccountAvatar(String publickey) {
return '${duniterNet}/node/peers/$publickey/avatar';
}
Future<String> getAvatar(String publicKey) async {
final String url = duniterAccountAvatar(publicKey);
final Response response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load avatar');
}
}
import 'package:flutter/material.dart';
/// Colors from Tailwind CSS (v3.0) - June 2022
///
/// https://tailwindcss.com/docs/customizing-colors
const int _primaryColor = 0xFF6366F1;
const MaterialColor primarySwatch = MaterialColor(_primaryColor, <int, Color>{
50: Color(0xFFEEF2FF), // indigo-50
100: Color(0xFFE0E7FF), // indigo-100
200: Color(0xFFC7D2FE), // indigo-200
300: Color(0xFFA5B4FC), // indigo-300
400: Color(0xFF818CF8), // indigo-400
500: Color(_primaryColor), // indigo-500
600: Color(0xFF4F46E5), // indigo-600
700: Color(0xFF4338CA), // indigo-700
800: Color(0xFF3730A3), // indigo-800
900: Color(0xFF312E81), // indigo-900
});
const int _textColor = 0xFF64748B;
const MaterialColor textSwatch = MaterialColor(_textColor, <int, Color>{
50: Color(0xFFF8FAFC), // slate-50
100: Color(0xFFF1F5F9), // slate-100
200: Color(0xFFE2E8F0), // slate-200
300: Color(0xFFCBD5E1), // slate-300
400: Color(0xFF94A3B8), // slate-400
500: Color(_textColor), // slate-500
600: Color(0xFF475569), // slate-600
700: Color(0xFF334155), // slate-700
800: Color(0xFF1E293B), // slate-800
900: Color(0xFF0F172A), // slate-900
});
const Color errorColor = Color(0xFFDC2626); // red-600
final ColorScheme lightColorScheme = ColorScheme.light(
primary: primarySwatch.shade500,
secondary: primarySwatch.shade500,
onSecondary: Colors.white,
error: errorColor,
background: textSwatch.shade200,
onBackground: textSwatch.shade500,
onSurface: textSwatch.shade500,
surface: textSwatch.shade50,
surfaceVariant: Colors.white,
shadow: textSwatch.shade900.withOpacity(.1),
);
final ColorScheme darkColorScheme = ColorScheme.dark(
primary: primarySwatch.shade500,
secondary: primarySwatch.shade500,
onSecondary: Colors.white,
error: errorColor,
background: const Color(0xFF171724),
onBackground: textSwatch.shade400,
onSurface: textSwatch.shade300,
surface: const Color(0xFF262630),
surfaceVariant: const Color(0xFF282832),
shadow: textSwatch.shade900.withOpacity(.2),
);
final ThemeData lightTheme = ThemeData(
colorScheme: lightColorScheme,
fontFamily: 'Nunito',
textTheme: TextTheme(
displayLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
displayMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
displaySmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
headlineLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
headlineMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
headlineSmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
titleLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
titleMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
titleSmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
bodyLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
bodyMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
bodySmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
labelLarge: TextStyle(
color: textSwatch.shade700,
fontFamily: 'Nunito',
),
labelMedium: TextStyle(
color: textSwatch.shade600,
fontFamily: 'Nunito',
),
labelSmall: TextStyle(
color: textSwatch.shade500,
fontFamily: 'Nunito',
),
),
checkboxTheme: CheckboxThemeData(
fillColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
),
radioTheme: RadioThemeData(
fillColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
),
switchTheme: SwitchThemeData(
thumbColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
trackColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
),
const ColorScheme lightColorScheme = ColorScheme(
brightness: Brightness.light,
primary: Color(0xFF526600),
onPrimary: Color(0xFFFFFFFF),
primaryContainer: Color(0xFFD4ED7F),
onPrimaryContainer: Color(0xFF171E00),
secondary: Color(0xFF5B6146),
onSecondary: Color(0xFFFFFFFF),
secondaryContainer: Color(0xFFE0E6C4),
onSecondaryContainer: Color(0xFF191E08),
tertiary: Color(0xFF516600),
onTertiary: Color(0xFFFFFFFF),
tertiaryContainer: Color(0xFFC7F333),
onTertiaryContainer: Color(0xFF161E00),
error: Color(0xFFBA1A1A),
errorContainer: Color(0xFFFFDAD6),
onError: Color(0xFFFFFFFF),
onErrorContainer: Color(0xFF410002),
background: Color(0xFFFEFCF4),
onBackground: Color(0xFF1B1C17),
surface: Color(0xFFFEFCF4),
onSurface: Color(0xFF1B1C17),
surfaceVariant: Color(0xFFE3E4D3),
onSurfaceVariant: Color(0xFF46483C),
outline: Color(0xFF77786B),
onInverseSurface: Color(0xFFF3F1E9),
inverseSurface: Color(0xFF30312B),
inversePrimary: Color(0xFFB8D166),
shadow: Color(0xFF000000),
surfaceTint: Color(0xFF526600),
outlineVariant: Color(0xFFC7C8B8),
scrim: Color(0xFF000000),
);
final ThemeData darkTheme = lightTheme.copyWith(
colorScheme: darkColorScheme,
textTheme: TextTheme(
displayLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
displayMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
displaySmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
headlineLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
headlineMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
headlineSmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
titleLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
titleMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
titleSmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
bodyLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
bodyMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
bodySmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
labelLarge: TextStyle(
color: textSwatch.shade200,
fontFamily: 'Nunito',
),
labelMedium: TextStyle(
color: textSwatch.shade300,
fontFamily: 'Nunito',
),
labelSmall: TextStyle(
color: textSwatch.shade400,
fontFamily: 'Nunito',
),
),
checkboxTheme: CheckboxThemeData(
fillColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
),
radioTheme: RadioThemeData(
fillColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
),
switchTheme: SwitchThemeData(
thumbColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
trackColor:
MaterialStateProperty.resolveWith<Color?>((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled)) {
return null;
}
if (states.contains(MaterialState.selected)) {
return primarySwatch.shade500;
}
return null;
}),
),
const ColorScheme darkColorScheme = ColorScheme(
brightness: Brightness.dark,
primary: Color(0xFFB8D166),
onPrimary: Color(0xFF293500),
primaryContainer: Color(0xFF3D4D00),
onPrimaryContainer: Color(0xFFD4ED7F),
secondary: Color(0xFFC4CAA9),
onSecondary: Color(0xFF2D331B),
secondaryContainer: Color(0xFF444930),
onSecondaryContainer: Color(0xFFE0E6C4),
tertiary: Color(0xFFACD605),
onTertiary: Color(0xFF283500),
tertiaryContainer: Color(0xFF3C4D00),
onTertiaryContainer: Color(0xFFC7F333),
error: Color(0xFFFFB4AB),
errorContainer: Color(0xFF93000A),
onError: Color(0xFF690005),
onErrorContainer: Color(0xFFFFDAD6),
background: Color(0xFF1B1C17),
onBackground: Color(0xFFE4E3DA),
surface: Color(0xFF1B1C17),
onSurface: Color(0xFFE4E3DA),
surfaceVariant: Color(0xFF46483C),
onSurfaceVariant: Color(0xFFC7C8B8),
outline: Color(0xFF909283),
onInverseSurface: Color(0xFF1B1C17),
inverseSurface: Color(0xFFE4E3DA),
inversePrimary: Color(0xFF526600),
shadow: Color(0xFF000000),
surfaceTint: Color(0xFFB8D166),
outlineVariant: Color(0xFF46483C),
scrim: Color(0xFF000000),
);
import 'dart:typed_data';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'payment_state.dart';
class PaymentCubit extends HydratedCubit<PaymentState> {
PaymentCubit() : super(PaymentState.emptyPayment);
void updatePayment({
String? description,
double? amount,
bool? isSent,
}) {
final PaymentState newState = state.copyWith(
description: description,
amount: amount,
isSent: isSent,
);
emit(newState);
}
void selectUser(String publicKey, String nick, Uint8List avatar) {
final PaymentState newState = state.copyWith(
publicKey: publicKey,
nick: nick,
avatar: avatar,
);
emit(newState);
}
void selectKey(String publicKey) {
final PaymentState newState = state.copyWith(
publicKey: publicKey,
);
emit(newState);
}
@override
PaymentState? fromJson(Map<String, dynamic> json) =>
PaymentState.fromJson(json);
@override
Map<String, dynamic>? toJson(PaymentState state) => state.toJson();
void clearRecipient() {
emit(PaymentState.emptyPayment);
}
}
import 'dart:typed_data';
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'payment_state.g.dart';
@JsonSerializable()
class PaymentState extends Equatable {
const PaymentState({
required this.publicKey,
required this.nick,
this.avatar,
required this.description,
required this.amount,
required this.isSent,
});
factory PaymentState.fromJson(Map<String, dynamic> json) =>
_$PaymentStateFromJson(json);
final String publicKey;
final String nick;
@JsonKey(fromJson: _fromList, toJson: _toList)
final Uint8List? avatar;
final String description;
final double amount;
final bool isSent;
Map<String, dynamic> toJson() => _$PaymentStateToJson(this);
PaymentState copyWith({
String? publicKey,
String? nick,
Uint8List? avatar,
String? description,
double? amount,
bool? isSent,
}) {
return PaymentState(
publicKey: publicKey ?? this.publicKey,
nick: nick ?? this.nick,
avatar: avatar ?? this.avatar,
description: description ?? this.description,
amount: amount ?? this.amount,
isSent: isSent ?? this.isSent,
);
}
static PaymentState emptyPayment = const PaymentState(
publicKey: '',
nick: '',
description: '',
amount: 0,
isSent: false,
);
@override
List<Object?> get props =>
<dynamic>[publicKey, nick, avatar, description, amount, isSent];
static Uint8List _fromList(List<int> list) => Uint8List.fromList(list);
static List<int> _toList(Uint8List? uint8List) =>
uint8List != null ? uint8List.toList() : <int>[];
}
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'payment_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
PaymentState _$PaymentStateFromJson(Map<String, dynamic> json) => PaymentState(
publicKey: json['publicKey'] as String,
nick: json['nick'] as String,
avatar: PaymentState._fromList(json['avatar'] as List<int>),
description: json['description'] as String,
amount: (json['amount'] as num).toDouble(),
isSent: json['isSent'] as bool,
);
Map<String, dynamic> _$PaymentStateToJson(PaymentState instance) =>
<String, dynamic>{
'publicKey': instance.publicKey,
'nick': instance.nick,
'avatar': PaymentState._toList(instance.avatar),
'description': instance.description,
'amount': instance.amount,
'isSent': instance.isSent,
};
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import 'node_bloc.dart';
// Tx history
// https://g1.duniter.org/tx/history/FadJvhddHL7qbRd3WcRPrWEJJwABQa3oZvmCBhotc7Kg
// https://g1.duniter.org/tx/history/6DrGg8cftpkgffv4Y4Lse9HSjgc8coEQor3yvMPHAnVH
Future<String> getTxHistory(String publicKey) async {
final Response response =
await NodeBloc().requestWithRetry('/tx/history/$publicKey');
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load tx history');
}
}
Future<Response> getPeers() async {
final Response response = await NodeBloc().requestWithRetry('/network/peers');
if (response.statusCode == 200) {
return response;
} else {
throw Exception('Failed to load duniter node peers');
}
}
Future<Response> searchUser(String searchTerm) async {
final Response response =
await NodeBloc().requestWithRetry('/wot/lookup/$searchTerm');
return response;
}
/*
http://doc.e-is.pro/cesium-plus-pod/REST_API.html#userprofile
Not found sample:
{
"_index": "user",
"_type": "profile",
"_id": "H97or89hW4kzKcvpmFPAAvc1znJrbJWJSYS9XnW37JrM",
"found": false
}
*/
Future<String> getDataImageFromKey(String publicKey) async {
// FIXME (vjrj) use node manager and retry...
final String url = 'https://g1.data.le-sou.org/user/profile/$publicKey';
final Response response = await http.get(Uri.parse(url));
if (response.statusCode == HttpStatus.ok) {
final Map<String, dynamic> data =
json.decode(response.body) as Map<String, dynamic>;
final Map<String, dynamic> source = data['_source'] as Map<String, dynamic>;
if (source.containsKey('avatar')) {
final Map<String, dynamic> avatarData =
source['avatar'] as Map<String, dynamic>;
if (avatarData.containsKey('_content')) {
final String content = avatarData['_content'] as String;
return 'data:image/png;base64,$content';
}
}
}
throw Exception('Failed to load avatar');
}
Uint8List imageFromBase64String(String base64String) {
return Uint8List.fromList(
base64Decode(base64String.substring(base64String.indexOf(',') + 1)));
}
Future<Uint8List> getAvatar(String pubKey) async {
final String dataImage = await getDataImageFromKey(pubKey);
return imageFromBase64String(dataImage);
}
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart';
import '../main.dart';
class DuniterNodeManager {
factory DuniterNodeManager() {
return _instance;
}
DuniterNodeManager._internal() {
_startResetErrorsTimer();
}
void init() {
loadNodes();
}
String get fastestNode {
return _fastestNode!;
}
static final DuniterNodeManager _instance = DuniterNodeManager._internal();
final String _peerListUrl = 'https://g1.duniter.org/network/peers';
final List<String> _nodes = <String>[];
int _currentNodeIndex = 0;
final int _retryCount = 3;
Map<String, int> _nodeErrors = <String, int>{};
Timer? _resetErrorsTimer;
String _fastestNode = 'https://g1.duniter.org';
late Duration _fastestLatency = const Duration(minutes: 1);
Future<dynamic> makeRequest(String endpoint) async {
Response response;
for (int i = 0; i < _nodes.length; i++) {
final String currentNode = _nodes[_currentNodeIndex];
try {
response = await http.get(Uri.parse('$currentNode$endpoint'));
if (response.statusCode == 200) {
_resetNodeErrors(currentNode);
return jsonDecode(response.body);
}
} catch (e) {
_incrementNodeErrors(currentNode);
logger('Error: $e');
}
_rotateNodes();
}
throw Exception('No nodes available');
}
Future<void> loadNodes() async {
try {
final Response response = await http.get(Uri.parse(_peerListUrl));
if (response.statusCode == 200) {
final Map<String, dynamic> peerList =
jsonDecode(response.body) as Map<String, dynamic>;
final List<dynamic> peers = (peerList['peers'] as List<dynamic>)
.where((dynamic peer) =>
(peer as Map<String, dynamic>)['currency'] == 'g1')
.where((dynamic peer) =>
(peer as Map<String, dynamic>)['version'] == 10)
.where((dynamic peer) =>
(peer as Map<String, dynamic>)['status'] == 'UP')
.toList();
for (final dynamic peer in peers) {
if (peer['endpoints'] != null) {
final List<String> endpoints =
List<String>.from(peer['endpoints'] as List<dynamic>);
for (int j = 0; j < endpoints.length; j++) {
if (endpoints[j].startsWith('BMAS')) {
String endpoint = endpoints[j].replaceAll('BMAS ', '');
if (endpoint.contains(' ')) {
endpoint = endpoint.substring(0, endpoint.indexOf(' '));
}
endpoint = 'https://${endpoint.replaceAll(':443', '')}';
_nodes.add(endpoint);
final Duration latency = await _pingNode(endpoint);
if (_fastestNode == null || latency < _fastestLatency!) {
_fastestNode = endpoint;
_fastestLatency = latency;
if (!kReleaseMode) {
logger('Current faster node $_fastestNode');
}
}
}
}
}
}
_resetNodeErrors(null);
}
logger('Loaded ${_nodes.length} duniter nodes');
} catch (e) {
logger('Error: $e');
rethrow;
}
}
void _rotateNodes() {
_currentNodeIndex = (_currentNodeIndex + 1) % _nodes.length;
}
void _incrementNodeErrors(String nodeUrl) {
_nodeErrors[nodeUrl] = (_nodeErrors[nodeUrl] ?? 0) + 1;
}
void _resetNodeErrors(String? nodeUrl) {
if (nodeUrl == null) {
_nodeErrors = <String, int>{for (String v in _nodes) v[0]: 0};
} else {
_nodeErrors[nodeUrl] = 0;
}
}
void _startResetErrorsTimer() {
_resetErrorsTimer = Timer.periodic(const Duration(minutes: 5), (_) {
_resetNodeErrors(null);
});
}
void _cancelResetErrorsTimer() {
_resetErrorsTimer?.cancel();
}
bool _hasNodeExceededRetryLimit(String nodeUrl) {
return _nodeErrors[nodeUrl] != null && _nodeErrors[nodeUrl]! >= _retryCount;
}
Future<void> dispose() async {
_cancelResetErrorsTimer();
}
Future<Duration> _pingNode(String node) async {
try {
final Stopwatch stopwatch = Stopwatch()..start();
await http.get(Uri.parse('$node/network/peers/self/ping'));
stopwatch.stop();
return stopwatch.elapsed;
} catch (e) {
// Handle exception when node is unavailable etc
logger('Node $node does not respond to ping $e');
_incrementNodeErrors(node);
return const Duration(days: 20);
}
}
}
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_html/html.dart' as html;
/*
This is a work in progress to export/import keys with some password
*/
class ExportImportPage extends StatefulWidget {
const ExportImportPage({super.key});
@override
State<ExportImportPage> createState() => _ExportImportPageState();
}
class _ExportImportPageState extends State<ExportImportPage> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController _passwordController = TextEditingController();
String _statusMessage = '';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Exportar/Importar Preferencias Compartidas'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Contraseña de patrón',
),
obscureText: true,
validator: (String? value) {
if (value != null && value.isEmpty) {
return 'Por favor, introduce una contraseña de patrón';
}
return null;
},
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: _exportarPreferenciasCompartidas,
child: const Text('Exportar Preferencias Compartidas'),
),
const SizedBox(height: 16.0),
ElevatedButton(
onPressed: _importarPreferenciasCompartidas,
child: const Text('Importar Preferencias Compartidas'),
),
],
),
),
const SizedBox(height: 16.0),
Text(_statusMessage),
],
),
),
);
}
Future<void> _exportarPreferenciasCompartidas() async {
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
return;
}
final String password = _passwordController.text;
final SharedPreferences prefs = await SharedPreferences.getInstance();
final String jsonString = jsonEncode(prefs
.getKeys()
.fold<Map<String, dynamic>>(
<String, dynamic>{},
(Map<String, dynamic> map, String key) =>
<String, dynamic>{...map, key: prefs.get(key)}));
final Uint8List plainText = Uint8List.fromList(utf8.encode(jsonString));
final encrypt.Key key = encrypt.Key.fromUtf8(password.padRight(32));
final encrypt.IV iv = encrypt.IV.fromLength(16);
final encrypt.Encrypter encrypter = encrypt.Encrypter(encrypt.AES(key));
final encrypt.Encrypted encrypted =
encrypter.encryptBytes(plainText, iv: iv);
Directory? appDir = await getDownloadsDirectory();
appDir = appDir ?? await getApplicationDocumentsDirectory();
final File file = File('${appDir.path}/shared_preferences.bin.txt');
await file.writeAsBytes(encrypted.bytes);
setState(() {
_statusMessage = 'Preferencias Compartidas exportadas con éxito';
});
}
Future<void> _importarPreferenciasCompartidas() async {
final Completer<void> completer = Completer<void>();
// Crear un elemento <input type="file"> para seleccionar archivos
final html.InputElement input = html.InputElement()..type = 'file';
input.multiple = false;
input.accept = '.txt'; // Opcional: limitar a tipos de archivo específicos
input.click();
// Esperar hasta que se seleccionen los archivos
input.onChange.listen((html.Event event) async {
if (input.files != null && input.files!.isEmpty) {
completer.complete();
return;
}
final html.File file = input.files!.first;
final html.FileReader reader = html.FileReader();
// Leer el archivo seleccionado como texto
reader.readAsText(file);
await reader.onLoadEnd.first;
// Restablecer las preferencias compartidas desde el archivo cargado
try {
final String jsonString = reader.result as String;
final jsonMap = jsonDecode(jsonString);
final SharedPreferences prefs = await SharedPreferences.getInstance();
// TODO(vjrj): jsonMap.forEach((key, value) => prefs.set(key, value));
setState(() {
_statusMessage = 'Preferencias Compartidas importadas con éxito';
});
} catch (e) {
setState(() {
_statusMessage = 'Error al importar las Preferencias Compartidas';
});
}
completer.complete();
});
return completer.future;
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment