From eca69391ef5bcb968c637334d0c98490834fb1e0 Mon Sep 17 00:00:00 2001 From: Daniel Krol Date: Tue, 15 Feb 2022 15:49:12 -0500 Subject: [PATCH 1/3] Add wallet json-schema, validate in one test. --- lbry/schema/Makefile | 1 + lbry/schema/types/v2/wallet.json | 139 +++++++++++++++++++++++++++++++ setup.py | 1 + tests/unit/wallet/test_wallet.py | 66 +++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 lbry/schema/types/v2/wallet.json diff --git a/lbry/schema/Makefile b/lbry/schema/Makefile index 917b2a8e2..888bda7ff 100644 --- a/lbry/schema/Makefile +++ b/lbry/schema/Makefile @@ -2,4 +2,5 @@ build: rm types/v2/* -rf touch types/v2/__init__.py cd types/v2/ && protoc --python_out=. -I ../../../../../types/v2/proto/ ../../../../../types/v2/proto/*.proto + cd types/v2/ && cp ../../../../../types/jsonschema/* ./ sed -e 's/^import\ \(.*\)_pb2\ /from . import\ \1_pb2\ /g' -i types/v2/*.py diff --git a/lbry/schema/types/v2/wallet.json b/lbry/schema/types/v2/wallet.json new file mode 100644 index 000000000..1393713b0 --- /dev/null +++ b/lbry/schema/types/v2/wallet.json @@ -0,0 +1,139 @@ +{ + "title": "Wallet", + "description": "An LBC wallet", + "type": "object", + "required": ["name", "version", "accounts", "preferences"], + "additionalProperties": false, + "properties": { + "name": { + "description": "Human readable name for this wallet", + "type": "string" + }, + "version": { + "description": "Wallet spec version", + "type": "integer", + "$comment": "Should this be a string? We may need some sort of decimal type if we want exact decimal versions." + }, + "accounts": { + "description": "Accounts associated with this wallet", + "type": "array", + "items": { + "type": "object", + "required": ["address_generator", "certificates", "encrypted", "ledger", "modified_on", "name", "private_key", "public_key", "seed"], + "additionalProperties": false, + "properties": { + "address_generator": { + "description": "Higher level manager of either singular or deterministically generated addresses", + "type": "object", + "oneOf": [ + { + "required": ["name", "change", "receiving"], + "additionalProperties": false, + "properties": { + "name": { + "description": "type of address generator: a deterministic chain of addresses", + "enum": ["deterministic-chain"], + "type": "string" + }, + "change": { + "$ref": "#/$defs/address_manager", + "description": "Manager for deterministically generated change address (not used for single address)" + }, + "receiving": { + "$ref": "#/$defs/address_manager", + "description": "Manager for deterministically generated receiving address (not used for single address)" + } + } + }, { + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "description": "type of address generator: a single address", + "enum": ["single-address"], + "type": "string" + } + } + } + ] + }, + "certificates": { + "type": "object", + "description": "Channel keys. Mapping from public key address to pem-formatted private key.", + "additionalProperties": {"type": "string"} + }, + "encrypted": { + "type": "boolean", + "description": "Whether private key and seed are encrypted with a password" + }, + "ledger": { + "description": "Which network to use", + "type": "string", + "examples": [ + "lbc_mainnet", + "lbc_testnet" + ] + }, + "modified_on": { + "description": "last modified time in Unix Time", + "type": "integer" + }, + "name": { + "description": "Name for account, possibly human readable", + "type": "string" + }, + "private_key": { + "description": "Private key for address if `address_generator` is a single address. Root of chain of private keys for addresses if `address_generator` is a deterministic chain of addresses. Encrypted if `encrypted` is true.", + "type": "string" + }, + "public_key": { + "description": "Public key for address if `address_generator` is a single address. Root of chain of public keys for addresses if `address_generator` is a deterministic chain of addresses.", + "type": "string" + }, + "seed": { + "description": "Human readable representation of `private_key`. encrypted if `encrypted` is set to `true`", + "type": "string" + } + } + } + }, + "preferences": { + "description": "Timestamped application-level preferences. Values can be objects or of a primitive type.", + "$comment": "enable-sync is seen in example wallet. encrypt-on-disk is seen in example wallet. they both have a boolean `value` field. Do we want them explicitly defined here? local and shared seem to have at least a similar structure (type, value [yes, again], version), value being the free-form part. Should we define those here? Or can there be any key under preferences, and `value` be literally be anything in any form?", + "type": "object", + "additionalProperties": { + "type": "object", + "required": ["ts", "value"], + "additionalProperties": false, + "properties": { + "ts": { + "type": "number", + "description": "When the item was set, in Unix time format.", + "$comment": "Do we want a string (decimal)?" + }, + "value": { + "$comment": "Sometimes this has been an object, sometimes just a boolean. I don't want to prescribe anything." + } + } + } + } + }, + "$defs": { + "address_manager": { + "description": "Manager for deterministically generated addresses", + "type": "object", + "required": ["gap", "maximum_uses_per_address"], + "additionalProperties": false, + "properties": { + "gap": { + "description": "Maximum allowed consecutive generated addresses with no transactions", + "type": "integer" + }, + "maximum_uses_per_address": { + "description": "Maximum number of uses for each generated address", + "type": "integer" + } + } + } + } +} diff --git a/setup.py b/setup.py index ed59735b1..94689d1b2 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ setup( 'multidict==4.6.1', 'coincurve==15.0.0', 'pbkdf2==1.3', + 'jsonschema==4.4.0', 'attrs==18.2.0', 'pylru==1.1.0', 'elasticsearch==7.10.1', diff --git a/tests/unit/wallet/test_wallet.py b/tests/unit/wallet/test_wallet.py index c2bc99812..bbe0fb6a6 100644 --- a/tests/unit/wallet/test_wallet.py +++ b/tests/unit/wallet/test_wallet.py @@ -1,3 +1,7 @@ +import json +import jsonschema +import os +import pathlib import tempfile from binascii import hexlify @@ -17,6 +21,12 @@ class TestWalletCreation(AsyncioTestCase): self.main_ledger = self.manager.get_or_create_ledger(Ledger.get_id(), config) self.test_ledger = self.manager.get_or_create_ledger(RegTestLedger.get_id(), config) + base_dir = pathlib.Path(__file__).parents[3] + types_dir = base_dir.joinpath('lbry', 'schema', 'types', 'v2') + + with types_dir.joinpath('wallet.json').open() as f: + self.wallet_schema = json.load(f) + def test_create_wallet_and_accounts(self): wallet = Wallet() self.assertEqual(wallet.name, 'Wallet') @@ -74,6 +84,62 @@ class TestWalletCreation(AsyncioTestCase): decrypted = Wallet.unpack('password', encrypted) self.assertEqual(decrypted['accounts'][0]['name'], 'An Account') + + def test_wallet_file_schema(self): + # One Deterministic Chain, one Single Address, to test both paths in the schema. + wallet_dict = { + 'version': 1, + 'name': 'Main Wallet', + 'preferences': {}, + 'accounts': [ + { + 'certificates': {'x': 'y'}, + 'name': 'Account 1', + 'ledger': 'lbc_mainnet', + 'modified_on': 123, + 'seed': + "carbon smart garage balance margin twelve chest sword toast envelope bottom stomac" + "h absent", + 'encrypted': False, + 'private_key': + 'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7' + 'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe', + 'public_key': + 'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMm' + 'Dgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9', + 'address_generator': { + 'name': 'deterministic-chain', + 'receiving': {'gap': 17, 'maximum_uses_per_address': 3}, + 'change': {'gap': 10, 'maximum_uses_per_address': 3} + } + }, + { + 'certificates': {'a': 'b'}, + 'name': 'Account 2', + 'ledger': 'lbc_mainnet', + 'modified_on': 123, + 'seed': + "carbon smart garage balance margin twelve chest sword toast envelope bottom stomac" + "h absent", + 'encrypted': True, + 'private_key': + 'xprv9s21ZrQH143K42ovpZygnjfHdAqSd9jo7zceDfPRogM7bkkoNVv7' + 'DRNLEoB8HoirMgH969NrgL8jNzLEegqFzPRWM37GXd4uE8uuRkx4LAe', + 'public_key': + 'xpub661MyMwAqRbcGWtPvbWh9sc2BCfw2cTeVDYF23o3N1t6UZ5wv3EMm' + 'Dgp66FxHuDtWdft3B5eL5xQtyzAtkdmhhC95gjRjLzSTdkho95asu9', + 'address_generator': { + 'name': 'single-address', + } + }, + ] + } + + storage = WalletStorage(default=wallet_dict) + wallet = Wallet.from_storage(storage, self.manager) + self.assertDictEqual(wallet_dict, wallet.to_dict()) + jsonschema.validate(schema=self.wallet_schema, instance=wallet.to_dict()) + def test_no_password_but_encryption_preferred(self): wallet_dict = { 'version': 1, From 94bf35781700d3400f1ec17d67c1ffba44376d11 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 6 Apr 2022 10:22:52 -0400 Subject: [PATCH 2/3] cleanup paths --- tests/unit/wallet/test_wallet.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/tests/unit/wallet/test_wallet.py b/tests/unit/wallet/test_wallet.py index bbe0fb6a6..e4742696e 100644 --- a/tests/unit/wallet/test_wallet.py +++ b/tests/unit/wallet/test_wallet.py @@ -1,10 +1,10 @@ import json import jsonschema import os -import pathlib import tempfile from binascii import hexlify +import lbry.schema.types.v2 as schema_v2 from unittest import TestCase, mock from lbry.testcase import AsyncioTestCase from lbry.wallet import ( @@ -21,12 +21,6 @@ class TestWalletCreation(AsyncioTestCase): self.main_ledger = self.manager.get_or_create_ledger(Ledger.get_id(), config) self.test_ledger = self.manager.get_or_create_ledger(RegTestLedger.get_id(), config) - base_dir = pathlib.Path(__file__).parents[3] - types_dir = base_dir.joinpath('lbry', 'schema', 'types', 'v2') - - with types_dir.joinpath('wallet.json').open() as f: - self.wallet_schema = json.load(f) - def test_create_wallet_and_accounts(self): wallet = Wallet() self.assertEqual(wallet.name, 'Wallet') @@ -84,9 +78,7 @@ class TestWalletCreation(AsyncioTestCase): decrypted = Wallet.unpack('password', encrypted) self.assertEqual(decrypted['accounts'][0]['name'], 'An Account') - def test_wallet_file_schema(self): - # One Deterministic Chain, one Single Address, to test both paths in the schema. wallet_dict = { 'version': 1, 'name': 'Main Wallet', @@ -138,7 +130,9 @@ class TestWalletCreation(AsyncioTestCase): storage = WalletStorage(default=wallet_dict) wallet = Wallet.from_storage(storage, self.manager) self.assertDictEqual(wallet_dict, wallet.to_dict()) - jsonschema.validate(schema=self.wallet_schema, instance=wallet.to_dict()) + with open(os.path.join(*schema_v2.__path__, 'wallet.json')) as f: + wallet_schema = json.load(f) + jsonschema.validate(schema=wallet_schema, instance=wallet.to_dict()) def test_no_password_but_encryption_preferred(self): wallet_dict = { From 37eb55375a6cfb1cd6fc492de0ec8ff8cd7b7ab9 Mon Sep 17 00:00:00 2001 From: Lex Berezhny Date: Wed, 6 Apr 2022 20:28:20 -0400 Subject: [PATCH 3/3] only install jsonschema during testing --- setup.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 94689d1b2..a58837470 100644 --- a/setup.py +++ b/setup.py @@ -53,7 +53,6 @@ setup( 'multidict==4.6.1', 'coincurve==15.0.0', 'pbkdf2==1.3', - 'jsonschema==4.4.0', 'attrs==18.2.0', 'pylru==1.1.0', 'elasticsearch==7.10.1', @@ -63,7 +62,10 @@ setup( extras_require={ 'torrent': ['lbry-libtorrent'], 'lint': ['pylint==2.10.0'], - 'test': ['coverage'], + 'test': [ + 'coverage', + 'jsonschema==4.4.0', + ], 'scribe': ['scribe @ git+https://github.com/lbryio/scribe.git'], }, classifiers=[