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

Target

Select target project
  • blockchain/nucleo
  • miguel/nucleo
  • Rcradiologia/nucleo
  • nelson/nucleo
4 results
Show changes
Commits on Source (208)
*/lastseen
*/status
cache
*/cache
network*/node
network*/bootnode
network*/contracts/*
*/*cache
bin/env
network/node
network/bootnode
network/*.pid
test2network*/node
test2network*/bootnode
test2network/*.pid
node_modules
bin/venv
src/*/*.bin
src/*/*.abi
__pycache__
LGPLv2-only
\ No newline at end of file
......@@ -3,6 +3,8 @@
## Sitio web: https://www.bfa.ar/
## Repositorio: https://gitlab.bfa.ar/blockchain/nucleo.git
Este repositorio contiene lo nececario para instalar un "nodo BFA" (nodo sellador, nodo gateway, nodo transaccional "etc", son casi iguales).
Esta guía debería funcionar en Debian o sus derivados. Testeado en *Debian* y *Ubuntu server* sin la GUI instalada en ninguno de ellos.
([Capturas de pantallas instalando un Ubuntu 18.04](https://gitlab.bfa.ar/blockchain/nucleo/wikis/Instalando-Ubuntu-Server-18.04))
......@@ -14,18 +16,23 @@ Esta guía debería funcionar en Debian o sus derivados. Testeado en *Debian* y
- `git clone https://gitlab.bfa.ar/blockchain/nucleo.git bfa`
3. Ejecutá el script de instalación. Esto cambiará algunas configuraciones en tu sistema. Si te preocupa (¿debería?), podés ejecutar este escript paso a paso manualmente.
- como root: `bfa/bin/installbfa.sh`
- te va a preguntar si queres conectarte a la red de `produccion` o de `prueba (test2)`
Van a aparecer varios **warnings** mientras se instala web3. Esto parece ser "normal". Ignorarlo no parece causar problemas.
4. Cambiá al usuario `bfa`
- como root: `su - bfa`
5. Comenzá la sincronización. **Esto puede llevar un rato largo** (este script se ejecuta automáticamente cuando se reinicia el sistema).
5. <del>Crea una cuenta</del> // solamente los 20-30 nodos selladores necesitan una cuenta en el nodo
- <del>como bfa: `admin.sh account`</del>
6. Comenzá la sincronización. **Esto puede llevar un rato largo** (este script se ejecuta automáticamente cuando se reinicia el sistema).
- como bfa: `start.sh`
6. Monitoreá los logs con `bfalog.sh`. Apretá CTRL-C en cualquier momento para detener el `tail -f`.
7. Cambiá la configuración de tu nodo usando `admin.sh syncmode`
7. `localstate.pl` muestra el estado actual del nodo.
8. Monitoreá los logs con `bfalog.sh`. Apretá CTRL-C en cualquier momento para detener el `tail -f`.
9. Cambiá la configuración de tu nodo usando `admin.sh syncmode`
- Hacé esto antes de haber sincronizado mucho en el paso anterior, ya que esto podría remover todos los datos de la cadena que hayas bajado y reiniciar la sincronización de la cadena.
8. Esperá a aque termine de sincronizar
9. Herramientas simples super básicas (más bien pruebas de concepto, para inspirar a los programadores):
10. Esperá a aque termine de sincronizar
11. Herramientas simples super básicas (más bien pruebas de concepto, para inspirar a los programadores):
- `explorer.sh` : Sigue el bloque más nuevo "*lastest*" por default, pero podés especificar un número de bloque cualquiera como argumento, por ejemplo `explorer.sh 0` permite ver el génesis (bloque 0).
- `walker.pl` : También toma un número de bloque para iniciar. Sigue esperando nuevos bloques.
- `walker.pl` : También toma un número de bloque para iniciar. Sigue esperando nuevos bloques.
- `sealerwatch.pl` : Mira cuando los selladores firman.
Hay otros programas "interesantes" en los directorios `bin/` y `src/`,
pero para los desarrolladores, el branch `dev` es más intersante y tambien el
......@@ -33,21 +40,34 @@ pero para los desarrolladores, el branch `dev` es más intersante y tambien el
**Puede tardarse alrededor de una hora conectarse la primera vez. En el log no se ve nada. Hay que tener paciencia.**
## start.sh
### network_id
Inicia un nodo para vos en la red BFA Ethereum.
Se puede consultar el ID de la red con el comando RPC `net.getVersion` o/y `net.version`.
## attach.sh
Red de producción (nombre: network):
47525974938 (0xb10c4d39a)
Te conecta a la línea de comandos (CLI) del `geth` que está corriendo en tu máquina local (falla si no hay un `geth` corriendo).
Red de pruebas (nombre: test2network):
55555000000 (0xcef5606c0)
## compile.and.deploy.contract
### chainId
Compila y despliega un *smart contract* a la blockchain. Debe haber una cuenta (*account*) local que tenga suficiente Ether para pagar por la transacción.
Normalmente chainId es el mismo numero que network_id, pero no es necesario. En BFA son distintos.
Se puede consultar el chainId con el comando RPC `eth.chainId`.
Argumento 1 es el nombre de archivo del *smart contract* a compilar.
Red de producción (network):
200941592 (0xbfa2018)
Ejemplo: `compile.and.deploy.contract src/TimestampAuthority.sol`
Red de pruebas (test2network):
99118822 (0x5e86ee6)
## start.sh
Inicia un nodo para vos en la red BFA Ethereum.
## attach.sh
Te conecta a la línea de comandos (CLI) del `geth` que está corriendo en tu máquina local (falla si no hay un `geth` corriendo).
## explorer.sh
......@@ -57,6 +77,10 @@ Scritp simple para mirar bloques
Muestra una línea por bloque que se va sellando en la red, luego espera hasta el siguiente bloque.
## sealerwatch.pl
Muestra cuando los selladores sellan bloques. Tiene amarillo y colorado para mostrar cuando algo no es optimo.
## rewind.sh
Si tu nodo local parece clavado y sigue conectado, podés probar esta
......@@ -81,12 +105,6 @@ Administra un *smart contract* de la Destilería ya desplegado. Va a mostrar
las cuentas registradas y la cantidad de Ether configurada ("*allowance*") de
cada una de ellas.
## sealeradd.sh / sealerrem.sh
Cuando un operador de un sellador vota para promover o remover un sellador,
este script hace la mayor parte. Toma un solo argumento, el número de la
cuenta del sellador por el cual se vota.
## unlock.js
Debloqua las cuentas del sistema (vease tambien `monitor.js`). Si una cuenta
......@@ -97,4 +115,16 @@ tiene clave, se puede poner la clave con este script.
Esto se corre desde `cron.sh`. Cada minuto actualizará el archivo
`network/status` que muestra información del estado del nodo muy básica.
Tambien habilita sellar/minar si la cuenta (eth.accounts[0]) esta permitido
segun la red. Desbloqua cuentas si no tienen passwords.
\ No newline at end of file
segun la red. Desbloqua cuentas si no tienen passwords.
## compile.and.deploy.contract
Compila y despliega un *smart contract* a la blockchain. Debe haber una cuenta (*account*) local que tenga suficiente Ether para pagar por la transacción.
Argumento 1 es el nombre de archivo del *smart contract* a compilar.
Ejemplo: `compile.and.deploy.contract src/TimestampAuthority.sol`
## localstate.pl
Muestra varias detalles del entorno local.
#!/usr/bin/node
"use strict"
const Libbfa = require( process.env.BFAHOME + '/bin/libbfa.js');
const rl = require('readline').createInterface(
{ input: process.stdin, output: process.stdout }
);
var web3;
var Distillery;
var bfa;
var notation = [ 15 ];
function init()
{
notation.push( Math.pow(10, notation[0]) );
switch ( notation[0] ) {
case 6:
notation.push( "Mwei" );
break;
case 9:
notation.push( "Gwei" );
break;
case 12:
notation.push( "micro" );
break;
case 15:
notation.push( "finney" );
break;
case 18:
notation.push( "ether" );
break;
case 21:
notation.push( "kether" );
break;
case 24:
notation.push( "grand" );
break;
case 27:
notation.push( "mether" );
break;
case 30:
notation.push( "gether" );
break;
case 33:
notation.push( "tether" );
break;
default:
notation = [ 15, Math.pow(10, 15), "finney" ];
}
bfa = new Libbfa();
web3 = bfa.newweb3();
Distillery = bfa.contract( web3, 'Distillery' );
web3.eth.getBalance( Distillery.contractaddress ).then(
function receivedOwnBalance(val) {
Distillery.contractbalance = val;
getlist();
},
function err(x){ bfa.fatal("Can't do that: "+x);process.exit(1); }
);
}
function palletSort(a,b)
{
if ( b == undefined )
{
if ( a == undefined )
return 0;
else
return -1;
}
if ( a == undefined )
return 1;
var strA = a[0].toLowerCase();
var strB = b[0].toLowerCase();
if ( strA < strB )
return -1;
if ( strA > strB )
return 1;
return 0;
}
function requestBalances( count )
{
var pallet = new Array;
var proms = new Array;
var i;
// Fetch addresses from the list in the contract.
for ( i=0; i<count; i++ )
{
proms.push(
Distillery.methods.atPosition(i).call( {} )
.then(
function(res) { pallet.push(res); },
function(err) { bfa.fatal("Fetching position data failed: "+err) }
)
);
}
Promise.all( proms ).then(
function(x) {
// The Well has now been filled out
// so we will ask for the balances of the found accounts
var p2 = new Array();
for ( i=0; i<count; i++ )
{
var cb = setBal.bind(null,pallet,i);
p2.push( web3.eth.getBalance( pallet[i][0] ).then(cb) );
}
Promise.all( p2 ).then(
function allbalances(a) { displayBalances(pallet) },
function(err) { bfa.fatal("Getting balances failed: "+err) }
);
},
function(err) { bfa.fatal("Getting account list failed: "+err) }
);
}
function setBal( arr, idx, val )
{
arr[idx][2]=val;
}
function editAccount( entry, pallet )
{
if ( entry == undefined )
return;
var acct;
var value;
// it is an existing account address?
if ( bfa.isAddr(entry) )
{
var i = 0;
var n = pallet.length;
while ( i < n )
{
if (String(pallet[i][0]).toLowerCase() == String(entry).toLowerCase() )
entry = i;
i++;
}
}
// it is a new account address?
if ( bfa.isAddr(entry) )
{
acct = entry;
value = 0;
}
else
if ( bfa.isNumeric(entry) && entry < pallet.length )
{
acct = pallet[entry][0];
value = pallet[entry][1];
}
else
if ( entry == "x" )
{
// trigger distribution
Distillery.methods.distribute().send( {"from": bfa.account, "gas": 4000000 } )
.then(
function distOK(x) {
console.log(
"Distribute returned succesfully in block# "
+ x.blockNumber
+ " using "
+ x.gasUsed
+ " gas."
);
getlist();
},
function distFail(x) {
bfa.fatal(
"Distribute returned errors in block# "
+ x.blockNumber
+ " using "
+ x.gasUsed
+ " gas."
);
console.log(x);
process.exit( 1 );
}
);
return;
}
else
if ( entry == "" )
{
// Do nothing, basically just update the display
return;
}
else
{
bfa.fatal("I don't know what to do with \""+entry+"\"." );
}
rl.question(
"Adjust the "
+ notation[2]
+ " fill value of "
+ acct
+ " (setting to 0 is the same as deleting)\n"
+ "Amount?: ",
(answer) => {
if ( bfa.isNumeric(answer) )
{
answer *= notation[1];
console.log("Sending update to the SC...");
Distillery.methods.setEtherAllowance(acct,answer)
.send( {"from": bfa.account } )
.then(
function(a){
console.log("Update accepted.")
getlist();
},
function(b){
bfa.fatal(
"\nMaybe you are not authorized:\n"
+b
+"\n\n\nI think you should leave now.\n"
);
}
)
}
else
bfa.fatal( "I have no idea what to do with \""+answer+"\"." );
rl.close;
}
);
}
function displayBalances( pallet )
{
var n = pallet.length;
var i;
pallet.sort(palletSort);
console.log(
"The contract's account ("
+ Distillery.contractaddress
+ ") has "
+ Math.floor(Distillery.contractbalance/notation[1]).toFixed(0)
+ " "
+ notation[2]
+ ".\n"
);
var longest = 1;
for ( i=0; i<n; i++ )
{
var len = (pallet[i][1]/notation[1]).toFixed(notation[0]).length;
if ( len > longest )
longest = len;
}
for ( i=0; i<n; i++ )
{
var entry = pallet[i];
if ( entry == undefined )
console.log( i+": <undef>" );
else
{
var numstr = (pallet[i][1]/notation[1]).toFixed(notation[0]);
while ( numstr.length < longest )
numstr = " "+numstr;
console.log(
i
+ ": "
+ pallet[i][0]
+ " fills to "
+ numstr
+ " "
+ notation[2]
+ " (has "
+ Number(pallet[i][2]/notation[1]).toFixed(notation[0])
+ ")."
);
}
}
console.log("\n[ Q=quit x=distribute ]");
rl.question("Which account to edit (enter index number of full account number)?: ",
(answer) => {
if ( answer != undefined )
{
if ( String(answer).toUpperCase() == 'Q' )
process.exit( 0 );
else
editAccount(answer, pallet);
}
rl.close;
}
)
}
function getlist()
{
Distillery.methods.numberOfBeneficiaries()
.call()
.then(
requestBalances,
function beneficiaryFail(x){bfa.fatal(x)}
);
}
init();
#!/usr/bin/python3
import sys
import os
if not os.environ.get('BFAHOME'):
print("$BFAHOME not set. Did you source bfa/bin/env ?", file=sys.stderr)
exit(1)
import json
import re
import decimal
import argparse
sys.path.append( os.path.join(os.environ['BFAHOME'],'bin' ))
import libbfa
bfa = None
notation = dict()
janitor = None
distillery = None
def distbalance() -> int:
return bfa.w3.eth.getBalance(distillery.address)
def distribute():
# trigger distribution
beforeBal = distbalance()
rcpt = janitor.transact( web3=bfa.w3, function=distillery.functions.distribute, extragas=4000000)
print('Distribute returned succesfully in block# {} using {} gas.'.format(rcpt.blockNumber, rcpt.gasUsed))
afterBal = distbalance()
print('Distributed {0:,.0f} Gwei.'.format((beforeBal - afterBal)/(10**9)))
def editAccount(entry:str, beneflist:list):
acct = None
# is it an account address?
if entry == '':
# Do nothing, basically just update the display
pass
elif re.search('^0x[0-9a-fA-F]{40}$', entry):
acct = entry.lower()
# is it a known account address?
elif re.search('^[0-9]+$', entry) and int(entry) < len(beneflist):
acct = beneflist[int(entry)].addr
elif entry == 'x':
distribute()
else:
print('I do not know what to do with "{}".'.format(entry), file=sys.stderr)
exit(1)
if acct is None:
return
answer = input('Adjust the {} fill value of {} (setting to 0 is the same as deleting)\nAmount?: '.format(notation['name'], acct))
if re.search('^[0-9\.]+$', answer) is None:
print('I have no idea what to do with "{}".'.format(answer), file=sys.stderr)
exit(1)
print('Sending update to the SC...')
weilimit = float(answer) * int(notation['num'])
rcpt = janitor.transact( bfa.w3.toChecksumAddress(acct), int(weilimit), web3=bfa.w3, function=distillery.functions.setEtherAllowance)
if rcpt.status:
print('Update accepted.')
else:
print('Update failed.')
def getBeneficiaries() -> list:
count = distillery.functions.numberOfBeneficiaries().call()
beneflist = list()
# Fetch addresses from the list in the contract.
for i in range(count):
print("Indexing accounts ({}/{})...\x1B[J\r".format(i,count), end='')
(addr,topuplimit) = distillery.functions.atPosition(i).call()
bal = bfa.w3.eth.getBalance( addr )
beneflist.append( { "addr": addr, "topuplimit": topuplimit, "balance": bal } )
print("\r\x1B[J".format(i,count), end='')
s = lambda x:x['addr'].lower()
beneflist.sort(key=s)
return beneflist
def print_beneficiary_list(beneflist:list):
# find the length of the longest number-string
longestlimit = 1
longestbalance = 1
numformat = notation['strformat'].format
for i in range(len(beneflist)):
entry = beneflist[i]
numstr = numformat(decimal.Decimal(entry['topuplimit']) / notation['num'])
thislen = len(numstr)
if thislen > longestlimit:
longestlimit = thislen
numstr = numformat(decimal.Decimal(entry['balance']) / notation['num'])
thislen = len(numstr)
if thislen > longestbalance:
longestbalance = thislen
# print them all
theformat = '{:' + str(len(str(len(beneflist)-1))) + \
'}: {} fills to {:' + \
str(longestlimit) + \
'.' + \
str(notation['potency']) + \
'f} {} (has {:' + \
str(longestbalance) + \
'.' + \
str(notation['potency']) + \
'f}).'
for i in range(len(beneflist)):
entry = beneflist[i]
numstr = numformat(decimal.Decimal(entry['topuplimit']) / notation['num'])
while len(numstr) < longestlimit:
numstr = ' ' + numstr
print(theformat.format(
i,
bfa.w3.toChecksumAddress(entry['addr']),
decimal.Decimal(entry['topuplimit'])/notation['num'],
notation['name'],
decimal.Decimal(entry['balance']) / notation['num']
))
def overview():
while True:
print( "The contract's account ({}) has {} {}.".format(
distillery.address,
int(decimal.Decimal(distbalance()) / notation['num']),
notation['name']
))
beneflist = getBeneficiaries()
print_beneficiary_list(beneflist)
answer = input("\n[ Q=quit x=distribute ]\n" +
"Which account to edit (enter index number or full account number)?: ")
if answer is None or answer.upper() == 'Q':
exit( 0 )
editAccount(answer, beneflist)
def init(**kwargs):
global janitor, notation, distillery, bfa;
bfa = libbfa.Bfa(kwargs.get('uri'))
janitor = libbfa.Account(kwargs.get('sender_addr'))
table = ('Kwei', 'Mwei', 'Gwei', 'micro', 'finney', 'ether',
'kether', 'grand', 'mether', 'gether', 'tether')
potency = 18
notation['potency'] = potency
notation['name'] = table[int(potency/3-1)]
notation['num'] = pow(10, potency)
notation['strformat'] = '{' + ':.{}f'.format(potency) + '}'
abifile = ''
if os.getenv('BFAHOME'):
abifile = os.path.join(os.path.join(os.environ['BFAHOME'], 'network'))
if os.getenv('BFANETWORKDIR'):
abifile = os.path.join(os.environ['BFANETWORKDIR'])
abifile = os.path.join(abifile, 'contracts', 'Distillery', 'abi')
with open(abifile, 'rt', encoding='utf-8') as infile:
abitxt = infile.read()
abi = json.loads(abitxt)
distillery = bfa.w3.eth.contract(address=kwargs.get('sc_addr'), abi=abi)
parser = argparse.ArgumentParser(description="Command interface for BFA2018 distillery1.")
parser.add_argument(
'--uri',
metavar='URI',
nargs=1,
default='prod',
help='URI of node to connect to.')
parser.add_argument(
'--sender-addr',
metavar='ADDR',
nargs=1,
default='0xd15dd6dbbe722b451156a013c01b29d91e23c3d6',
help='Address of controller of the smart contract.')
parser.add_argument(
'--sc-addr',
metavar='ADDR',
nargs=1,
default='0xECB6aFF6e38dC58C4d9AaE2F7927A282bcB77AC2',
help='Address of smart contract.')
parser.add_argument(
'--distribute',
action='store_true',
help='Run distribute once and then exit.')
parsed = parser.parse_args()
init(uri=parsed.uri, sc_addr=parsed.sc_addr, sender_addr=parsed.sender_addr)
if parsed.distribute:
distribute()
else:
overview()
......@@ -116,6 +116,28 @@ function admin_bootnode
fi
}
function create_account
{
num=$( ls -1 ${BFANODEDIR}/keystore/*--* 2>/dev/null | wc -l )
if [ $num -gt 0 ]
then
if [ $num -eq 1 ]
then
plural=""
else
plural="s"
fi
yesno n "You already have ${num} account${plural}. Do you wish to create an extra?"
unset plural num
if [ "$REPLY" = "n" ]
then
return
fi
fi
unset num
geth --cache 0 --datadir ${BFANODEDIR} --password /dev/null account new
}
case "$1" in
bootnode)
admin_bootnode
......@@ -123,8 +145,11 @@ case "$1" in
syncmode)
admin_syncmode
;;
account)
create_account
;;
*)
echo Usage: `basename $0` "{bootnode|syncmode}"
echo Usage: `basename $0` "{bootnode|syncmode|account}"
trap '' ERR
exit 1
esac
#!/bin/bash
# Robert Martin-Legene <robert@nic.ar>
if [ -z "${BFAHOME}" ]; then echo "\$BFAHOME not set. Did you source bfa/bin/env ?" >&2; exit 1; fi
# shellcheck disable=SC1090
source "${BFAHOME}/bin/libbfa.sh" || exit 1
declare -A commands help
TOMLCONFIGFILE="${BFANETWORKDIR}/config.full+archive+nohttp"
function register_subcommand
{
declare -g commands help
commands[$1]=$2
help[$1]=$3
}
function _usage()
{
local after c
after=''
for c in $(echo "${!commands[@]}" | sort)
do
after="${after}|$c"
done
echo "Usage: $(basename "$0") {${after:1}}" >&2
for c in $(echo "${!commands[@]}" | sort)
do
printf '%-15s %s\n' "$c" "${help[$c]}" >&2
done
trap - ERR
exit 1
}
function _max
{
test "$1" -lt "$2" && echo "$2"
echo "$1"
}
function pidsfromsigfiles
{
local pids pid file
pids=
for file in \
"${BFANETWORKDIR}/bootnode.pid" \
"${BFANETWORKDIR}/start-bootnode-loop.pid"\
"${BFANODEDIR}/monitor.pid" \
"${BFANODEDIR}/start-monitor-loop.pid" \
"${BFANODEDIR}/geth.pid" \
"${BFANODEDIR}/start-geth-loop.pid"
do
test -r "$file" || continue
pid=$(< "$file")
if ! [[ "$pid" =~ ^[0-9]+$ ]]
then
rm -f "$file"
continue
fi
if ! kill -0 "$pid" >/dev/null 2>/dev/null
then
rm -f "$file"
continue
fi
pids="$pids $pid"
done
echo "${pids# }"
}
register_subcommand 'ps' 'psprocs' 'Show processes relating to known .pid files'
function psprocs
{
pids=$(pidsfromsigfiles)
if [ -z "$pids" ]
then
echo "There are no processes of interest." >&2
return 1
fi
# shellcheck disable=SC2086
ps -p ${pids// /,}
}
register_subcommand 'top' 'topprocs' 'Show relevant processes in top.'
function topprocs
{
pids=$(pidsfromsigfiles)
if [ -z "$pids" ]
then
echo "There are no processes of interest." >&2
return 1
fi
# shellcheck disable=SC2086
local args pid
args=
# shellcheck disable=SC2086
for pid in $pids
do
args="$args -p $pid"
done
# shellcheck disable=SC2086
exec top $args
}
function sendsig
{
local signal pids
signal=$1
shift
pids="$*"
if [ -z "$pids" ]
then
return
fi
# shellcheck disable=SC2086
ps -p ${pids// /,}
echo "Sending ${signal} signal to pid $pids."
# shellcheck disable=SC2086
kill "$signal" $pids || true
}
register_subcommand 'kill' 'killbfastuff' 'Hard kill BFA background processes (BAD IDEA! Use stop instead).'
function killbfastuff
{
local pids
pids=$(pidsfromsigfiles)
if [ -z "$pids" ]
then
echo "Nothing to send signals to." >&2
return 2
fi
sendsig -KILL "$pids"
}
register_subcommand 'stop' 'graceful' 'Ask the BFA background processes to end gracefully.'
function graceful
{
local max pids
max=30
pids=$(pidsfromsigfiles)
if [ -z "$pids" ]
then
echo "Nothing to send signals to." >&2
return 1
fi
sendsig -TERM "$pids"
sleep 1
while :
do
pids=$(pidsfromsigfiles)
max=$((max - 1))
test "$max" -gt 0 || break
test -n "$pids" || break
printf '\rThese are still alive: %s\x1b[J' "$pids"
sleep 0.5
done
printf '\r\x1b[J'
if [ -n "$pids" ]
then
printf 'This/these pids is/are still running: %s\n' "$pids"
fi
}
register_subcommand 'initdb' 'initdb' 'Stop geth and reset the node to block zero (genesis).'
function initdb
{
killbfastuff || true
yes | geth --cache 0 --datadir "${BFANODEDIR}" removedb
geth --networkid "${BFANETWORKID}" --cache 0 --datadir "${BFANODEDIR}" init "${BFANETWORKDIR}/genesis.json"
}
register_subcommand 'exportdb' 'exportdb' 'Export blockchain in chunks of 1 million blocks per file.'
function exportdb
{
local million maxblocks blockstart toblock filename
million=1000000
graceful || true
maxblocks=$(bfageth --exec 'eth.blockNumber' console 2> /dev/null)
# 0 is genesis.. shouldn't dump that
blockstart=1
while [ "$blockstart" -lt "$maxblocks" ]
do
toblock=$(( blockstart + million - 1 ))
test "$toblock" -gt "$maxblocks" &&
toblock=$maxblocks
printf -v filename 'bfa2018.blocks.%09d-%09d.export.gz' "$blockstart" "$toblock"
if [ ! -e "$filename" ]
then
echo "Dumping blocks# $blockstart to $toblock to the file named $filename"
bfageth export "$filename" "$blockstart" "$toblock"
fi
blockstart=$(( toblock + 1 ))
done
}
register_subcommand 'importdb' 'importdb' 'Import blocks safely from previous block exports.'
function importdb
{
local dumpurl million blockstart continue_at haveblocks theirsize oursize
dumpurl="https://s3.wasabisys.com/bfa/blockdumps"
million=1000000
graceful || true
haveblocks=$(bfageth --exec 'eth.blockNumber' console 2> /dev/null)
blockstart=$((haveblocks/million*million+1))
while :
do
printf -v filename 'bfa2018.blocks.%09d-%09d.export.gz' "$blockstart" "$((blockstart + million - 1))"
blockstart=$((blockstart + million))
theirsize=$(curl --head "${dumpurl}/${filename}" 2>/dev/null | grep -i '^Content-Length: ' | head -1)
if [ -z "$theirsize" ]
then
break
fi
theirsize="${theirsize//*: }"
# remove trailing newline
theirsize="${theirsize%$'\r'}"
#
unset oursize continue_at
if [ -r "$filename" ]
then
oursize=$(stat -c %s "$filename")
continue_at="--continue-at $oursize"
fi
if [ "${oursize:-0}" -lt "$theirsize" ]
then
echo "Downloading $filename"
curl --fail $continue_at -o "$filename" "${dumpurl}/${filename}" || break
fi
# shellcheck disable=SC2086
bfageth import <(gzip -dc "$filename")
rm -f "$filename"
done
}
register_subcommand 'bootnode' 'admin_bootnode' 'Enable/disable the local bootnode.'
function admin_bootnode
{
keyfile="${BFANETWORKDIR}/bootnode/key"
echo "Only very few wants to actually run a boot node."
echo "If you have a keyfile for a bootnode, then you will"
echo "automatically start one, when restarting your system."
if [ -f "$keyfile" ]
then
echo "You are set up to run a boot node."
echo "Deleting your bootnode keyfile disables your bootnode."
yesno n "Do you want to delete your bootnode keyfile?"
if [ "$REPLY" = "y" ]
then
rm "$keyfile"
fi
pidfile="${BFANETWORKDIR}/bootnode/pid"
if [ -r "$pidfile" ]
then
pid=$(< "$pidfile")
if kill -0 "$pid"
then
echo "Terminating your bootnode."
kill "$(< "$pidfile")" || true
fi
fi
else
echo "You are not set up to run a boot node."
yesno n "Do you want to create a keyfile for a bootnode?"
if [ "$REPLY" = "y" ]
then
bootnode -genkey "$keyfile"
fi
echo "You can now start your bootnode by running start.sh"
fi
}
register_subcommand 'account' 'bfaaccount' 'Account manipulation.'
function bfaaccount
{
case "$1" in
'create')
exec create_account
;;
*)
echo "Usage: $0 account create" >&2
echo ' create' 'Create an extra account locally on the node.' >&2
exit 1
;;
esac
}
function create_account
{
local num filename plural
num=0
for filename in "${BFANODEDIR}"/keystore/*
do
test -f "$filename" &&
num=$(( num + 1 ))
done
if [ "$num" -gt 0 ]
then
plural=""
if [ "$num" -ne 1 ]
then
plural="s"
fi
yesno n "You already have ${num} account${plural}. Do you wish to create an extra?"
if [ "$REPLY" = "n" ]
then
return
fi
fi
geth --cache 0 --datadir "${BFANODEDIR}" --password /dev/null account new
}
register_subcommand 'truncatelog' 'truncatelog' \
'Truncate the log file. You may want to stop the background processes first.'
function truncatelog
{
true > "${BFANODEDIR}/log"
}
register_subcommand 'geth' 'bfageth' 'Start geth for BFA.'
function bfageth
{
if [ -z "$*" ]
then
echo "You should not run this command without any arguments. Start the background processes with start.sh instead." >&2
exit 1
fi
local DATACFG
# Strange how it seems to ignore this variable from the config file
DATACFG=$(sed --regexp-extended -ne '/^\[Node\]$/,/^$/{/^DataDir *=/{s/^.*= *"(..*)"$/--datadir \1/;p}}' "${TOMLCONFIGFILE}")
geth --config "${TOMLCONFIGFILE}" $DATACFG "$@"
}
function bfaadmin
{
case "$1" in
'bootnode')
admin_bootnode
;;
'account')
create_account
;;
*)
echo "Usage: $(basename "$0") {bootnode|account}"
trap '' ERR
exit 1
esac
}
register_subcommand 'tail' 'bfatail' 'tail -f on the logfile.'
function bfatail
{
exec tail -n 100 -F "${BFANODEDIR}/log"
exit 1
}
register_subcommand 'log' 'bfalog' 'Open the logfile with less(1).'
function bfalog
{
exec less "${BFANODEDIR}/log"
exit 1
}
function main
{
case "$(basename "$0")" in
'bfa')
local cmd
cmd=$1
shift || _usage
test -n "$cmd" || _usage
test -n "${commands[$cmd]}" || _usage
# shellcheck disable=SC2086
eval ${commands[$cmd]} "$@"
;;
'admin.sh')
bfaadmin "$@"
;;
'bfalog.sh')
bfatail
;;
'bfageth')
bfageth "$@"
exit 1
esac
}
main "$@"
#!/bin/bash
if [ -z "${BFAHOME}" ]; then echo "\$BFAHOME not set. Did you source bfa/bin/env ?" >&2; exit 1; fi
source ${BFAHOME}/bin/libbfa.sh || exit 1
exec geth --datadir ${BFANODEDIR} --networkid ${BFANETWORKID} "$@"
#!/bin/bash
# Robert Martin-Legene <robert@nic.ar>
if [ -z "${BFAHOME}" ]; then echo "\$BFAHOME not set. Did you source bfa/bin/env ?" >&2; exit 1; fi
source ${BFAHOME}/bin/libbfa.sh || exit 1
exec tail -n 100 -F ${BFANODEDIR}/log
bfa
\ No newline at end of file
#!/bin/bash
# Robert Martin-Legene <robert@nic.ar>
# 2019
# You can start this as root or not - as long as $BFAHOME is set, it
# should work.
trap "exit 1" ERR
set -o errtrace
if [ -z "${BFAHOME}" ]; then echo "\$BFAHOME not set. Did you source bfa/bin/env ?" >&2; exit 1; fi
source ${BFAHOME}/bin/libbfa.sh || exit 1
function runasownerof
{
path=$1
precmd=
shift 1
pushd $path > /dev/null
if [ $( stat --format=%u $path ) -ne $UID ]
then
precmd="sudo -u $( stat --format=%U $path )"
fi
unset path
${precmd} "$@"
rv=$?
popd > /dev/null
unset precmd
return $rv
}
function aptinstall
{
for pkg in $*
do
dpkg --verify $pkg 2>/dev/null ||
(
runasownerof / apt -y install $pkg
)
done
}
set -x
cd ${BFAHOME}
git pull
npm rebuild
if [ "$1" = "" ]
then
# Pulling may update this script itself.
# We pull an updated repository, including an updated version of
# ourself, and then we execute the updates "us"
# This first part of the if is static. Changes (updates to this
# script) goes after the 'else'
#
# To keep things neat, make sure we pull as the user owning the
# directory.
chown -R bfa:bfa ${BFAHOME}
runasownerof ${BFAHOME} git pull
exec $0 wealreadypulled
else
# make sure bfa is in group sudo
id bfa | grep -q sudo || runasownerof / adduser bfa sudo
aptinstall libclass-accessor-perl
# rebuild installed modules
runasownerof ${BFAHOME} npm rebuild
# install new ones (if any)
runasownerof ${BFAHOME} npm install
# delete stale pid files
runasownerof ${BFAHOME} find ${BFAHOME} -name '*.pid' -delete
set +x
echo "*** Now would be a good time to restart the server."
fi
......@@ -21,7 +21,7 @@ function create
cleanup "$js"
cat > $js <<EOT
var mycontract = eth.contract($abi)
var transaction = mycontract.new( { from: eth.accounts[0], data: "0x${bin}", gas: 1000000 } )
var transaction = mycontract.new( { from: eth.accounts[0], data: "0x${bin}", gas: 3000000, gasPrice: 1000000000 } )
var rcpt
while ( !rcpt )
{
......
#!/bin/bash
trap 'exit 1' ERR
# Log in home directory
exec < /dev/null > ${HOME}/bfa-cron-output.log 2>&1
# Go to script bin, 'cause we expect to find $BFAHOME/bin/env there
cd `dirname $0`
test -e "env" || cp -p ../network/env env
source ./env
./start.sh
./start.sh < /dev/null >&0 2>&0
#!/usr/bin/node
// 20200828 Robert Martin-Legene
// License: GPLv2-only
// (c) Secretaria Legal y Tecnica, Presidencia De La Nacion, Argentina
// Looks for the contract you're specifying as argument.
// Tries to show you all the events that contract has ever logged on the blockchain.
// The contract's ABI must be in a text file in ${BFANETWORKDIR}/contracts/${contractaddr}/abi
const Web3 = require( "web3" );
const fs = require( "fs" );
const web3 = new Web3( "http://127.0.0.1:8545" );
function writeEvent(ev)
{
console.log("");
console.log( "Block number: " + ev.blockNumber );
console.log( "TX hash: " + ev.transactionHash );
console.log( "Event name: " + ev.event );
Object.keys( ev.returnValues ).forEach(
function writeEventValue( value )
{
// Will skip keys which are made entirely of digits.
if ( ! value.match(/^[0-9]+$/) )
{
console.log( "* " + value + ": " + ev.returnValues[value] )
}
}
);
}
function gotPastEvents( e, result )
{
// We are called a single time.
// Either success or failure.
if ( e )
{
console.error( e );
process.exit( 1 );
}
result.forEach( writeEvent );
console.log( "\n" + result.length + " events." );
}
var contractname = process.argv[2];
if ( contractname === undefined )
{
console.error( "Usage: " + process.argv[1] + " <contractname|contractaddr>" );
process.exit( 1 );
}
if ( process.env.BFANETWORKDIR === undefined )
{
console.error( "$BFANETWORKDIR must be defined" );
process.exit( 1 );
}
var pathprefix = process.env.BFANETWORKDIR + "/contracts/";
var filename;
if ( fs.existsSync( contractname ))
filename = contractname;
else
if ( fs.existsSync( contractname.toLowerCase() ))
filename = contractname.toLowerCase();
else
if ( fs.existsSync( pathprefix + contractname ))
filename = pathprefix + contractname;
else
if ( fs.existsSync( pathprefix + contractname.toLowerCase() ))
filename = pathprefix + contractname.toLowerCase();
if ( filename === undefined )
{
console.error( "Contract not found." );
process.exit( 1 );
}
var contractaddr = filename;
var dirname = "";
var idx = filename.lastIndexOf("/");
if ( idx > -1 )
{
dirname = filename.substr( 0, idx+1 );
filename = filename.substr( idx+1 )
contractaddr = filename;
}
var stats = fs.lstatSync( dirname + filename );
if ( stats.isSymbolicLink() )
{
let linkname = fs.readlinkSync( dirname + filename, { encoding: 'utf8' } );
filename = dirname + linkname;
contractaddr = linkname;
}
filename += "/abi";
var abi_str = fs.readFileSync( filename, { encoding: 'utf8' } );
var abi = JSON.parse( abi_str );
var contract = new web3.eth.Contract( abi, contractaddr );
console.log( "Address: " + contractaddr );
contract.getPastEvents( "allEvents", { fromBlock: "earliest", toBlock: "latest" }, gotPastEvents);
......@@ -6,7 +6,7 @@ NODEJSPGP=0x68576280
# for building, which we hope has more space.
NEW=/home/root/new
trap 'exit 1' ERR
trap 'echo "The installation failed." >&2 ; exit 1' ERR
set -o errtrace
# Be verbose
set -e
......@@ -25,6 +25,22 @@ function info
echo '***'
}
# Runs as the owner of the given directory, and in the given directory
function runasownerof
{
local where=$1 precmd=
shift 1
pushd $where > /dev/null
if [ $( stat --format=%u . ) -ne $UID ]
then
precmd="sudo --preserve-env --shell --set-home --user=$( stat --format=%U . ) PATH=${PATH}"
fi
${precmd} "$@"
local rv=$?
popd > /dev/null
return $rv
}
# For getting a recent nodejs
function nodejsinstall
{
......@@ -32,98 +48,59 @@ function nodejsinstall
# Nodejs software repository PGP key
if [ `apt-key export ${NODEJSPGP} 2>&1 | wc -l` -le 50 ]
then
info Adding nodejs software repository PGP key
info "Adding nodejs software repository PGP key"
apt-key adv --keyserver keyserver.ubuntu.com --recv ${NODEJSPGP}
fi
local file=/etc/apt/sources.list.d/nodesource.list
if [ ! -r "$file" ]
then
info Adding nodejs repository to apt sources.
info "Adding nodejs repository to apt sources."
echo "deb https://deb.nodesource.com/node_10.x $(lsb_release -sc) main" > $file
info And now updating the software package list.
info "And now updating the software package list."
apt update
fi
# nodejs also provides npm
aptinstall nodejs
info "Installing nodejs modules (will show many warnings)"
runasownerof ${BFAHOME} npm install
runasownerof ${BFAHOME} npm audit fix
}
function web3install
{
(
cd ~bfa
test -r package.json ||
info Initialising nodejs. &&
sudo --set-home -u bfa npm init -y
)
# nodejs package(s) that we need.
echo 'require("web3")' | sudo --set-home -u bfa nodejs 2>/dev/null && return
info Installing nodejs module: web3 "(will show many warnings)"
sudo --set-home -u bfa npm install web3
}
function golanginstall
function gethinstall
{
if [ ! -d /usr/local/go ]
install --verbose --owner=bfa --group=bfa --directory ${NEW}
if [ -d ${NEW}/go-ethereum ]
then
info Downloading package of go binaries.
mkdir -p ${NEW}
cd ${NEW}
local arch=
case "$(uname -m)" in
x86_64)
arch=amd64
;;
*)
echo Do not know how to help you. >&2
exit 1
;;
esac
#Download go*.linux-${ARCH}.tar.gz from https://golang.org/dl/
golangurl=$(
curl -f https://golang.org/dl/ 2>/dev/null |
grep linux-${arch}.tar.gz |
grep downloadBox |
sed 's/.*href="//;s/".*//' |
head -1
)
name=$( basename $golangurl )
if [ -r "$name" ]
then
# If we have the download, check if it is corrupt
tar -ztf "$name" >/dev/null 2>&1 ||
rm -f "$name"
fi
if [ ! -r "$name" ]
then
curl -f -O $golangurl ||
rm -f "$name"
fi
# Integrity checking the archive
tar -xzf "$name"
info Unpacking $name into /usr/local
tar -C /usr/local -xzf go*.tar.gz
info "Running git pull to ensure that the local go-ethereum repo is up-to-date."
runasownerof ${NEW}/go-ethereum git checkout master
runasownerof ${NEW}/go-ethereum git pull
else
info "Download geth source code."
runasownerof ${NEW} git clone https://github.com/ethereum/go-ethereum
fi
PATH=${PATH}:/usr/local/go/bin
runasownerof ${NEW}/go-ethereum git checkout ${geth_tag}
chown -R bfa ${NEW}/go-ethereum
info "Compiling geth tagged as ${geth_tag}"
runasownerof ${NEW}/go-ethereum make all
HISBINDIR=$( echo ~bfa/bin )
install --verbose --owner=bfa --group=bfa --directory ${HISBINDIR}
install --verbose --owner=bfa --group=bfa --target-directory=${HISBINDIR} ${NEW}/go-ethereum/build/bin/{geth,bootnode,abigen,ethkey,puppeth,rlpdump,wnode,swarm,swarm-smoke}
}
function gethinstall
function initgenesis
{
mkdir -p ${NEW}
cd ${NEW}
info Download geth source code.
test -d go-ethereum ||
git clone https://github.com/ethereum/go-ethereum
cd ${NEW}/go-ethereum
info Running git pull to ensure that the local go-ethereum repo is up-to-date.
git pull
#
cd ${NEW}/go-ethereum
info Compiling geth
make all
mkdir -p ~bfa/bin
cp -vp ${NEW}/go-ethereum/build/bin/{geth,bootnode,abigen,ethkey,puppeth,rlpdump,wnode,swarm,swarm-smoke} ~bfa/bin/
chown -R bfa:bfa ~bfa
(
HOME=$( echo ~bfa )
source ${BFAHOME}/bin/env
BFANETWORKDIR=${BFANETWORKDIR:-${BFAHOME}/network}
BFANODEDIR=${BFANODEDIR:-${BFANETWORKDIR}/node}
if [ ! -d "${BFANODEDIR}" -o ! -d "${BFANODEDIR}/geth/chaindata" ]
then
info "Node is not initialised. Initialising with genesis."
runasownerof "${BFAHOME}" geth --networkid ${BFANETWORKID} --cache 0 --datadir "${BFANODEDIR}" init "${BFANETWORKDIR}/genesis.json"
chown -R bfa:bfa ~bfa
fi
)
}
function aptinstall
......@@ -133,7 +110,7 @@ function aptinstall
# consider apt install --install-suggests if you are masochist
dpkg --verify $pkg 2>/dev/null ||
(
info Installing $pkg
info "Installing $pkg"
apt -y install $pkg
)
done
......@@ -143,34 +120,46 @@ function usersetup
{
if ! id bfa >/dev/null 2>&1
then
info Adding required user \"bfa\"
info "Adding required user \"bfa\""
adduser --disabled-password --gecos 'Blockchain Federal Argentina' bfa
info "Adding user \"bfa\" to group \"sudo\""
adduser bfa sudo
fi
cd ~bfa
# cloning if not done already
if [ ! -d ~bfa/bfa ]
# If we're running inside a docker, this may already exist but
# probably owned by root. Let's make sure things are proper.
chown -R bfa:bfa ~bfa
#
}
function userconfig
{
if [ $( expand < ~bfa/.bashrc | grep -E "source ${BFAHOME}/bin/env" | wc -l ) -eq 0 ]
then
git clone https://gitlab.bfa.ar/blockchain/nucleo.git bfa
info "Adding to automatically source ${BFAHOME}/bin/env via .bashrc"
echo "test -r ${BFAHOME}/bin/env && source ${BFAHOME}/bin/env" >> ~bfa/.bashrc
fi
# updating
cd ~bfa/bfa
git pull
#
cd ~bfa
if [ $( expand < .bashrc | grep -E 'source .*bfa/bin/env' | wc -l ) -eq 0 ]
# cloning if not done already, or just update (pull)
if [ ! -d "${BFAHOME}" ]
then
info Adding to automatically source \~bfa/bfa/bin/env via .bashrc
echo 'source ~/bfa/bin/env' >> .bashrc
# initial cloning
runasownerof ${BFAHOME%/*} git clone https://gitlab.bfa.ar/blockchain/nucleo.git $BFAHOME
else
runasownerof "${BFAHOME}" git pull
fi
mkdir -p ~bfa/bfa/network
if [ ! -e "${BFAHOME}/bin/env" ]
then
cp -p ${BFAHOME}/$envfile ${BFAHOME}/bin/env
fi
PATH=${PATH}:${BFAHOME}/bin
source ${BFAHOME}/lib/versions
}
function cronit
{
if [ $( ( crontab -u bfa -l 2>/dev/null || true ) | grep -E 'bfa/bin/cron.sh$' | wc -l ) -eq 0 ]
if [ $( ( crontab -u bfa -l 2>/dev/null || true ) | grep -E "${BFAHOME#~bfa/}/bin/cron.sh" | wc -l ) -eq 0 ]
then
info Install crontab to start automatically upon reboot
(( crontab -u bfa -l 2>/dev/null || true ) ; echo '@reboot bfa/bin/cron.sh' ) | crontab -u bfa -
info "Install crontab to start automatically upon reboot"
(( crontab -u bfa -l 2>/dev/null || true ) ; echo "@reboot ${BFAHOME#~bfa/}/bin/cron.sh" ) | crontab -u bfa -
fi
}
......@@ -179,17 +168,51 @@ function welcome
info "(re)log in as user bfa"
}
function setupquestions
{
if [ -t 0 ]
then
read -p "Donde quiere instalar (sera BFAHOME) [$( echo ~bfa/bfa )]? : " -t 300 BFAHOME
fi
if [ "$BFAHOME" = "" ]
then
BFAHOME=$( echo ~bfa/bfa )
fi
# Default to production
envfile=network/env
if [ ! -e "${BFAHOME}/bin/env" ]
then
REPLY=
if [ -t 0 ]
then
while [ "$REPLY" != "1" -a "$REPLY" != "2" ]
do
echo "Quiere conectarse a la red BFA de produccion o prueba?"
echo "1. Produccion"
echo "2. Prueba (test2)"
read -p "Red: " -t 60 -n 1
echo
done
fi
if [ "$REPLY" = "2" ]
then
envfile=test2network/env
fi
fi
}
usersetup
setupquestions
# Ubuntu necesita mas repos
grep -q Ubuntu /etc/issue && apt-add-repository multiverse
#
apt update
# development tools
aptinstall dirmngr apt-transport-https curl git curl build-essential sudo
aptinstall jq libjson-perl libwww-perl
usersetup
aptinstall dirmngr apt-transport-https curl git curl build-essential sudo software-properties-common golang
aptinstall jq libjson-perl libwww-perl libclass-accessor-perl
userconfig
nodejsinstall
web3install
golanginstall
gethinstall
initgenesis
cronit
welcome
......@@ -2,6 +2,9 @@
"use strict"
var request = require('request');
var net = require('net');
module.exports = class Libbfa
{
constructor() {
......@@ -35,7 +38,7 @@ module.exports = class Libbfa
files.push( filename );
});
}
// found none?
// found any?
if ( files.length > 0 )
{
files.sort();
......@@ -43,9 +46,24 @@ module.exports = class Libbfa
}
}
//
this.netport = Number.parseInt( this.setfromfile( this.nodedir+'/netport', 30303 ));
this.rpcport = Number.parseInt( this.setfromfile( this.nodedir+'/rpcport', 8545 ));
this.account = process.env.BFAACCOUNT;
this.netport = 30303;
this.rpcport = 8545;
this.sockettype = 'ipc';
this.socketurl = 'http://127.0.0.1:' + this.rpcport; // old
this.socketurl = this.nodedir+'/geth.ipc'; // overwrite with newer ipc method
if ( this.sockettype == 'ipc' ) {
this.provider = new this.Web3.providers.IpcProvider( this.nodedir+'/geth.ipc', net );
this.req_url = 'http://unix:' + this.nodedir + '/geth.ipc:/';
} else if ( this.sockettype == 'ws' ) {
this.provider = new this.Web3.providers.WebsocketProvider( this.socketurl );
this.req_url = this.socketurl;
} else if ( this.sockettype == 'http') {
this.provider = new this.Web3.providers.HttpProvider( this.socketurl );
this.req_url = this.socketurl;
} else {
fatal("Unknown sockettype.");
}
}
contract(w3, name)
......@@ -70,57 +88,120 @@ module.exports = class Libbfa
fatal( txt )
{
console.log( txt );
console.error( txt );
process.exit( 1 );
}
newweb3()
{
var provider = 'http://127.0.0.1:' + this.rpcport;
var w3 = new this.Web3( provider );
w3.eth.extend({
//property: 'bfaclique',
methods: [{
name: 'bfaGetSigners',
call: 'clique_getSigners',
params: 0
}]
});
w3.eth.extend({
methods: [{
name: 'bfaMinerstart',
call: 'miner_start',
params: 0
}]
});
w3.eth.extend({
methods: [{
name: 'bfaMinerstop',
call: 'miner_stop',
params: 0
}]
});
w3.eth.extend({
methods: [{
name: 'bfaAdminpeers',
call: 'admin_peers',
params: 0
}]
});
w3.eth.extend({
methods: [{
name: 'bfaAdminaddPeer',
call: 'admin_addPeer',
params: 1
}]
});
w3.eth.personal.extend({
methods: [{
name: 'bfalistWallets',
call: 'personal_listWallets',
params: 0
}]
});
var w3 = new this.Web3( this.provider );
var req_url = this.req_url;
var _bfa = this;
// This could just remain the same number all the time.
var unneededcounter = 1;
w3.jsonify = function( opname, params )
{
var obj = {};
obj.id = unneededcounter++;
obj.jsonrpc = "2.0";
obj.method = opname;
obj.params = params;
return obj;
};
w3.rpcreq = function( opname, params, callback )
{
request.post({
uri: req_url,
json: true,
body: w3.jsonify( opname, params ),
callback: function RPCresponse( err, obj )
{
var r;
var e;
if ( err )
e = err;
else
if ( obj.body.error && obj.body.error.code && obj.body.error.message )
e = 'Error ' + obj.body.error.code + ": "+ obj.body.error.message;
else
r = obj.body.result;
callback(e, r);
}
});
};
w3.req = function( opname, params, callback )
{
if ( _bfa.sockettype == 'ipc' )
{
w3.ipcreq( opname, params, callback );
}
else
{
w3.rpcreq( opname, params, callback );
}
}
w3.ipcreq = function( opname, params, callback )
{
var socket = net.connect( _bfa.socketurl );
var result;
var err;
socket.on("ready", () => {
// When the socket has been established.
// We create a new connection per request, because it
// is easier than reliably handling JSON object boundaries
// in a TCP stream .
// Writes out data and closes our end of the connection.
// Geth will reply and then close it's end.
socket.end( JSON.stringify( w3.jsonify(opname,params).valueOf()));
});
socket.on("data", (d) => {
try {
result = JSON.parse( d.toString() );
}
catch {
err = d.toString();
}
});
socket.on("timeout", () => {
socket.close();
});
socket.on("error", (e) => {
console.error(e);
err = e;
});
socket.on("close", () => {
if ( result === undefined )
err = 'Error undefined (close)'
else
if ( result.error && result.error.code && result.error.message )
err = 'Error ' + result.error.code + ": "+ result.error.message;
else
result = result.result;
callback( err, result );
});
}
w3.bfa = {
clique: {
getSigners: function clique_getSigners( cb )
{ w3.req( 'clique_getSigners', [], cb ) },
},
miner: {
start: function miner_start()
{ w3.req( 'miner_start', [], function(){} ) },
stop: function miner_stop()
{ w3.req( 'miner_stop', [], function(){} ) }
},
admin: {
peers: function admin_peers( cb )
{ w3.req( 'admin_peers', [], cb ) },
addPeer: function admin_addPeer( peer )
{ w3.req( 'admin_addPeer', [ peer ], function(){} ) }
},
personal: {
listWallets: function personal_listWallets( cb )
{ w3.req( 'personal_listWallets', [], cb ) }
}
};
if ( undefined != process.env.BFAACCOUNT ) {
w3.eth.defaultAccount = this.account;
}
......
......@@ -10,7 +10,7 @@ $Carp::Verbose = 1;
sub _cat
{
my $filename = shift;
my ( $self, $filename ) = @_;
my $fh = IO::File->new($filename);
return if not defined $fh;
local $_ = join( '', $fh->getlines );
......@@ -26,8 +26,7 @@ sub _filecontents_or_default
{
my ($self, $filename, $default) = @_;
local $_ = $self->_cat( $filename );
return $default if not defined $_;
return $_;
return defined $_ ? $_ : $default;
}
sub new
......@@ -68,7 +67,8 @@ sub new
}
#
$self->{'netport'} = $self->_filecontents_or_default( $self->{'nodedir'}.'/netport', 30303 );
$self->{'rpcport'} = $self->_filecontents_or_default( $self->{'nodedir'}.'/netport', 8545 );
$self->{'rpcport'} = $self->_filecontents_or_default( $self->{'nodedir'}.'/rpcport', 8545 );
$self->{'rpchost'} = $self->_filecontents_or_default( $self->{'nodedir'}.'/rpchost', 'http://localhost' );
$self->{'ua'} = LWP::UserAgent->new;
return $self;
}
......@@ -92,14 +92,20 @@ sub contract
sub rpcreq
{
my ( $self, $opname, @params ) = @_;
my $req = HTTP::Request->new( POST => "http://127.0.0.1:".$self->{'rpcport'} );
$req->content_type('application/json');
my $extra = scalar @params
my ( $self, $opname, @params )
= @_;
my $ua = $self->{'ua'}->clone;
$ua->ssl_opts( 'verify_hostname' => 0 );
my $endpoint = sprintf '%s:%d', $self->{rpchost}, $self->{'rpcport'};
my $extra = scalar @params
? sprintf(qq(,\"params\":[%s]), join(',', @params))
: '';
$req->content( qq({"jsonrpc":"2.0","method":"${opname}"${extra},"id":1}));
my $res = $self->{'ua'}->request($req);
#
my $res = $ua->post(
$endpoint,
'Content-Type' => 'application/json',
'Content' => qq({"jsonrpc":"2.0","method":"${opname}"${extra},"id":1}),
);
die $res->status_line
unless $res->is_success;
return $res->content;
......
......@@ -67,10 +67,10 @@ function geth_exec
function geth_rpc
{
local cmd=$1
test -n "$cmd"
local params=
local params= connectstring= cmd=$1
shift
test -n "$cmd"
rpc_counter=$(( $rpc_counter + 1 ))
if [ $# -gt 0 ]
then
params=',"params":['
......@@ -82,12 +82,18 @@ function geth_rpc
# Eat the last comma and add a ]
params=${params/%,/]}
fi
if [ "$BFASOCKETTYPE" = "ipc" ]
then
connectstring="--unix-socket ${BFASOCKETURL}"
else
connectstring="${BFASOCKETURL}"
fi
local json=$(
curl \
-H 'Content-type: application/json' \
-X POST \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"${cmd}\"${params},\"id\":1}" \
http://127.0.0.1:$rpcport \
curl \
-H 'Content-type: application/json' \
-X POST \
--data "{\"jsonrpc\":\"2.0\",\"method\":\"${cmd}\"${params},\"id\":${rpc_counter}}" \
${connectstring} \
2>/dev/null
)
test -n "$json"
......@@ -99,26 +105,14 @@ function geth_rpc
echo "$json" | jq .result
}
function extradata
function create_account
{
# something uniqueish
## find default interface
local def_if=$(
( ip -4 route show ; ip -6 route show ) |
expand |
sed -ne '/^default /{s/ */ /g;s/^.* dev //;s/ .*//;p;q}'
)
local mymac=$(
ip link show ${def_if} |
sed -ne '/link\|ether/{s/^.*link.ether //;s/ .*//;s/://g;p;q}'
)
#
echo -n "${BFAACCOUNT:0:19}.${mymac:0:12}"
geth --cache 0 --datadir ${BFANODEDIR} --password /dev/null account new
}
function prereq
{
err=0
local err=0
while [ -n "$1" ]
do
if ! which $1 > /dev/null
......@@ -162,55 +156,56 @@ function contractSendTx
echo "contract.${func}.sendTransaction(${args} {from: eth.accounts[0], gas: 1000000} )"
}
###############
# bfainit #
test -n "${BFAHOME}" -a \
-d "${BFAHOME}" ||
fatal "\$BFAHOME in your environment must point to a directory."
#
# BFANETWORKID
test -n "${BFANETWORKID}" || BFANETWORKID=47525974938
#
# BFANETWORKDIR
test -n "${BFANETWORKDIR}" || BFANETWORKDIR="${BFAHOME}/network"
mkdir -p "${BFANETWORKDIR}"
test -d "${BFANETWORKDIR}" || fatal "\$BFANETWORKDIR (\"${BFANETWORKDIR}\") not found."
#
# BFANODEDIR
test -n "$BFANODEDIR" || BFANODEDIR="${BFANETWORKDIR}/node"
if [ ! -d "${BFANODEDIR}" -o ! -d "${BFANODEDIR}/geth/chaindata" ]
then
echo "Node is not initialised. Initialising with BFA genesis."
geth --cache 0 --datadir "${BFANODEDIR}" init "${BFAHOME}/src/genesis.json"
fi
#
# netport
netport=30303
if [ -r "${BFANODEDIR}/netport" ]
then
netport=$( cat ${BFANODEDIR}/netport )
test $? = 0
fi
#
# rpcport
rpcport=8545
if [ -r "${BFANODEDIR}/rpcport" ]
then
rpcport=$( cat ${BFANODEDIR}/rpcport )
test $? = 0
fi
#
# BFAACCOUNT
if [ -z "$BFAACCOUNT" ]
then
if ! ls -1d ${BFANODEDIR}/keystore/*--* >/dev/null 2>&1
function bfainit
{
rpc_counter=0
###############
# bfainit #
test -n "${BFAHOME}" -a \
-d "${BFAHOME}" ||
fatal "\$BFAHOME in your environment must point to a directory."
#
# BFANETWORKID
test -n "${BFANETWORKID}" || BFANETWORKID=47525974938
export BFANETWORKID
#
# BFANETWORKDIR
test -n "${BFANETWORKDIR}" || BFANETWORKDIR="${BFAHOME}/network"
mkdir -p "${BFANETWORKDIR}"
test -d "${BFANETWORKDIR}" || fatal "\$BFANETWORKDIR (\"${BFANETWORKDIR}\") not found."
export BFANETWORKDIR
#
# BFANODEDIR
test -n "$BFANODEDIR" || BFANODEDIR="${BFANETWORKDIR}/node"
export BFANODEDIR
#
# Default to IPC connections, because we have more geth modules available.
export ${BFASOCKETTYPE:=ipc}
case "${BFASOCKETTYPE}" in
ipc)
true ${BFASOCKETURL:="ipc:${BFANODEDIR}/geth.ipc"}
;;
http)
true ${BFASOCKETURL:="http://127.0.0.1:8545"}
;;
ws)
true ${BFASOCKETURL:="ws://127.0.0.1:8546"}
;;
*)
echo "Unknown socket type. Supported types are http, ws, ipc" >&2
exit 1
esac
# Init the blockchain with the genesis block
if [ ! -d "${BFANODEDIR}/geth/chaindata" ]
then
echo "No accounts found. Creating a new one (they are free)."
geth --cache 0 --datadir ${BFANODEDIR} --password /dev/null account new
mkdir -p "${BFANODEDIR}"
echo "Node is not initialised. Initialising with genesis."
geth --networkid ${BFANETWORKID} --cache 0 --datadir "${BFANODEDIR}" init "${BFANETWORKDIR}/genesis.json"
fi
BFAACCOUNT=$(
ls -1d "${BFANODEDIR}"/keystore/*--* 2>/dev/null |
head -1 |
sed 's/.*--//'
)
}
if [ -z "$SOURCED_BFAINIT_SH" ]
then
export SOURCED_BFAINIT_SH=yes
bfainit
fi
# libbfa.py
## Library for Python3 to talk to your local POA node.
```python
#!/usr/bin/env python3
import libbfa
bee = '0xbee0966BdC4568726AB3d3131b02e6255e29285D'
d18 = '0xbfA2c97c3f59cc929e8fEB1aEE2Aca0B38235d18'
```
Which network do you want to connect to?
If you want to connect to an open/public node
you can specify `bfa2018` or `test2`. If you
want to connect to another (your own?) node,
you can specify the URL, or give a `Provider`
```python
bfa = libbfa.Bfa('test2')
bfa = libbfa.Bfa('http://localhost:8545')
```
Find the file locally which matches the name
mentioned in the first argument. '0x' is
removed before case-insensitive matching
is performed.
Second argument (if specified) is the password.
```python
acct = libbfa.Account(bee)
acct = libbfa.Account(bee, 'pepe')
```
Create a skeleton for the 'factory'
The name of the contract is given in the second
argument and must be a contract in the current
directory (not symlinked).
If the contract is not compiled locally already
your local docker installation is used (must have).
```python
Factory = libbfa.CompiledContract(bfa.w3, 'Majority')
```
### Deploy a new contract.
If no address is given, an
account (object) must be given, which will be used
to deploy a new instance of the contract on the
blockchain. *You usually only want to deploy a
contract one time.* (If you need multiple tries,
consider using the test-net).
If you choose to deploy a new contract, remember to
give the arguments that the contract wishes for it's
constructor **or you will get an absurd error message
about being out of gas**.
The number `86400` in this example is the desired
argument for deployment of this particular contract
that the smart contract will get passed to it's
constructor. This contract takes a `uint256`.
Once you succesfully have a deployed contract in the
network, you can find it's address in the receipt's
`.address` field. Make a note of that address,
because you must use that address for all
subsequent calls to reach that contract.
```python
newdeployment = Factory.instance(86400, account=acct)
contractaddress = newdeployment.address
print('Your contract is deployed at address ' + contractaddress)
newdeployment = None
```
Now it is deployed and we can pretend that happened
many days ago. The 4 lines above need not be repeated,
but the others do (libbfa, account and Factory setup).
Now, we'll create a new reference to the same contract.
Since our contract is deployed now, we
see how to reference the already deployed
contract.
```python
samecontract = Factory.instance(address=contractaddress)
# better variable name for later
majority=samecontract
```
call()s are free (no gas needed) and local to the node
you are working with.
They require no account, no signature, no
transaction and are almost instant.
```python
print('Council length: {}'.format(majority.functions.councilLength().call()))
print('Votes length: {}'.format(majority.functions.votesLength().call()))
print('isCouncil bee: {}'.format(majority.functions.isCouncil(bee).call()))
print('mayVote bee,d18,True: {}'.format(majority.functions.mayVote(bee,d18,True).call()))
print('mayVote bee,d18,False: {}'.format(majority.functions.mayVote(bee,d18,False).call()))
print('mayVote d18,bee,True: {}'.format(majority.functions.mayVote(d18,bee,True).call()))
```
Transactions form part of the blockchain and must be mined
by the sealers, so they take a little longer to complete.
They are signed by the sender account.
Your *kwargs* must reference a configured `web3` (probably the one from
`libbfa`), and also give a reference to the `function` that you are
going to send a transaction to.
The first arguments (all the *args*) will be sent to the function in
the smart contract, so make sure the match in type.
The function name is a funny mix between Python variable names you
have defined yourself, plus functions which have arisen from the smart
contracts ABI.
```python
r = acct.transact(d18, False, web3=bfa.w3, function=majority.functions.vote)
print(r)
```
### Error examples
<a name="argument-after-asterisk-must-be-an-iterable-not-nonetype"></a>
argument after * must be an iterable, not NoneType
```python
# Error text: argument after * must be an iterable, not NoneType
print(majorcontr.functions.councilLength.call())
# Fix
print(majorcontr.functions.councilLength().call())
```
<a name="argument-after-asterisk-must-be-an-iterable-not-nonetype"></a>
gas required exceeds allowance
```python
# Error text: ValueError: {'code': -32000, 'message': 'gas required exceeds allowance (8000000)'}
```
Tu transacción va a fallar.
import os
import sys
import subprocess
import json
import re
import web3
import web3.exceptions
import web3.middleware
import ecdsa
from Crypto.Hash import keccak
import eth_account;
class Bfa:
def __init__(self, provider=''):
if 'BFAHOME' not in os.environ:
if os.path.isdir('/home/bfa/bfa'):
os.putenv('BFAHOME', '/home/bfa/bfa')
elif 'HOME' in os.environ and os.path.isdir(os.path.join(os.environ['HOME'], 'bfa')):
os.putenv('BFAHOME', os.path.join(os.environ['HOME'], 'bfa'))
if isinstance(provider, str):
if provider in ['prod', 'bfa2018', 'network', '']:
provider = web3.HTTPProvider("http://public.bfa.ar:8545/")
elif provider in ['test2network', 'test2', 'test']:
provider = web3.HTTPProvider("http://public.test2.bfa.ar:8545/")
elif provider.startswith('http://') or provider.startswith('https://'):
provider = web3.HTTPProvider(provider)
else:
raise ValueError('I do not know how to handle that provider.')
w3 = web3.Web3(provider)
# inject POA compatibility middleware
w3.middleware_onion.inject(web3.middleware.geth_poa_middleware, layer=0)
self.w3 = w3
class Account:
def __init__(self, *args):
if len(args) == 0:
self.new()
return
accountname = None
passphrase = ''
if len(args) >= 1:
accountname = args[0]
if len(args) >= 2:
passphrase = args[1]
self.keyfile = None
self.key = None
self.unlock(accountname, passphrase)
self.nonce = 0
def __repr__(self) -> str:
return str(dict(keyfile=self.keyfile, key=str(self.key)))
def __str__(self) -> str:
return self.address
def new(self):
acct = eth_account.Account.create()
self.address = acct.address
self.key = acct.key
self.save()
# print(acct.address)
# print(acct.key.hex())
return acct
def save(self):
dir = None
if os.getenv('BFANODEDIR') and os.path.exists(os.path.join(os.environ['BFANODEDIR'], 'keystore')):
dir = os.path.join(os.environ['BFANODEDIR'], 'keystore')
elif os.getenv('BFANETWORKDIR') and os.path.exists(os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore')):
dir = os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore')
elif os.getenv('BFAHOME') and os.path.exists(os.path.join(os.environ['BFAHOME'], 'network', 'node', 'keystore')):
dir = os.path.join(os.environ['BFAHOME'], 'network', 'node', 'keystore')
elif os.getenv('HOME'):
dir = os.path.join(os.environ['HOME'], '.ethereum', 'keystore')
os.makedirs(dir, mode=0o700, exist_ok=True)
else:
raise OSError('I have no idea where to save the file.')
self.keyfile = os.path.join(dir, self.address)
encrypted = eth_account.Account.encrypt(self.key.hex(), '')
try:
with open(self.keyfile, 'w', encoding='utf=8') as outfile:
outfile.write(str(encrypted).replace("'", '"'))
except:
# os.remove(filename)
raise
pass
@staticmethod
def findkeyfilesindirectories(**kwargs):
pattern = ''
if 'pattern' in kwargs:
pattern = kwargs['pattern']
# Remove leading 0x if present
if pattern.startswith('0x'):
pattern = pattern[2:]
# Lower case the pattern (account name)
pattern = pattern.lower()
# Find candidate directories of where to look for accounts
wheretolook = []
if os.getenv('BFANODEDIR'):
wheretolook += [os.path.join(os.environ['BFANODEDIR'], 'keystore')]
elif os.getenv('BFANETWORKDIR'):
wheretolook += [os.path.join(os.environ['BFANETWORKDIR'], 'node', 'keystore')]
elif os.getenv('BFAHOME'):
wheretolook += [os.path.join(os.environ['BFAHOME'], 'network', 'node', 'keystore')]
if os.getenv('HOME'):
wheretolook += [
os.path.join(os.environ['HOME'], '.ethereum', 'keystore'),
os.path.join(os.environ['HOME'], '.ethereum', 'keystore', 'test2'),
os.path.join(os.environ['HOME'], '.ethereum', 'keystore', 'network')
]
# Look for our pattern (or all files) in the directories
matches = []
ourregexp = '.*{}$'.format(pattern)
for d in wheretolook:
if os.path.exists(d):
for f in os.listdir(d):
fn = os.path.join(d, f)
if os.path.isfile(fn):
if re.match(ourregexp, f.lower()):
matches += [os.path.join(d, f)]
if pattern:
if len(matches) == 0:
# if a pattern was given but no matches were found,
# return None
return None
else:
# if a pattern was found but some matches were found,
# return just the first one
return matches[0]
# If no pattern was given, return everything found, or the
# empty list.
return matches
def unlock(self, accountname: str, passphrase: str):
if os.path.isfile(accountname):
self.keyfile = accountname
else:
self.keyfile = self.findkeyfilesindirectories(pattern=accountname)
if self.keyfile is None:
raise FileNotFoundError('The account was not found.')
with open(self.keyfile) as fd:
encrypted_key = fd.read()
try:
self.key = web3.Web3().eth.account.decrypt(encrypted_key, passphrase)
except ValueError as exc:
raise ValueError(
'The passphrase given for the account in file {} is incorrect, '
'or the input file is not a valid key file.'.format(self.keyfile)
) from exc
# ADDRESS
publickey = ecdsa.SigningKey.from_string(self.key, curve=ecdsa.SECP256k1).verifying_key
pkbytestring = publickey.to_string() # returns bytestring
ourhash = keccak.new(digest_bits=256)
ourhash.update(pkbytestring)
digest = ourhash.hexdigest()
self.address = web3.Web3().toChecksumAddress('0x' + digest[-40:])
def transact(self, *args, **kwargs):
w3 = kwargs.get('web3')
tx_details = self.calculate_tx_details(w3, *args, **kwargs)
afunction = kwargs.get('function')
txobj = tx_details
if afunction:
txobj = afunction(*args).buildTransaction(tx_details)
receipt = self.txsignsendwait(w3, txobj)
return receipt
def txsignsendwait(self, w3: web3.Web3, txobj: dict):
signedobj = self.signtx(txobj)
txhashbytes = w3.eth.sendRawTransaction(signedobj.rawTransaction)
self.nonce = self.nonce + 1
receipt = w3.eth.waitForTransactionReceipt(txhashbytes)
return receipt
def signtx(self, tx: dict):
signed = web3.Web3().eth.account.sign_transaction(tx, self.key)
return signed
def calculate_tx_details(self, w3: web3.Web3, *args, **kwargs) -> dict:
# if kwargs has extragas=50000 then we add that number to the amount
# of gas for the transaction
afunction = kwargs.get('function')
# Nonce may have increased on the network without us noticing
# or past transactions may not yet have been mined (and a flooded
# txpool).
# This is a resonable fix (try not to send too many transactions)
# If you use waitForTransactionReceipt() between each transaction
# you will not have problems because of this.
self.nonce = max(self.nonce, w3.eth.getTransactionCount(self.address))
# Set minimum gasPrice to 1 Gwei, but allow more if the network says so.
details = {
'chainId': w3.eth.chain_id,
'gasPrice': min(w3.toWei('1', 'gwei'), w3.eth.gasPrice),
'nonce': self.nonce,
'from': self.address,
}
for kw in [ 'to', 'value' ]:
val = kwargs.get(kw)
if val is not None:
details[kw] = val
# Ask for balance, so we can tell it, in case we have an exception
balance = w3.eth.getBalance(self.address)
try:
if afunction:
# Ask a node how much gas it would cost to deploy
gas = afunction(*args).estimateGas(details)
else:
gas = w3.eth.estimateGas(details)
except web3.exceptions.SolidityError as exc:
raise web3.exceptions.SolidityError(
'The Ethereum Virtual Machine probably did not like that.'
) from exc
except ValueError as exc:
raise ValueError(
'Your transaction will fail. Maybe you are calling your '
'contract wrong or are not allowed to call the funcion '
'by a function modifier or '
'your account may not have enough balance to work on this '
'network. Your account balance is currently {} wei.'
.format(balance)) from exc
if kwargs.get('extragas') is not None:
gas += kwargs.get('extragas')
details['gas'] = gas
return details
class Abi(list):
def __str__(self):
txt = ''
for i in range(len(self)):
elem = self.__getitem__(i)
if type(elem) is not dict:
continue
if 'type' in elem:
txt += "({}) ".format(elem['type'])
name = ""
if 'name' in elem:
name = elem['name']
if 'inputs' in elem:
inputlist = list()
for inputnum in range(len(elem['inputs'])):
_input = elem['inputs'][inputnum]
args = _input['type']
if 'name' in _input and _input['name'] != '':
args = "{}: {}".format(_input['name'], args)
inputlist.append(args)
txt += "{}({})".format(name, ', '.join(inputlist))
if 'outputs' in elem:
outputlist = list()
for outputnum in range(len(elem['outputs'])):
output = elem['outputs'][outputnum]
if 'name' in output and output['name'] != '':
outputlist.append("{}: {}".format(output['name'], output['type']))
else:
outputlist.append("{}".format(output['type']))
txt += " -> ({})".format(', '.join(outputlist))
if 'stateMutability' in elem:
txt += ' [{}]'.format(elem['stateMutability'])
txt += "\n"
return txt
# print(txt, file=sys.stderr)
# return super(Abi, self).__str__()
class CompiledContract:
solc_features = [
'abi', 'asm', 'ast', 'bin', 'bin-runtime', 'compact-format',
'devdoc', 'generated-sources', 'generated-sources-runtime',
'hashes', 'interface', 'metadata', 'opcodes', 'srcmap',
'srcmap-runtime', 'storage-layout', 'userdoc'
]
dockerWorkdir = '/casa'
def __init__(self, w3: web3.Web3, name: str):
self.w3 = w3
self.name = name
self.json = None
self.readtextfile()
# Did read give us json, or should we compile it?
if self.json is None:
self.compile()
def dockerargs(self):
return [
'docker', 'run',
'--rm',
# Mount our cwd as /casa
'-v',
'{}:{}'.format(os.getcwd(), self.dockerWorkdir),
# Run as us inside the docker, so we have access to this directory
'-u', str(os.getuid()),
'bfaar/nodo',
'/usr/local/bin/solc',
'--evm-version', 'byzantium',
# Get as many things dumped into our JSON as possible.
# You never know when you'll need it, and space is cheap.
'--combined-json', ','.join(self.solc_features),
# We like optimized things.
'--optimize',
# File name of input file.
'{}/contract.sol'.format(self.dockerWorkdir)
]
def compile(self):
# Make a copy with a fixed name
# Mostly so people can use symlinks to the source files
# which won't work when we call docker.
candidate_sol = '{}.sol'.format(self.name)
if os.path.exists(self.name):
filename = self.name
elif os.path.exists(candidate_sol):
filename = candidate_sol
else:
filename = self.name
with open(filename, 'r') as infile:
with open('contract.sol', 'w') as outfile:
outfile.write(infile.read())
try:
solc = subprocess.run(self.dockerargs(), stdout=subprocess.PIPE, check=True)
finally:
# Don't leave too much mess.
os.remove('contract.sol')
txt = solc.stdout
output = txt.decode('utf-8')
self.json = json.loads(output)
self.writetextfile()
def readtextfile(self):
output = None
for filename in ( '{}.compiled.json'.format(self.name), self.name ):
if os.path.exists( filename ):
with open(filename, 'rt', encoding='utf-8') as infile:
output = infile.read()
break
if output is None:
print("File not found.", file=sys.stderr)
raise FileNotFoundError
if len(output) < 2:
print("The JSON file is too small ({} bytes read from {}).".format(len(output), filename), file=sys.stderr)
raise NameError
try:
self.json = json.loads(output)
except json.decoder.JSONDecodeError as exc:
print("It was not possible to parse the JSON file (from is {}).".format(filename), file=sys.stderr)
raise exc
def writetextfile(self):
filename = self.name + '.compiled.json'
try:
with open(filename, 'xt', encoding='utf-8') as outfile:
outfile.write(json.dumps(self.json))
except:
os.remove(filename)
raise
def _where(self):
# Inch our way closer and closer, all the time,
# as long as we see labels which look vaguely familiar.
# This may enable us to be more liberal in the JSON input
# we receive from the file containing the cached compiled JSON.
point = self.json
for teststring in [
'contracts',
'{}/{}.sol:{}'.format(self.dockerWorkdir, 'contract', self.name),
'{}/{}.sol:{}'.format(self.dockerWorkdir, self.name, self.name)
]:
if type(point) is dict and teststring in point:
point = point[teststring]
return point
def bytecode(self):
self._where()
return '0x' + self._where()['bin']
def abi(self):
# Old method which also works, but our own class has a nicer __str__:
# return json.loads(self.json['contracts'][where]['abi'])
where = self._where()
if type(where) is str:
where = json.loads(where)
if (type(where) is dict) and 'abi' in where:
where = where['abi']
if type(where) is str:
where = json.loads(where)
return Abi(where)
def instance(self, *args, **kwargs):
addr = kwargs.get('address')
if addr is None:
account = kwargs.get('account')
if account is None:
raise KeyError('Either address or account must be speficied, in order to get an instance.')
ourinstance = self.w3.eth.contract(abi=self.abi(), bytecode=self.bytecode())
receipt = account.transact(*args, web3=self.w3, function=ourinstance.constructor)
if receipt.status == 0:
raise SystemError('Failed to deploy.')
addr = receipt.contractAddress
return self.w3.eth.contract(address=addr, abi=self.abi())