Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
recursive-include rein/html *
recursive-include rein/lib/crypto/resources *
recursive-include rein/locale *
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@ clean:
rm -f enrollment.txt.sig

test:
python -m unittest2 rein/lib/script.py
python -m unittest2 rein/lib/bitcoinaddress.py
nosetests -v --ignore-files="test_cli.py"

test_all:
nosetests -v
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,27 +80,40 @@ For the mediator payment, a mandatory multisig address is created. To spend the

Like with the primary payment, a user is prompted for a signed mediator payment if they are a job creator accepting a delivery or they are the mediator resolving a dispute.

## Development and Testing
## Developer Notes

We have [a quick Vagrant-based setup](https://github.com/ReinProject/devsetup) that gives you a virtual machine with the python-rein client, Causway server and its Bitcoin Core (testnet) node all configured to work together. Testing usually involves creating users and walking through jobs so a virtual machine that has all components going, even allowing payments to be sent is very helpful.

Tests are run using nose:
To generate or update pot files for translation, run the following from the root of the repo:

$ nosetests
xgettext.pl rein/cli.py rein/lib/*.py

## Testing

We have [a quick Vagrant-based setup](https://github.com/ReinProject/devsetup) that gives you a virtual machine with the python-rein client, Causway server and its Bitcoin Core (testnet) node all configured to work together. Testing usually involves creating users and walking through jobs so a virtual machine that has all components going, even allowing payments to be sent is very helpful.

Tests are run using nose, with the make file specifying two different test commands:

$ make test
nosetests -v --ignore-files="test_cli.py"
..
----------------------------------------------------------------------
Ran 2 tests in 0.837s
Ran 5 tests in 0.344

OK

And unittest2:
and

$ make test
python -m unittest2 rein/lib/*.py
....
$ make test_all
nosetests -v
..
----------------------------------------------------------------------
Ran 4 tests in 0.001s
Ran 7 tests in 2.393s

OK

By default, the more limited "test" command should be used when a dedicated test-environment, such as the Vagrant set-up mentioned above, is not available. It currently runs all tests except the tests specified in "tests/test_cli.py".

Tox fails right now but does run flake so will be helpful for cleanup.

Be aware that new unit tests should be added to a file within the tests directory. Both the file name and the test method names should follow the naming convention "test_*".
19 changes: 12 additions & 7 deletions rein/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .lib.util import unique
from .lib.io import safe_get
from .lib.script import build_2_of_3, build_mandatory_multisig, check_redeem_scripts
from .lib.localization import init_localization
from .lib.transaction import partial_spend_p2sh, spend_p2sh, spend_p2sh_mediator, partial_spend_p2sh_mediator, partial_spend_p2sh_mediator_2
from .lib.rating import add_rating, get_user_jobs

Expand All @@ -45,17 +46,20 @@
from .lib.mediator import Mediator

rein = config.Config()
init_localization()

import bitcoin
from bitcoin.wallet import P2PKHBitcoinAddress
from bitcoin.core import x
if (rein.testnet): bitcoin.SelectParams('testnet')
init_localization()


@click.group()
@click.option('--debug/--no-debug', default=False)
@click.pass_context
def cli(ctx, debug):
"""
_("""
Rein is a decentralized professional services market and Python-rein is a client
that provides a user interface. Use this program from your local browser or command
line to create an account, post a job, bid, etc.
Expand All @@ -79,7 +83,7 @@ def cli(ctx, debug):
$ rein resolve - mediator posts decision

For more info and the setup guide visit: http://reinproject.org
"""
""")
if debug:
click.echo("Debuggin'")
pass
Expand All @@ -88,23 +92,24 @@ def cli(ctx, debug):
@cli.command()
@click.option('--multi/--no-multi', default=False, help="add even if an identity exists")
def setup(multi):
"""
_("""
Setup or import an identity.

You will choose a name or handle for your account, include public contact information,
and a delegate Bitcoin address/private key that the program will use to sign documents
on your behalf. An enrollment document will be created and you will need to sign it
with your master Bitcoin private key.
"""
""")
log = rein.get_log()
if multi:
rein.set_multiuser()
log.info('entering setup')
if multi or rein.has_no_account():
click.echo("\n" + highlight("Welcome to Rein.", True, True) + "\n\n"
"Do you want to import a backup or create a new account?\n\n"
"1 - Create new account\n2 - Import backup\n")
click.echo("\n" + highlight(_("Welcome to Rein."), True, True) + "\n\n" +
_("Do you want to import a backup or create a new account?\n\n") +
_("1 - Create new account\n2 - Import backup\n"))
choice = click.prompt(highlight("Choice", True, True), type=int, default=1)

if choice == 1:
create_account(rein)
log.info('account created')
Expand Down
12 changes: 0 additions & 12 deletions rein/lib/bitcoinaddress.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
from crypto.util import ripemd160
from bitcoin import base58
from binascii import unhexlify
import unittest

digits58 = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'


def to_bytes(n, length, endianess='big'):
h = '%x' % n
s = ('0' * (len(h) % 2) + h).zfill(length * 2).decode('hex')
Expand All @@ -26,16 +24,6 @@ def check_bitcoin_address(bc):
bcbytes = decode_base58(bc, 25)
return bcbytes[-4:] == sha256(sha256(bcbytes[:-4]).digest()).digest()[:4]


class BitcoinAddressTest(unittest.TestCase):
def test_check_bitcoin_address(self):
self.assertTrue(check_bitcoin_address('1CptxARjqcfkVwGFSjR82zmPT8YtRMubub'))
self.assertTrue(check_bitcoin_address('3746f7fjJ6fG1pQXDjA8xy9WAzf4968WWv'))
self.assertFalse(check_bitcoin_address('2746f7fjJ6fG1pQXDjA8xy9WAzf4968WWv'))

def test_check_sin(self):
self.assertEqual(generate_sin('02F840A04114081690223B7069071A70D6DABB891763B638CC20C7EC3BD58E6C86'), 'TfG4ScDgysrSpodWD4Re5UtXmcLbY5CiUHA')

def generate_sin(master_key):
"""Generates a type 2 'Secure Identity Number' using the bip32 master public key"""
prefix = 0x0F
Expand Down
68 changes: 34 additions & 34 deletions rein/lib/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,16 @@

def validate_privkey(form, field):
if not privkey_to_address(field.data):
raise ValidationError("Not a valid private key.")
raise ValidationError(_("Not a valid private key."))

def validate_address(form, field):
if not check_bitcoin_address(field.data):
raise ValidationError("Invalid address")
raise ValidationError(_("Invalid address"))

def validate_en(form, field):
message = field.data.replace("\r\n","\n")
if not validate_enrollment(message):
raise ValidationError("Invalid signature")
raise ValidationError(_("Invalid signature"))

def validate_mediator_fee(form, field):
try:
Expand All @@ -38,55 +38,55 @@ def avoid_self_rating(form, field):
raise ValidationError('You may not rate yourself!')

class SetupForm(Form):
name = TextField('Name / Handle', validators = [Required()])
contact = TextField('Email / Bitmessage', validators = [Required()])
maddr = TextField('Master Bitcoin address', validators = [Required(), validate_address])
daddr = TextField('Delegate Bitcoin address', validators = [Required(), validate_address])
dkey = PasswordField('Delegate Bitcoin private Key', validators = [Required(), validate_privkey])
will_mediate = RadioField('Register as a mediator?', choices = [('1','Yes'), ('0', 'No')])
mediator_fee = TextField('Mediator Fee', validators = [validate_mediator_fee]) # TODO make required only if Yes above
name = TextField(_('Name / Handle'), validators = [Required()])
contact = TextField(_('Email / Bitmessage'), validators = [Required()])
maddr = TextField(_('Master Bitcoin address'), validators = [Required(), validate_address])
daddr = TextField(_('Delegate Bitcoin address'), validators = [Required(), validate_address])
dkey = PasswordField(_('Delegate Bitcoin private Key'), validators = [Required(), validate_privkey])
will_mediate = RadioField(_('Register as a mediator?'), choices = [('1','Yes'), ('0', 'No')])
mediator_fee = TextField(_('Mediator Fee'), validators = [validate_mediator_fee]) # TODO make required only if Yes above

class SignForm(Form):
identity_id = HiddenInput("identity_id")
signed = TextAreaField('Signed enrollment', validators = [Required(), validate_en])
signed = TextAreaField(_('Signed enrollment'), validators = [Required(), validate_en])

class JobPostForm(Form):
job_name = TextField('Job name', validators = [Required()])
description = TextAreaField('Description', validators = [Required()])
tags = TextField('Tags', validators = [Required()])
expire_days = TextField('Expiration (days)', validators = [Required()])
mediator_maddr = RadioField('Choose mediator')
job_name = TextField(_('Job name'), validators = [Required()])
description = TextAreaField(_('Description'), validators = [Required()])
tags = TextField(_('Tags'), validators = [Required()])
expire_days = TextField(_('Expiration (days)'), validators = [Required()])
mediator_maddr = RadioField(_('Choose mediator'))

class BidForm(Form):
description = TextAreaField('Description', validators = [Required()])
bid_amount = TextAreaField('Bid amount', validators = [Required()])
job_id = RadioField('Choose Job to bid on')
description = TextAreaField(_('Description'), validators = [Required()])
bid_amount = TextAreaField(_('Bid amount'), validators = [Required()])
job_id = RadioField(_('Choose Job to bid on'))

class JobOfferForm(Form):
bid_id = RadioField('Choose bid')
bid_id = RadioField(_('Choose bid'))

class DeliverForm(Form):
deliverable = TextAreaField('Deliverables', validators = [Required()])
job_id = RadioField('Choose job associated with deliverables')
deliverable = TextAreaField(_('Deliverables'), validators = [Required()])
job_id = RadioField(_('Choose job associated with deliverables'))

class DisputeForm(Form):
dispute_detail = TextAreaField('Dispute detail', validators = [Required()])
order_id = RadioField('Choose job')
dispute_detail = TextAreaField(_('Dispute detail'), validators = [Required()])
order_id = RadioField(_('Choose job'))

class AcceptForm(Form):
deliverable_id = RadioField('Deliverables')
deliverable_id = RadioField(_('Deliverables'))

class ResolveForm(Form):
resolution = TextAreaField('Resolution', validators = [Required()])
client_payment_amount = TextField('Client payment amount in BTC (remainder sent to worker)', validators = [Required()])
dispute_id = RadioField('Disputes')
resolution = TextAreaField(_('Resolution'), validators = [Required()])
client_payment_amount = TextField(_('Client payment amount in BTC (remainder sent to worker)'), validators = [Required()])
dispute_id = RadioField(_('Disputes'))

class AcceptResolutionForm(Form):
resolution_id = RadioField('Resolution')
resolution_id = RadioField(_('Resolution'))

class RatingForm(Form):
job_id = TextField('Job id', validators = [Required()], default='')
user_id = TextField('User SIN', validators = [Required(), avoid_self_rating], default='')
rated_by_id = TextField('Rated by SIN', validators = [Required()], default='')
rating = TextField('Rating', validators=[Required()], default=0, widget=HiddenInput())
comments = TextAreaField('Comments', validators = [], default='')
job_id = TextField(_('Job ID'), validators = [Required()], default='')
user_id = TextField(_('User SIN'), validators = [Required(), avoid_self_rating], default='')
rated_by_id = TextField(_('Rated by SIN'), validators = [Required()], default='')
rating = TextField(_('Rating'), validators=[Required()], default=0, widget=HiddenInput())
comments = TextAreaField(_('Comments'), validators = [], default='')
2 changes: 1 addition & 1 deletion rein/lib/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def safe_get(log, url):
try:
answer = requests.get(url=url, proxies=rein.proxies)
except requests.ConnectionError:
click.echo('Error connecting to server.')
click.echo(_('Error connecting to server.'))
log.error('server connect error ' + url)
return None

Expand Down
26 changes: 26 additions & 0 deletions rein/lib/localization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import gettext
import locale
import logging
import os
import rein.locale

def init_localization():
'''prepare l10n'''
# Extract system locale identifier
loc = locale.getdefaultlocale()[0]
file_name = "messages-%s.mo" % loc.split('_')[0].upper()
file_path = os.path.join(os.path.dirname(os.path.abspath(rein.locale.__file__)), file_name)

try:
logging.debug("Opening message file %s for locale %s", file_name, loc[0])
trans = gettext.GNUTranslations(open(file_path, "rb" ))

except IOError:
logging.debug("Locale not found. Using default messages")
trans = gettext.NullTranslations()

trans.install()

if __name__ == '__main__':
init_localization()

2 changes: 1 addition & 1 deletion rein/lib/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def sign_and_store_document(rein, doc_type, document, signature_address=None, si
click.echo("\n%s\n" % document)
done = False
while not done:
filename = click.prompt("File containing signed document", type=str, default=doc_type + '.sig.txt')
filename = click.prompt(_("File containing signed document"), type=str, default=doc_type + '.sig.txt')
if os.path.isfile(filename):
done = True
f = open(filename, 'r')
Expand Down
Loading