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..a58837470 100644 --- a/setup.py +++ b/setup.py @@ -62,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=[ diff --git a/tests/unit/wallet/test_wallet.py b/tests/unit/wallet/test_wallet.py index c2bc99812..e4742696e 100644 --- a/tests/unit/wallet/test_wallet.py +++ b/tests/unit/wallet/test_wallet.py @@ -1,6 +1,10 @@ +import json +import jsonschema +import os 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 ( @@ -74,6 +78,62 @@ class TestWalletCreation(AsyncioTestCase): decrypted = Wallet.unpack('password', encrypted) self.assertEqual(decrypted['accounts'][0]['name'], 'An Account') + def test_wallet_file_schema(self): + 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()) + 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 = { 'version': 1,