mirror of https://github.com/nucypher/nucypher.git
commit
723cc00afc
|
@ -9,6 +9,7 @@ recursive-exclude * __pycache__
|
|||
global-exclude *.py[cod]
|
||||
|
||||
recursive-include nucypher/blockchain/eth/contract_registry *.json *.md
|
||||
recursive-include nucypher/policy/conditions *.json
|
||||
prune nucypher/blockchain/eth/contract_registry/historical
|
||||
recursive-include nucypher/network/templates *.html *.mako
|
||||
recursive-include nucypher/utilities/templates *.html *.mako
|
||||
|
|
3
Pipfile
3
Pipfile
|
@ -12,7 +12,7 @@ constant-sorrow = ">=0.1.0a9"
|
|||
bytestring-splitter = ">=2.4.0"
|
||||
hendrix = ">=3.4"
|
||||
lmdb = "*"
|
||||
nucypher-core = ">=0.2"
|
||||
nucypher-core = ">=0.4.0"
|
||||
# Cryptography
|
||||
pyopenssl = "*"
|
||||
cryptography = ">=3.2"
|
||||
|
@ -38,6 +38,7 @@ py-evm = "*"
|
|||
eth-tester = "*"
|
||||
web3 = "*"
|
||||
eth-utils = "*"
|
||||
eip712-structs = "*"
|
||||
# CLI / Configuration
|
||||
appdirs = "*"
|
||||
click = ">=7.0"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,4 +1,4 @@
|
|||
FROM python:3.8.12-slim
|
||||
FROM nucypher/rust-python:3.8.12
|
||||
|
||||
# Update
|
||||
RUN apt-get update -y && apt upgrade -y
|
||||
|
|
|
@ -1,49 +1,50 @@
|
|||
-i https://pypi.python.org/simple
|
||||
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
attrs==22.1.0; python_version >= '3.5'
|
||||
bandit==1.7.4
|
||||
certifi==2021.10.8
|
||||
certifi==2022.9.24; python_version >= '3.6'
|
||||
cfgv==3.3.1; python_full_version >= '3.6.1'
|
||||
charset-normalizer==2.0.12; python_version >= '3'
|
||||
coverage==6.3.2
|
||||
charset-normalizer==2.1.1; python_version >= '3.6'
|
||||
coverage==6.5.0
|
||||
decorator==5.1.1; python_version >= '3.5'
|
||||
distlib==0.3.4
|
||||
distlib==0.3.6
|
||||
exceptiongroup==1.0.0rc9; python_version < '3.11'
|
||||
execnet==1.9.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
filelock==3.6.0; python_version >= '3.7'
|
||||
filelock==3.8.0; python_version >= '3.7'
|
||||
gitdb==4.0.9; python_version >= '3.6'
|
||||
gitpython==3.1.27; python_version >= '3.7'
|
||||
greenlet==2.0.0a2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
hypothesis==6.45.0
|
||||
identify==2.4.12; python_version >= '3.7'
|
||||
idna==3.3; python_version >= '3'
|
||||
hypothesis==6.56.0
|
||||
identify==2.5.5; python_version >= '3.7'
|
||||
idna==3.4; python_version >= '3.5'
|
||||
iniconfig==1.1.1
|
||||
mypy-extensions==0.4.3
|
||||
mypy==0.942
|
||||
nodeenv==1.6.0
|
||||
mypy==0.981
|
||||
nodeenv==1.7.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'
|
||||
packaging==21.3; python_version >= '3.6'
|
||||
pbr==5.8.1; python_version >= '2.6'
|
||||
pbr==5.10.0; python_version >= '2.6'
|
||||
platformdirs==2.5.2; python_version >= '3.7'
|
||||
pluggy==1.0.0; python_version >= '3.6'
|
||||
pre-commit==2.18.1
|
||||
pre-commit==2.20.0
|
||||
py-solc-x==0.10.1
|
||||
py==1.11.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pyflakes==2.4.0
|
||||
pyparsing==3.0.8; python_full_version >= '3.6.8'
|
||||
pytest-cov==3.0.0
|
||||
pyflakes==2.5.0
|
||||
pyparsing==3.0.9; python_full_version >= '3.6.8'
|
||||
pytest-cov==4.0.0
|
||||
pytest-forked==1.4.0; python_version >= '3.6'
|
||||
pytest-mock==3.7.0
|
||||
pytest-mock==3.9.0
|
||||
pytest-timeout==2.1.0
|
||||
pytest-twisted==1.13.4
|
||||
pytest-xdist==2.5.0
|
||||
pytest==6.2.5
|
||||
pyyaml==6.0; python_version >= '3.6'
|
||||
requests==2.27.1
|
||||
semantic-version==2.9.0; python_version >= '2.7'
|
||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
requests==2.28.1
|
||||
semantic-version==2.10.0; python_version >= '2.7'
|
||||
setuptools==65.4.1; python_version >= '3.7'
|
||||
smmap==5.0.0; python_version >= '3.6'
|
||||
sortedcontainers==2.4.0
|
||||
stevedore==3.5.0; python_version >= '3.6'
|
||||
stevedore==4.0.0; python_version >= '3.8'
|
||||
toml==0.10.2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
|
||||
tomli==2.0.1; python_version >= '3.7'
|
||||
typing-extensions==3.10.0.2
|
||||
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
virtualenv==20.14.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
tomli==2.0.1; python_version < '3.11'
|
||||
typing-extensions==4.3.0; python_version >= '3.7'
|
||||
urllib3==1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'
|
||||
virtualenv==20.16.5; python_version >= '3.6'
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
-i https://pypi.python.org/simple
|
||||
alabaster==0.7.12
|
||||
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
attrs==22.1.0; python_version >= '3.5'
|
||||
autosemver==0.5.5
|
||||
babel==2.10.1; python_version >= '3.6'
|
||||
certifi==2021.10.8
|
||||
charset-normalizer==2.0.12; python_version >= '3'
|
||||
babel==2.10.3; python_version >= '3.6'
|
||||
certifi==2022.9.24; python_version >= '3.6'
|
||||
charset-normalizer==2.1.1; python_version >= '3.6'
|
||||
click-default-group==1.2.2
|
||||
click==8.1.2; python_version >= '3.7'
|
||||
coverage==6.3.2; python_version >= '3.7'
|
||||
click==8.1.3; python_version >= '3.7'
|
||||
coverage==6.5.0; python_version >= '3.7'
|
||||
docutils==0.17.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
dulwich==0.19.16
|
||||
execnet==1.9.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
git+https://github.com/inspirehep/jsonschema2rst.git@d211d9709046431a6b6a4594c97c22d8713d854b#egg=jsonschema2rst
|
||||
idna==3.3; python_version >= '3'
|
||||
imagesize==1.3.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
idna==3.4; python_version >= '3.5'
|
||||
imagesize==1.4.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
incremental==21.3.0
|
||||
iniconfig==1.1.1
|
||||
isort==5.10.1; python_full_version >= '3.6.1' and python_version < '4'
|
||||
|
@ -25,17 +25,17 @@ pep8==1.7.1
|
|||
pluggy==1.0.0; python_version >= '3.6'
|
||||
py-solc-x==0.10.1
|
||||
py==1.11.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pygments==2.12.0; python_version >= '3.6'
|
||||
pyparsing==3.0.8; python_full_version >= '3.6.8'
|
||||
pygments==2.13.0; python_version >= '3.6'
|
||||
pyparsing==3.0.9; python_full_version >= '3.6.8'
|
||||
pytest-cache==1.0
|
||||
pytest-cov==3.0.0; python_version >= '3.6'
|
||||
pytest-cov==4.0.0; python_version >= '3.6'
|
||||
pytest-pep8==1.0.6
|
||||
pytest==7.1.2; python_version >= '3.7'
|
||||
pytz==2022.1
|
||||
pytest==7.1.3; python_version >= '3.7'
|
||||
pytz==2022.4
|
||||
pyyaml==6.0; python_version >= '3.6'
|
||||
requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
semantic-version==2.9.0; python_version >= '2.7'
|
||||
setuptools==62.1.0; python_version >= '3.7'
|
||||
requests==2.28.1; python_version >= '3.7' and python_version < '4'
|
||||
semantic-version==2.10.0; python_version >= '2.7'
|
||||
setuptools==65.4.1; python_version >= '3.7'
|
||||
snowballstemmer==2.2.0
|
||||
sphinx-rtd-theme==1.0.0
|
||||
sphinx==3.0.1
|
||||
|
@ -45,6 +45,6 @@ sphinxcontrib-htmlhelp==2.0.0; python_version >= '3.6'
|
|||
sphinxcontrib-jsmath==1.0.1; python_version >= '3.5'
|
||||
sphinxcontrib-qthelp==1.0.3; python_version >= '3.5'
|
||||
sphinxcontrib-serializinghtml==1.1.5; python_version >= '3.5'
|
||||
tomli==2.0.1; python_version >= '3.6'
|
||||
towncrier==21.9.0
|
||||
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
tomli==2.0.1; python_version >= '3.7'
|
||||
towncrier==22.8.0
|
||||
urllib3==1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'
|
||||
|
|
|
@ -649,7 +649,7 @@ Example Response
|
|||
}
|
||||
]
|
||||
},
|
||||
"version": "6.0.0"
|
||||
"version": "6.1.0"
|
||||
}
|
||||
|
||||
|
||||
|
@ -674,6 +674,11 @@ Parameters
|
|||
+-------------------------------------------+---------------+----------------------------------------+
|
||||
| ``bob_verifying_key`` | String | Bob's verifying key encoded as hex. |
|
||||
+-------------------------------------------+---------------+----------------------------------------+
|
||||
| ``context`` *(Optional)* | String | | Associated JSON dictionary required |
|
||||
| | | | during re-encryption e.g. data to |
|
||||
| | | | satisfy re-encryption conditions. |
|
||||
+-------------------------------------------+---------------+----------------------------------------+
|
||||
|
||||
|
||||
* A single *retrieval kit* is an encapsulation of the information necessary to obtain cfrags from Ursulas.
|
||||
It contains a capsule and the checksum addresses of the Ursulas from which the requester has
|
||||
|
@ -698,6 +703,10 @@ Parameters
|
|||
because some Ursulas may have experienced a blip in connectivity. This is an optional optimization that provides
|
||||
retry functionality that skips previously successful reencryption operations.
|
||||
|
||||
* A *context* is an associated JSON dictionary of data required during re-encryption. One such example is when a condition for re-encryption
|
||||
requires proof of ownership of a wallet address; the *context* is used to provide the data and signature required for the proof.
|
||||
|
||||
|
||||
Returns
|
||||
^^^^^^^
|
||||
The result of the re-encryption operations performed:
|
||||
|
@ -720,12 +729,6 @@ Example Request
|
|||
"bob_verifying_key": "039c19e5d44b016af126d89488c4ae5599e0fde9ea30047754d1fe173d05eee468",
|
||||
"policy_encrypting_key": "02cdb2cec70b568c0624b72450c2043836aa831b06b196a50db461e87acddb791e"}'
|
||||
|
||||
OR
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl -X POST "<PORTER URI>/retrieve_crags?retrieval_kits=%5B%27gANDYgMKitDPd%2FQttLGy%2Bs7Oacnm8pfbl3Qs2UD3IS1d9wF3awJsXnjFq7OkRQE45DV4%2BMa2lDSJ5SeKEBqJK5GdPMB6CRwJ1hX7Y5SYgzpZtr%2FZ5%2FS3DHgVKn%2B8fWX92FaqEXIGcQBjYnVpbHRpbnMKc2V0CnEBXXEChXEDUnEEhnEFLg%3D%3D%27%5D&alice_verifying_key=02d3389864e9e7206ae1d18301bbd67ad8e0bdf257b3085c9aa13e9438ff9133f2&bob_encrypting_key=03d41cb7aa2df98cb9fb1591b5556363862a367faae6d0e4874a860321141788cb&bob_verifying_key=039c19e5d44b016af126d89488c4ae5599e0fde9ea30047754d1fe173d05eee468&policy_encrypting_key=02cdb2cec70b568c0624b72450c2043836aa831b06b196a50db461e87acddb791e&treasure_map=ivOS2%2FMarBpkLAksM0O%2BpgLUHAV%2F0ceIBarBKwqUpAXARhpvuwAAAm0DoDAtioScWJSHWNGzQd9pMGW2dRF4IvJX%2FExALF6AcLICLCBP%2Btte8QR4l0GLNy3YwK4oO8f8Ht0Ij%2Bv0feWWwgeo3R7FVeC4ExDuYvgdsV6jCP3vqZnLphIPk8LQeo1XVAABAtM4mGTp5yBq4dGDAbvWetjgvfJXswhcmqE%2BlDj%2FkTPyAAAB5H0rD40N1u5Ct455sh4SicbHTGsXcRSt%2FadeHVl3zylNpWDsFbeon7VI5fGGmWLAKmCJ5ARU1Mgfwg0pfsXDgHTky6XOeXnNw630z9muBE4NMUiESOQm%2FRAsphMR%2FDEIMRaCgjhaE2diVdVAm15JjRXV9JN5gAp58Y1ecPcWR2lMcgAMHBFMX60bpbgjySha94Hwb0kR2SKIFkPQnuMljoQxutTDAyh55eE2sHf9ZOAVZkpKQu8NkaWy7adx%2F1QefezNbngX9c2yYml133Al4oGrLWYA3fnbod2Y6F1oeG5As5ZIW%2FO8k7Rf%2B3i9a%2BDS1i%2BKbgETHQGxOkQSpNPUmwJjtzDJQ1xFMmKkxgwUtXenfyrzDDPU6EQloWK2PmyTD%2FhSKHLpkLyzYp95gadzDiS8RlOnNw%2FuP8vfMPSrXYGZSKXvHvlrQxKOjnF7FrheauwwRPjM0yYTftPs3jNkZwCTl%2BEwn6NdLur927SeGyAB3gHCjHenje%2B3hU1jsn%2FmwfwLJwSMT7V0rbXV6I0NYhjQy2Ajj%2B7ev%2FNSvRdeneeYTU3iHoO6nIhWHBLVExWafu59B6hhsm261kvXw718eiUcL%2B1X1eZ5WApplCuXGQV7L6DZxlQPanRJy7BZZQmFwEUoMCnx9mGbOKmNbeCADx3vwKY5nrbTDAAAAm0Cccv5a3jS2QiICCzCyA0Ot3U7VT1F3d%2BB3cHcmv8DaCwDODb8IadnsiVK%2BdfbPLn3ne%2Blm8d9yqhh6bLi6KNDb6yiWrjWnd4Irnnh3amMwik00vdyQKYvdsaSEJqtVLmtcQABAtM4mGTp5yBq4dGDAbvWetjgvfJXswhcmqE%2BlDj%2FkTPyAAAB5Do%2Feww%2BG709VPQwkxd0tRFyJh97Wcb5uSGs%2B4fK9O%2B5CTf5rDQSO3ueWLRF4ytRzd3QjK6%2B8FlXsJQM5n5pGLUNNWpUlimk2MmPaLehC9uGBfQzoTfQof%2BU8CBXkTTnRi0IeAYMq8eiuEnNR%2FoOJjpjuwgZH4gue%2FsSDF8FyhFU4SwF%2FWdjLg0FgmZzRlqABNXeE8vOofydEMYgUMPd8qxjimAGhkYlBUNjlme4BUdA2AqndMttpc3y9ILTobaGSnjgWfq9Ztw%2Fn72scPI11T%2BYMaaXd33dacNPx%2BpVzcgqi358PT8WQ6U3n%2B1be8mhF8VGEO7%2F5zLFHECRCv06erER8ChTZvr4rb8Y0xRCz%2FpatllLqvWZkGSmotmsi9qAptgG%2FXkozOZIqmBuM2AuQTwaePyuJzelc5xD51OlkQRahV6%2Bok3CokckwtOXtC6dzq4dmh03Uj5ZeKj8IgITDPN6jCf5TwLmXSuEGl5W%2FxmrEUeNlrthlJm7Cdd1NpLn3RZNCgSS4%2BPw9cpY6fj%2FmF8yR0erf9Tkrxr7FXzSe%2FUWkfeB3aQPulP4U3nM7vJIz9DBcJxtdozfqHchZ%2FK%2BVnaW%2F7IlNhvu3Cwk%2BN3D9sUwf%2FuHQuE%2FQSsYZ0fjUCnB1UgJMjho5Sd4CHLNoCFroNj71YtnpdXjUQAAAm0D5ITdM1U28%2B6%2FLU%2B%2BJw%2FUTMOefScVULkEyaojkyJK574Dc96zie3HtMN0ahALfOg5yn2z2zZpwqsLk9mpT23GD8AYR55RcvLHGIjJTubtuMINy7ZBgbZmisPDt5DvHVKj1wABAtM4mGTp5yBq4dGDAbvWetjgvfJXswhcmqE%2BlDj%2FkTPyAAAB5B9Wn5rfJ8LS81DIkZj6By39KZPYLoNSpR%2BVEZsLaSEo%2FHTMG43Q%2FgG%2FYjZMQHBEZwleE1H35P3EuDlWOriEQxveH7ihmHVSDfj8R%2B6xo%2F263QCLqtg9djSFPW7h6QRx5JBM%2BWABcmIZQrAvMDe1q7F8VOGRDMf8tW%2F7sySMFn9pQ7735kasw8iNsGPX9gVNcncSuh8hmgWGzwciUU%2FY5SYmQvl0Oc15G5%2FkFhIA9nDVfZR4sMBRB9ApYbnNYsxtH12wWhTo04hPEGfzsqKK10muLy%2Bqpo3VBhX24HPTBAvYm68f0UVD%2Ba0cZWmgYKypmMqApJ87RnPvXbE3rmKepJM8u02O4X1OBlfDZBrTsbCbMxeniS6bzE6VPE62jOW6GIuyV6%2BNQS3PZTuTWG%2Fp7T5n2EC%2FPf%2FCvGLq41gQDU9VT2aCbHkbr9C0klVJfUwqdE%2F51zLmcY8wpx3P%2BOS%2BlrIjxQzOpWSKQfsNyt1DhKpKb5Y1wWrUGm6s0sBEG7FQK2SmWMhpjB36ZRdmtQ8%2Fmvh20KELR6W%2BocGosR20TXdGINzJEnobbTkkGNz2sqzePvL7Ql5Utc%2FGCaZYC2yIvJEGBOSBVtKvwqTOaMOFTaCIx4R5f3X17umkMD1YCvir39cREkU%3D"
|
||||
|
||||
|
||||
Example Response
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
@ -747,7 +750,7 @@ Example Response
|
|||
}
|
||||
]
|
||||
},
|
||||
"version": "6.0.0"
|
||||
"version": "6.1.0"
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class Economics:
|
|||
|
||||
_default_min_authorization = TToken(40_000, 'T').to_units()
|
||||
_default_min_operator_seconds = 60 * 60 * 24 # one day in seconds
|
||||
_default_fee_rate = Wei(Web3.toWei(1, 'gwei'))
|
||||
_default_fee_rate = Wei(Web3.to_wei(1, 'gwei'))
|
||||
|
||||
# TODO: Reintroduce Adjudicator
|
||||
# Slashing parameters
|
||||
|
|
|
@ -110,7 +110,7 @@ class BaseActor:
|
|||
"""Return this actor's current ETH balance"""
|
||||
blockchain = BlockchainInterfaceFactory.get_interface() # TODO: EthAgent? #1509
|
||||
balance = blockchain.client.get_balance(self.wallet_address)
|
||||
return Web3.fromWei(balance, 'ether')
|
||||
return Web3.from_wei(balance, 'ether')
|
||||
|
||||
@property
|
||||
def wallet_address(self):
|
||||
|
@ -362,7 +362,7 @@ class Operator(BaseActor):
|
|||
ether_balance = client.get_balance(self.operator_address)
|
||||
if ether_balance:
|
||||
# funds found
|
||||
funded, balance = True, Web3.fromWei(ether_balance, 'ether')
|
||||
funded, balance = True, Web3.from_wei(ether_balance, 'ether')
|
||||
emitter.message(f"✓ Operator {self.operator_address} is funded with {balance} ETH", color='green')
|
||||
else:
|
||||
emitter.message(f"! Operator {self.operator_address} is not funded with ETH", color="yellow")
|
||||
|
|
|
@ -227,7 +227,7 @@ class EthereumClient:
|
|||
|
||||
@property
|
||||
def is_connected(self):
|
||||
return self.w3.isConnected()
|
||||
return self.w3.is_connected()
|
||||
|
||||
@property
|
||||
def etherbase(self) -> str:
|
||||
|
@ -238,7 +238,7 @@ class EthereumClient:
|
|||
return self.w3.eth.accounts
|
||||
|
||||
def get_balance(self, account):
|
||||
return self.w3.eth.getBalance(account)
|
||||
return self.w3.eth.get_balance(account)
|
||||
|
||||
def inject_middleware(self, middleware, **kwargs):
|
||||
self.w3.middleware_onion.inject(middleware, **kwargs)
|
||||
|
@ -247,16 +247,16 @@ class EthereumClient:
|
|||
self.w3.middleware_onion.add(middleware)
|
||||
|
||||
def set_gas_strategy(self, gas_strategy):
|
||||
self.w3.eth.setGasPriceStrategy(gas_strategy)
|
||||
self.w3.eth.set_gas_price_strategy(gas_strategy)
|
||||
|
||||
@property
|
||||
def chain_id(self) -> int:
|
||||
try:
|
||||
# from hex-str
|
||||
return int(self.w3.eth.chainId, 16)
|
||||
return int(self.w3.eth.chain_id, 16)
|
||||
except TypeError:
|
||||
# from str
|
||||
return int(self.w3.eth.chainId)
|
||||
return int(self.w3.eth.chain_id)
|
||||
|
||||
@property
|
||||
def net_version(self) -> int:
|
||||
|
@ -270,18 +270,18 @@ class EthereumClient:
|
|||
"""
|
||||
Returns client's gas price. Underneath, it uses the eth_gasPrice JSON-RPC method
|
||||
"""
|
||||
return self.w3.eth.gasPrice
|
||||
return self.w3.eth.gas_price
|
||||
|
||||
def gas_price_for_transaction(self, transaction=None) -> Wei:
|
||||
"""
|
||||
Obtains a gas price via the current gas strategy, if any; otherwise, it resorts to the client's gas price.
|
||||
This method mirrors the behavior of web3._utils.transactions when building transactions.
|
||||
"""
|
||||
return self.w3.eth.generateGasPrice(transaction) or self.gas_price
|
||||
return self.w3.eth.generate_gas_price(transaction) or self.gas_price
|
||||
|
||||
@property
|
||||
def block_number(self) -> BlockNumber:
|
||||
return self.w3.eth.blockNumber
|
||||
return self.w3.eth.block_number
|
||||
|
||||
@property
|
||||
def coinbase(self) -> ChecksumAddress:
|
||||
|
@ -310,7 +310,7 @@ class EthereumClient:
|
|||
else:
|
||||
# If not asking for confirmations, just use web3 and assume the returned receipt is final
|
||||
try:
|
||||
receipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash,
|
||||
receipt = self.w3.eth.wait_for_transaction_receipt(transaction_hash=transaction_hash,
|
||||
timeout=timeout,
|
||||
poll_latency=self.TRANSACTION_POLLING_TIME)
|
||||
except TimeExhausted:
|
||||
|
@ -320,9 +320,11 @@ class EthereumClient:
|
|||
|
||||
def block_until_enough_confirmations(self, transaction_hash: str, timeout: float, confirmations: int) -> dict:
|
||||
|
||||
receipt: TxReceipt = self.w3.eth.waitForTransactionReceipt(transaction_hash=transaction_hash,
|
||||
timeout=timeout,
|
||||
poll_latency=self.TRANSACTION_POLLING_TIME)
|
||||
receipt: TxReceipt = self.w3.eth.wait_for_transaction_receipt(
|
||||
transaction_hash=transaction_hash,
|
||||
timeout=timeout,
|
||||
poll_latency=self.TRANSACTION_POLLING_TIME
|
||||
)
|
||||
|
||||
preliminary_block_hash = Web3.toHex(receipt['blockHash'])
|
||||
tx_block_number = Web3.toInt(receipt['blockNumber'])
|
||||
|
@ -348,7 +350,7 @@ class EthereumClient:
|
|||
def check_transaction_is_on_chain(self, receipt: TxReceipt) -> bool:
|
||||
transaction_hash = Web3.toHex(receipt['transactionHash'])
|
||||
try:
|
||||
new_receipt = self.w3.eth.getTransactionReceipt(transaction_hash)
|
||||
new_receipt = self.w3.eth.get_transaction_receipt(transaction_hash)
|
||||
except TransactionNotFound:
|
||||
reorg_detected = True
|
||||
else:
|
||||
|
@ -365,20 +367,20 @@ class EthereumClient:
|
|||
raise NotImplementedError
|
||||
|
||||
def get_transaction(self, transaction_hash) -> dict:
|
||||
return self.w3.eth.getTransaction(transaction_hash)
|
||||
return self.w3.eth.get_transaction(transaction_hash)
|
||||
|
||||
def get_transaction_receipt(self, transaction_hash) -> Union[dict, None]:
|
||||
return self.w3.eth.getTransactionReceipt(transaction_hash)
|
||||
return self.w3.eth.get_transaction_receipt(transaction_hash)
|
||||
|
||||
def get_transaction_count(self, account: str, pending: bool) -> int:
|
||||
block_identifier = 'pending' if pending else 'latest'
|
||||
return self.w3.eth.getTransactionCount(account, block_identifier)
|
||||
return self.w3.eth.get_transaction_count(account, block_identifier)
|
||||
|
||||
def send_transaction(self, transaction_dict: dict) -> str:
|
||||
return self.w3.eth.sendTransaction(transaction_dict)
|
||||
return self.w3.eth.send_transaction(transaction_dict)
|
||||
|
||||
def send_raw_transaction(self, transaction_bytes: bytes) -> str:
|
||||
return self.w3.eth.sendRawTransaction(transaction_bytes)
|
||||
return self.w3.eth.send_raw_transaction(transaction_bytes)
|
||||
|
||||
def sign_message(self, account: str, message: bytes) -> str:
|
||||
"""
|
||||
|
@ -389,12 +391,12 @@ class EthereumClient:
|
|||
return self.w3.eth.sign(account, data=message)
|
||||
|
||||
def get_blocktime(self):
|
||||
highest_block = self.w3.eth.getBlock('latest')
|
||||
highest_block = self.w3.eth.get_block('latest')
|
||||
now = highest_block['timestamp']
|
||||
return now
|
||||
|
||||
def get_block(self, block_identifier):
|
||||
return self.w3.eth.getBlock(block_identifier)
|
||||
return self.w3.eth.get_block(block_identifier)
|
||||
|
||||
def _has_latest_block(self) -> bool:
|
||||
# TODO: Investigate using `web3.middleware.make_stalecheck_middleware` #2060
|
||||
|
@ -457,7 +459,7 @@ class GethClient(EthereumClient):
|
|||
transaction_dict = dissoc(transaction_dict, 'to')
|
||||
|
||||
# Sign
|
||||
result = self.w3.eth.signTransaction(transaction_dict)
|
||||
result = self.w3.eth.sign_transaction(transaction_dict)
|
||||
|
||||
# Return RLP bytes
|
||||
rlp_encoded_transaction = result.raw
|
||||
|
|
|
@ -34,7 +34,7 @@ class EventRecord:
|
|||
except BlockchainInterfaceFactory.NoRegisteredInterfaces:
|
||||
self.timestamp = None
|
||||
else:
|
||||
self.timestamp = blockchain.client.w3.eth.getBlock(self.block_number)['timestamp']
|
||||
self.timestamp = blockchain.client.w3.eth.get_block(self.block_number)['timestamp']
|
||||
|
||||
def __repr__(self):
|
||||
pairs_to_show = dict(self.args.items())
|
||||
|
|
|
@ -308,7 +308,7 @@ class BlockchainInterface:
|
|||
configuration_message = f"Using gas strategy '{reported_gas_strategy}'"
|
||||
|
||||
if self.max_gas_price:
|
||||
__price = Web3.toWei(self.max_gas_price, 'gwei') # from gwei to wei
|
||||
__price = Web3.to_wei(self.max_gas_price, 'gwei') # from gwei to wei
|
||||
gas_strategy = max_price_gas_strategy_wrapper(gas_strategy=gas_strategy, max_gas_price_wei=__price)
|
||||
configuration_message += f", with a max price of {self.max_gas_price} gwei."
|
||||
|
||||
|
@ -316,7 +316,7 @@ class BlockchainInterface:
|
|||
|
||||
# TODO: This line must not be called prior to establishing a connection
|
||||
# Move it down to a lower layer, near the client.
|
||||
# gwei_gas_price = Web3.fromWei(self.client.gas_price_for_transaction(), 'gwei')
|
||||
# gwei_gas_price = Web3.from_wei(self.client.gas_price_for_transaction(), 'gwei')
|
||||
|
||||
self.log.info(configuration_message)
|
||||
# self.log.debug(f"Gas strategy currently reports a gas price of {gwei_gas_price} gwei.")
|
||||
|
@ -505,11 +505,11 @@ class BlockchainInterface:
|
|||
self.__log_transaction(transaction_dict=payload, contract_function=contract_function)
|
||||
try:
|
||||
if 'gas' not in payload: # i.e., transaction_gas_limit is not None
|
||||
# As web3 buildTransaction() will estimate gas with block identifier "pending" by default,
|
||||
# As web3 build_transaction() will estimate gas with block identifier "pending" by default,
|
||||
# explicitly estimate gas here with block identifier 'latest' if not otherwise specified
|
||||
# as a pending transaction can cause gas estimation to fail, notably in case of worklock refunds.
|
||||
payload['gas'] = contract_function.estimateGas(payload, block_identifier='latest')
|
||||
transaction_dict = contract_function.buildTransaction(payload)
|
||||
payload['gas'] = contract_function.estimate_gas(payload, block_identifier='latest')
|
||||
transaction_dict = contract_function.build_transaction(payload)
|
||||
except (TestTransactionFailed, ValidationError, ValueError) as error:
|
||||
# Note: Geth (1.9.15) raises ValueError in the same condition that pyevm raises ValidationError here.
|
||||
# Treat this condition as "Transaction Failed" during gas estimation.
|
||||
|
@ -570,9 +570,9 @@ class BlockchainInterface:
|
|||
max_unit_price = transaction_dict['gasPrice']
|
||||
tx_type = 'Legacy'
|
||||
|
||||
max_price_gwei = Web3.fromWei(max_unit_price, 'gwei')
|
||||
max_price_gwei = Web3.from_wei(max_unit_price, 'gwei')
|
||||
max_cost_wei = max_unit_price * transaction_dict['gas']
|
||||
max_cost = Web3.fromWei(max_cost_wei, 'ether')
|
||||
max_cost = Web3.from_wei(max_cost_wei, 'ether')
|
||||
|
||||
if transacting_power.is_device:
|
||||
emitter.message(f'Confirm transaction {transaction_name} on hardware wallet... '
|
||||
|
|
|
@ -128,11 +128,11 @@ def prettify_eth_amount(amount, original_denomination: str = 'wei') -> str:
|
|||
"""
|
||||
try:
|
||||
# First obtain canonical representation in wei. Works for int, float, Decimal and str amounts
|
||||
amount_in_wei = Web3.toWei(Decimal(amount), original_denomination)
|
||||
amount_in_wei = Web3.to_wei(Decimal(amount), original_denomination)
|
||||
|
||||
common_denominations = ('wei', 'gwei', 'ether')
|
||||
|
||||
options = [str(Web3.fromWei(amount_in_wei, d)) for d in common_denominations]
|
||||
options = [str(Web3.from_wei(amount_in_wei, d)) for d in common_denominations]
|
||||
|
||||
best_option = min(zip(map(len, options), options, common_denominations))
|
||||
_length, pretty_amount, denomination = best_option
|
||||
|
|
|
@ -24,16 +24,27 @@ from http import HTTPStatus
|
|||
from json.decoder import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Dict, Iterable, List, NamedTuple, Tuple, Union, Optional, Sequence, Set, Any
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
NamedTuple,
|
||||
Optional,
|
||||
Sequence,
|
||||
Set,
|
||||
Tuple,
|
||||
Union,
|
||||
)
|
||||
|
||||
import maya
|
||||
from constant_sorrow import constants
|
||||
from constant_sorrow.constants import (
|
||||
PUBLIC_ONLY,
|
||||
STRANGER_ALICE,
|
||||
READY,
|
||||
INVALIDATED,
|
||||
NOT_SIGNED
|
||||
NOT_SIGNED,
|
||||
PUBLIC_ONLY,
|
||||
READY,
|
||||
STRANGER_ALICE,
|
||||
)
|
||||
from cryptography.hazmat.primitives.serialization import Encoding
|
||||
from cryptography.x509 import Certificate, NameOID
|
||||
|
@ -41,35 +52,42 @@ from eth_typing.evm import ChecksumAddress
|
|||
from eth_utils import to_checksum_address
|
||||
from flask import Response, request
|
||||
from nucypher_core import (
|
||||
MessageKit,
|
||||
Address,
|
||||
HRAC,
|
||||
Conditions,
|
||||
EncryptedKeyFrag,
|
||||
TreasureMap,
|
||||
EncryptedTreasureMap,
|
||||
ReencryptionResponse,
|
||||
MessageKit,
|
||||
NodeMetadata,
|
||||
NodeMetadataPayload,
|
||||
HRAC,
|
||||
)
|
||||
from nucypher_core.umbral import (
|
||||
PublicKey, VerifiedKeyFrag, reencrypt,
|
||||
|
||||
ReencryptionResponse,
|
||||
TreasureMap,
|
||||
)
|
||||
from nucypher_core.umbral import PublicKey, VerifiedKeyFrag, reencrypt
|
||||
from twisted.internet import reactor, stdio
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.logger import Logger
|
||||
from web3.types import TxReceipt
|
||||
|
||||
import nucypher
|
||||
from nucypher.acumen.nicknames import Nickname
|
||||
from nucypher.acumen.perception import ArchivedFleetState, RemoteUrsulaStatus
|
||||
from nucypher.blockchain.eth.actors import Operator, BlockchainPolicyAuthor
|
||||
from nucypher.blockchain.eth.actors import BlockchainPolicyAuthor, Operator
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.characters.banners import ALICE_BANNER, BOB_BANNER, ENRICO_BANNER, URSULA_BANNER
|
||||
from nucypher.characters.banners import (
|
||||
ALICE_BANNER,
|
||||
BOB_BANNER,
|
||||
ENRICO_BANNER,
|
||||
URSULA_BANNER,
|
||||
)
|
||||
from nucypher.characters.base import Character, Learner
|
||||
from nucypher.characters.control.interfaces import AliceInterface, BobInterface, EnricoInterface
|
||||
from nucypher.characters.control.interfaces import (
|
||||
AliceInterface,
|
||||
BobInterface,
|
||||
EnricoInterface,
|
||||
)
|
||||
from nucypher.cli.processes import UrsulaCommandProtocol
|
||||
from nucypher.config.storages import NodeStorage
|
||||
from nucypher.control.controllers import WebController
|
||||
|
@ -80,19 +98,19 @@ from nucypher.crypto.powers import (
|
|||
DelegatingPower,
|
||||
PowerUpError,
|
||||
SigningPower,
|
||||
TransactingPower,
|
||||
TLSHostingPower,
|
||||
TransactingPower,
|
||||
)
|
||||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.network.nodes import NodeSprout, TEACHER_NODES, Teacher
|
||||
from nucypher.network.nodes import TEACHER_NODES, NodeSprout, Teacher
|
||||
from nucypher.network.protocols import parse_node_uri
|
||||
from nucypher.network.retrieval import RetrievalClient
|
||||
from nucypher.network.server import ProxyRESTServer, make_rest_app
|
||||
from nucypher.network.trackers import AvailabilityTracker, OperatorBondedTracker
|
||||
from nucypher.policy.kits import PolicyMessageKit
|
||||
from nucypher.policy.payment import PaymentMethod, FreeReencryptions
|
||||
from nucypher.policy.policies import Policy, BlockchainPolicy, FederatedPolicy
|
||||
from nucypher.policy.payment import FreeReencryptions, PaymentMethod
|
||||
from nucypher.policy.policies import BlockchainPolicy, FederatedPolicy, Policy
|
||||
from nucypher.utilities.logging import Logger
|
||||
from nucypher.utilities.networking import validate_operator_ip
|
||||
|
||||
|
@ -553,9 +571,10 @@ class Bob(Character):
|
|||
def retrieve(
|
||||
self,
|
||||
message_kits: Sequence[Union[MessageKit, PolicyMessageKit]],
|
||||
alice_verifying_key: PublicKey, # KeyFrag signer's key
|
||||
alice_verifying_key: PublicKey, # KeyFrag signer's key
|
||||
encrypted_treasure_map: EncryptedTreasureMap,
|
||||
publisher_verifying_key: Optional[PublicKey] = None,
|
||||
**context, # TODO: dont use one context to rule them all
|
||||
) -> List[PolicyMessageKit]:
|
||||
"""
|
||||
Attempts to retrieve reencrypted capsule fragments
|
||||
|
@ -581,7 +600,7 @@ class Bob(Character):
|
|||
treasure_map = self._treasure_maps[map_hash]
|
||||
else:
|
||||
# Have to decrypt the treasure map first to find out what the threshold is.
|
||||
# Otherwise we could check the message kits for completeness right away.
|
||||
# Otherwise, we could check the message kits for completeness right away.
|
||||
treasure_map = self._decrypt_treasure_map(encrypted_treasure_map, publisher_verifying_key)
|
||||
self._treasure_maps[map_hash] = treasure_map
|
||||
|
||||
|
@ -597,12 +616,14 @@ class Bob(Character):
|
|||
|
||||
# Retrieve capsule frags
|
||||
client = RetrievalClient(learner=self)
|
||||
retrieval_results = client.retrieve_cfrags(
|
||||
retrieval_results, _ = client.retrieve_cfrags(
|
||||
treasure_map=treasure_map,
|
||||
retrieval_kits=retrieval_kits,
|
||||
alice_verifying_key=alice_verifying_key,
|
||||
bob_encrypting_key=self.public_keys(DecryptingPower),
|
||||
bob_verifying_key=self.stamp.as_umbral_pubkey())
|
||||
bob_verifying_key=self.stamp.as_umbral_pubkey(),
|
||||
**context
|
||||
)
|
||||
|
||||
# Refill message kits with newly retrieved capsule frags
|
||||
results = []
|
||||
|
@ -952,6 +973,7 @@ class Ursula(Teacher, Character, Operator):
|
|||
if prometheus_config:
|
||||
# Locally scoped to prevent import without prometheus explicitly installed
|
||||
from nucypher.utilities.prometheus.metrics import start_prometheus_exporter
|
||||
|
||||
start_prometheus_exporter(ursula=self, prometheus_config=prometheus_config)
|
||||
if emitter:
|
||||
emitter.message(f"✓ Prometheus Exporter", color='green')
|
||||
|
@ -1045,7 +1067,7 @@ class Ursula(Teacher, Character, Operator):
|
|||
operator_signature = None
|
||||
else:
|
||||
operator_signature = self.operator_signature
|
||||
payload = NodeMetadataPayload(staking_provider_address=self.canonical_address,
|
||||
payload = NodeMetadataPayload(staking_provider_address=Address(self.canonical_address),
|
||||
domain=self.domain,
|
||||
timestamp_epoch=timestamp.epoch,
|
||||
operator_signature=operator_signature,
|
||||
|
@ -1235,10 +1257,8 @@ class Ursula(Teacher, Character, Operator):
|
|||
cfrag = reencrypt(capsule, kfrag)
|
||||
cfrags.append(cfrag)
|
||||
self.log.info(f"Re-encrypted capsule {capsule} -> made {cfrag}.")
|
||||
|
||||
return ReencryptionResponse(signer=self.stamp.as_umbral_signer(),
|
||||
capsules=capsules,
|
||||
vcfrags=cfrags)
|
||||
results = list(zip(capsules, cfrags))
|
||||
return ReencryptionResponse(signer=self.stamp.as_umbral_signer(), capsules_and_vcfrags=results)
|
||||
|
||||
def status_info(self, omit_known_nodes: bool = False) -> 'LocalUrsulaStatus':
|
||||
|
||||
|
@ -1331,10 +1351,12 @@ class Enrico(Character):
|
|||
if is_me:
|
||||
self.log.info(self.banner.format(policy_encrypting_key))
|
||||
|
||||
def encrypt_message(self, plaintext: bytes) -> MessageKit:
|
||||
def encrypt_message(self, plaintext: bytes, conditions: Optional[Dict[str, Union[str, int]]] = None) -> MessageKit:
|
||||
# TODO: #2107 Rename to "encrypt"
|
||||
conditions = Conditions(json.dumps(conditions or list()))
|
||||
message_kit = MessageKit(policy_encrypting_key=self.policy_pubkey,
|
||||
plaintext=plaintext)
|
||||
plaintext=plaintext,
|
||||
conditions=conditions)
|
||||
return message_kit
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -117,7 +117,7 @@ def collect_policy_rate_and_value(alice: Alice, rate: int, value: int, shares: i
|
|||
rate = alice.payment_method.rate # wei
|
||||
|
||||
if not force:
|
||||
default_gwei = Web3.fromWei(rate, 'gwei') # wei -> gwei
|
||||
default_gwei = Web3.from_wei(rate, 'gwei') # wei -> gwei
|
||||
prompt = "Confirm rate of {node_rate} gwei * {shares} nodes ({period_rate} gwei per period)?"
|
||||
|
||||
if not click.confirm(prompt.format(node_rate=default_gwei, period_rate=default_gwei * shares, shares=shares), default=True):
|
||||
|
@ -125,7 +125,7 @@ def collect_policy_rate_and_value(alice: Alice, rate: int, value: int, shares: i
|
|||
# TODO: Interactive rate sampling & validation (#1709)
|
||||
interactive_prompt = prompt.format(node_rate=interactive_rate, period_rate=interactive_rate * shares, shares=shares)
|
||||
click.confirm(interactive_prompt, default=True, abort=True)
|
||||
rate = Web3.toWei(interactive_rate, 'gwei') # gwei -> wei
|
||||
rate = Web3.to_wei(interactive_rate, 'gwei') # gwei -> wei
|
||||
|
||||
return rate, value
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ def confirm_staged_grant(emitter, grant_request: Dict, federated: bool, seconds_
|
|||
emitter.echo(tabulate(table, tablefmt="simple"))
|
||||
return
|
||||
|
||||
period_rate = Web3.fromWei(pretty_request['shares'] * pretty_request['rate'], 'gwei')
|
||||
period_rate = Web3.from_wei(pretty_request['shares'] * pretty_request['rate'], 'gwei')
|
||||
pretty_request['rate'] = f"{pretty_request['rate']} wei/period * {pretty_request['shares']} nodes"
|
||||
|
||||
expiration = pretty_request['expiration']
|
||||
|
|
|
@ -117,7 +117,7 @@ def select_client_account(emitter,
|
|||
is_staking = 'Yes' if bool(staker.stakes) else 'No'
|
||||
row.append(is_staking)
|
||||
if show_eth_balance:
|
||||
ether_balance = Web3.fromWei(blockchain.client.get_balance(account), 'ether')
|
||||
ether_balance = Web3.from_wei(blockchain.client.get_balance(account), 'ether')
|
||||
row.append(f'{ether_balance} ETH')
|
||||
if show_nu_balance:
|
||||
token_agent = ContractAgency.get_agent(NucypherTokenAgent, registry=registry)
|
||||
|
|
|
@ -114,7 +114,7 @@ Registry ................ {registry.filepath}
|
|||
token_contract_info = f"""
|
||||
|
||||
{token_agent.contract_name} ........... {token_agent.contract_address}
|
||||
~ Ethers ............ {Web3.fromWei(blockchain.client.get_balance(token_agent.contract_address), 'ether')} ETH
|
||||
~ Ethers ............ {Web3.from_wei(blockchain.client.get_balance(token_agent.contract_address), 'ether')} ETH
|
||||
~ Tokens ............ {NU.from_units(token_agent.get_balance(token_agent.contract_address))}"""
|
||||
except BaseContractRegistry.UnknownContract:
|
||||
message = f"\n{NucypherTokenAgent.contract_name} is not enrolled in {registry.filepath}"
|
||||
|
@ -146,12 +146,12 @@ Registry ................ {registry.filepath}
|
|||
{agent.contract_name} .... {bare_contract.address}
|
||||
~ Version ............ {bare_contract.version}
|
||||
~ Owner .............. {bare_contract.functions.owner().call()}
|
||||
~ Ethers ............. {Web3.fromWei(blockchain.client.get_balance(bare_contract.address), 'ether')} ETH
|
||||
~ Ethers ............. {Web3.from_wei(blockchain.client.get_balance(bare_contract.address), 'ether')} ETH
|
||||
~ Tokens ............. {NU.from_units(token_agent.get_balance(bare_contract.address))}
|
||||
~ Dispatcher ......... {dispatcher_deployer.contract_address}
|
||||
~ Owner .......... {dispatcher_deployer.contract.functions.owner().call()}
|
||||
~ Target ......... {dispatcher_deployer.contract.functions.target().call()}
|
||||
~ Ethers ......... {Web3.fromWei(blockchain.client.get_balance(dispatcher_deployer.contract_address), 'ether')} ETH
|
||||
~ Ethers ......... {Web3.from_wei(blockchain.client.get_balance(dispatcher_deployer.contract_address), 'ether')} ETH
|
||||
~ Tokens ......... {NU.from_units(token_agent.get_balance(dispatcher_deployer.contract_address))}"""
|
||||
emitter.echo(proxy_payload)
|
||||
emitter.echo(sep, nl=False)
|
||||
|
|
|
@ -35,7 +35,7 @@ def paint_contract_status(registry, emitter):
|
|||
|
||||
blockchain = f"""
|
||||
| '{blockchain.client.chain_name}' Blockchain Network |
|
||||
Gas Price ................ {Web3.fromWei(blockchain.client.gas_price, 'gwei')} Gwei
|
||||
Gas Price ................ {Web3.from_wei(blockchain.client.gas_price, 'gwei')} Gwei
|
||||
ETH Provider URI ......... {blockchain.eth_provider_uri}
|
||||
Registry ................. {registry.filepath}
|
||||
"""
|
||||
|
|
|
@ -28,9 +28,9 @@ from nucypher_core.umbral import PublicKey
|
|||
from nucypher.blockchain.economics import Economics
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterface
|
||||
from nucypher.blockchain.eth.networks import NetworksInventory
|
||||
from nucypher.blockchain.eth.token import NU, TToken
|
||||
from nucypher.blockchain.eth.token import TToken
|
||||
from nucypher.policy.payment import PAYMENT_METHODS
|
||||
from nucypher.utilities.networking import InvalidOperatorIP, validate_operator_ip
|
||||
from nucypher.utilities.networking import validate_operator_ip, InvalidOperatorIP
|
||||
|
||||
|
||||
class ChecksumAddress(click.ParamType):
|
||||
|
|
|
@ -74,4 +74,4 @@ TEMPORARY_DOMAIN = ":temporary-domain:" # for use with `--dev` node runtimes
|
|||
NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS = 'NUCYPHER_EVENTS_THROTTLE_MAX_BLOCKS'
|
||||
|
||||
# Probationary period
|
||||
END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601("2022-08-31T23:59:59.0Z")
|
||||
END_OF_POLICIES_PROBATIONARY_PERIOD = MayaDT.from_iso8601('2023-4-20T23:59:59.0Z')
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from base64 import b64encode, b64decode
|
||||
import json
|
||||
from base64 import b64decode, b64encode
|
||||
|
||||
import click
|
||||
from marshmallow import fields
|
||||
|
@ -71,11 +72,49 @@ class PositiveInteger(Integer):
|
|||
class Base64BytesRepresentation(BaseField, fields.Field):
|
||||
"""Serializes/Deserializes any object's byte representation to/from bae64."""
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
value_bytes = value if isinstance(value, bytes) else bytes(value)
|
||||
return b64encode(value_bytes).decode()
|
||||
try:
|
||||
value_bytes = value if isinstance(value, bytes) else bytes(value)
|
||||
return b64encode(value_bytes).decode()
|
||||
except Exception as e:
|
||||
raise InvalidInputData(
|
||||
f"Provided object type, {type(value)}, is not serializable: {e}"
|
||||
)
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
try:
|
||||
return b64decode(value)
|
||||
except ValueError as e:
|
||||
raise InvalidInputData(f"Could not parse {self.name}: {e}")
|
||||
|
||||
|
||||
class JSON(BaseField, fields.Field):
|
||||
"""Serializes/Deserializes objects to/from JSON strings."""
|
||||
def __init__(self, expected_type=None, *args, **kwargs):
|
||||
# enforce type-safety (TODO too strict?)
|
||||
self.expected_type = expected_type
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
if self.expected_type and (type(value) != self.expected_type):
|
||||
raise InvalidInputData(
|
||||
f"Unexpected object type, {type(value)}; expected {self.expected_type}")
|
||||
|
||||
try:
|
||||
value_json = json.dumps(value)
|
||||
return value_json
|
||||
except Exception as e:
|
||||
raise InvalidInputData(
|
||||
f"Provided object type, {type(value)}, is not JSON serializable: {e}"
|
||||
)
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
try:
|
||||
result = json.loads(value)
|
||||
except Exception as e:
|
||||
raise InvalidInputData(f"Invalid JSON: {e}")
|
||||
else:
|
||||
if self.expected_type and (type(result) != self.expected_type):
|
||||
raise InvalidInputData(
|
||||
f"Unexpected object type, {type(result)}; expected {self.expected_type}")
|
||||
|
||||
return result
|
||||
|
|
|
@ -14,34 +14,41 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import random
|
||||
from typing import Dict, Sequence, List
|
||||
from collections import defaultdict
|
||||
from typing import Dict, List, Optional, Sequence, Tuple, Union
|
||||
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
from twisted.logger import Logger
|
||||
|
||||
from nucypher_core import (
|
||||
TreasureMap,
|
||||
Conditions,
|
||||
Context,
|
||||
ReencryptionResponse,
|
||||
ReencryptionRequest,
|
||||
RetrievalKit,
|
||||
)
|
||||
TreasureMap,
|
||||
)
|
||||
from nucypher_core.umbral import (
|
||||
Capsule,
|
||||
PublicKey,
|
||||
VerifiedCapsuleFrag,
|
||||
VerificationError,
|
||||
VerifiedCapsuleFrag,
|
||||
)
|
||||
from twisted.logger import Logger
|
||||
|
||||
from nucypher.crypto.signing import InvalidSignature
|
||||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
from nucypher.policy.kits import RetrievalResult
|
||||
|
||||
|
||||
class RetrievalError:
|
||||
def __init__(self, errors: Dict[ChecksumAddress, str]):
|
||||
self.errors = errors
|
||||
|
||||
|
||||
class RetrievalPlan:
|
||||
"""
|
||||
An emphemeral object providing a service of selecting Ursulas for reencryption requests
|
||||
|
@ -51,13 +58,23 @@ class RetrievalPlan:
|
|||
def __init__(self, treasure_map: TreasureMap, retrieval_kits: Sequence[RetrievalKit]):
|
||||
|
||||
# Record the retrieval kits order
|
||||
self._capsules = [retrieval_kit.capsule for retrieval_kit in retrieval_kits]
|
||||
self._capsules, rust_conditions = tuple(zip(*((rk.capsule, rk.conditions) for rk in retrieval_kits)))
|
||||
|
||||
# Transform Conditions -> ConditionsLingo
|
||||
json_conditions = (json.loads(str(c)) for c in rust_conditions)
|
||||
self._conditions = list(ConditionLingo.from_list(lingo) if lingo else None for lingo in json_conditions)
|
||||
|
||||
self._threshold = treasure_map.threshold
|
||||
|
||||
# Records the retrieval results, indexed by capsule
|
||||
self._results = {retrieval_kit.capsule: {}
|
||||
for retrieval_kit in retrieval_kits} # {capsule: {ursula_address: cfrag}}
|
||||
self._results = {
|
||||
retrieval_kit.capsule: {} for retrieval_kit in retrieval_kits
|
||||
} # {capsule: {ursula_address: cfrag}}
|
||||
|
||||
# Records the retrieval result errors, indexed by capsule
|
||||
self._errors = {
|
||||
retrieval_kit.capsule: {} for retrieval_kit in retrieval_kits
|
||||
} # {capsule: {ursula_address: error}}
|
||||
|
||||
# Records the addresses of Ursulas that were already queried, indexed by capsule.
|
||||
self._queried_addresses = {retrieval_kit.capsule: set(retrieval_kit.queried_addresses)
|
||||
|
@ -94,12 +111,14 @@ class RetrievalPlan:
|
|||
# Only request reencryption for capsules that:
|
||||
# - haven't been processed by this Ursula
|
||||
# - don't already have cfrags from `threshold` Ursulas
|
||||
capsules = [capsule for capsule in self._capsules
|
||||
packets = [(capsule, lingo) for capsule, lingo in zip(self._capsules, self._conditions)
|
||||
if (capsule not in self._processed_capsules.get(ursula_address, set())
|
||||
and len(self._queried_addresses[capsule]) < self._threshold)]
|
||||
if len(capsules) > 0:
|
||||
if len(packets) > 0:
|
||||
capsules, conditions = list(zip(*packets))
|
||||
return RetrievalWorkOrder(ursula_address=ursula_address,
|
||||
capsules=capsules)
|
||||
capsules=list(capsules),
|
||||
lingo=list(conditions))
|
||||
|
||||
# Execution will not reach this point if `is_complete()` returned `False` before this call.
|
||||
raise RuntimeError("No Ursulas left")
|
||||
|
@ -113,6 +132,13 @@ class RetrievalPlan:
|
|||
self._processed_capsules[work_order.ursula_address].add(capsule)
|
||||
self._results[capsule][work_order.ursula_address] = cfrag
|
||||
|
||||
def update_errors(self,
|
||||
work_order: "RetrievalWorkOrder",
|
||||
ursula_address: ChecksumAddress,
|
||||
error_message: str):
|
||||
for capsule in work_order.capsules:
|
||||
self._errors[capsule][ursula_address] = error_message
|
||||
|
||||
def is_complete(self) -> bool:
|
||||
return (
|
||||
# there are no more Ursulas to query
|
||||
|
@ -121,11 +147,22 @@ class RetrievalPlan:
|
|||
all(len(addresses) >= self._threshold for addresses in self._queried_addresses.values())
|
||||
)
|
||||
|
||||
def results(self) -> List['RetrievalResult']:
|
||||
# TODO (#1995): when that issue is fixed, conversion is no longer needed
|
||||
return [RetrievalResult({to_checksum_address(address): cfrag
|
||||
for address, cfrag in self._results[capsule].items()})
|
||||
for capsule in self._capsules]
|
||||
def results(self) -> Tuple[List["RetrievalResult"], List[RetrievalError]]:
|
||||
results = []
|
||||
errors = []
|
||||
# maintain the same order with both lists
|
||||
for capsule in self._capsules:
|
||||
results.append(
|
||||
RetrievalResult(
|
||||
{
|
||||
to_checksum_address(address): cfrag
|
||||
for address, cfrag in self._results[capsule].items()
|
||||
}
|
||||
)
|
||||
)
|
||||
errors.append(RetrievalError(errors=self._errors[capsule]))
|
||||
|
||||
return results, errors
|
||||
|
||||
|
||||
class RetrievalWorkOrder:
|
||||
|
@ -133,9 +170,19 @@ class RetrievalWorkOrder:
|
|||
A work order issued by a retrieval plan to request reencryption from an Ursula
|
||||
"""
|
||||
|
||||
def __init__(self, ursula_address: ChecksumAddress, capsules: List[Capsule]):
|
||||
def __init__(self,
|
||||
ursula_address: ChecksumAddress, capsules: List[Capsule],
|
||||
lingo: Optional[List[ConditionLingo]] = None
|
||||
):
|
||||
self.ursula_address = ursula_address
|
||||
self.capsules = capsules
|
||||
self.__lingo = lingo
|
||||
|
||||
def conditions(self, as_json=True) -> Union[str, List[ConditionLingo]]:
|
||||
lingo = self.__lingo or list()
|
||||
if as_json:
|
||||
return json.dumps([l.to_list() if l else None for l in lingo])
|
||||
return lingo
|
||||
|
||||
|
||||
class RetrievalClient:
|
||||
|
@ -214,7 +261,7 @@ class RetrievalClient:
|
|||
self.log.warn(message)
|
||||
raise RuntimeError(message) from e
|
||||
except middleware.UnexpectedResponse:
|
||||
raise # TODO: Handle this
|
||||
raise # TODO: Handle this
|
||||
|
||||
try:
|
||||
reencryption_response = ReencryptionResponse.from_bytes(response.content)
|
||||
|
@ -225,36 +272,37 @@ class RetrievalClient:
|
|||
|
||||
ursula_verifying_key = ursula.stamp.as_umbral_pubkey()
|
||||
|
||||
# FIXME: Uncomment
|
||||
try:
|
||||
verified_cfrags = reencryption_response.verify(capsules=reencryption_request.capsules,
|
||||
alice_verifying_key=alice_verifying_key,
|
||||
ursula_verifying_key=ursula_verifying_key,
|
||||
policy_encrypting_key=policy_encrypting_key,
|
||||
bob_encrypting_key=bob_encrypting_key,
|
||||
)
|
||||
bob_encrypting_key=bob_encrypting_key)
|
||||
except InvalidSignature as e:
|
||||
self.log.warn(str(e))
|
||||
self.log.warn(f"Invalid signature for ReencryptionResponse: {e}")
|
||||
raise
|
||||
except VerificationError:
|
||||
except VerificationError as e:
|
||||
# In future we may want to remember this Ursula and do something about it
|
||||
self.log.warn("Failed to verify capsule frags in the ReencryptionResponse")
|
||||
self.log.warn(
|
||||
f"Failed to verify capsule frags in the ReencryptionResponse: {e}"
|
||||
)
|
||||
raise
|
||||
except Exception as e:
|
||||
message = f"Failed to verify the ReencryptionResponse: {e}"
|
||||
message = f"Failed to verify the ReencryptionResponse ({e.__class__.__name__}): {e}"
|
||||
self.log.warn(message)
|
||||
raise RuntimeError(message)
|
||||
|
||||
return {capsule: vcfrag for capsule, vcfrag
|
||||
in zip(reencryption_request.capsules, verified_cfrags)}
|
||||
|
||||
def retrieve_cfrags(
|
||||
self,
|
||||
treasure_map: TreasureMap,
|
||||
retrieval_kits: Sequence[RetrievalKit],
|
||||
alice_verifying_key: PublicKey, # KeyFrag signer's key
|
||||
bob_encrypting_key: PublicKey, # User's public key (reencryption target)
|
||||
bob_verifying_key: PublicKey,
|
||||
) -> List[RetrievalResult]:
|
||||
def retrieve_cfrags(self,
|
||||
treasure_map: TreasureMap,
|
||||
retrieval_kits: Sequence[RetrievalKit],
|
||||
alice_verifying_key: PublicKey, # KeyFrag signer's key
|
||||
bob_encrypting_key: PublicKey, # User's public key (reencryption target)
|
||||
bob_verifying_key: PublicKey,
|
||||
**context) -> Tuple[List[RetrievalResult], List[RetrievalError]]:
|
||||
|
||||
self._ensure_ursula_availability(treasure_map)
|
||||
|
||||
|
@ -273,12 +321,27 @@ class RetrievalClient:
|
|||
continue
|
||||
|
||||
ursula = self._learner.known_nodes[ursula_checksum_address]
|
||||
|
||||
# TODO: Move to a method and handle errors?
|
||||
# TODO: This serialization is rather low-level compared to the rest of this method.
|
||||
# nucypher-core consumes bytes only for conditions and context.
|
||||
condition_string = work_order.conditions(as_json=True) # '[[lingo], null, [lingo]]'
|
||||
request_context_string = json.dumps(context)
|
||||
|
||||
# TODO: As this pattern swells further, it makes sense to do this in a purpose-built facility,
|
||||
# such as a factory that makes helper classes and casts the appropriate types.
|
||||
rust_conditions = Conditions(condition_string)
|
||||
rust_context = Context(request_context_string)
|
||||
|
||||
reencryption_request = ReencryptionRequest(
|
||||
hrac=treasure_map.hrac,
|
||||
capsules=work_order.capsules,
|
||||
encrypted_kfrag=treasure_map.destinations[work_order.ursula_address],
|
||||
bob_verifying_key=bob_verifying_key,
|
||||
publisher_verifying_key=treasure_map.publisher_verifying_key)
|
||||
publisher_verifying_key=treasure_map.publisher_verifying_key,
|
||||
conditions=rust_conditions,
|
||||
context=rust_context
|
||||
)
|
||||
|
||||
try:
|
||||
cfrags = self._request_reencryption(ursula=ursula,
|
||||
|
@ -287,9 +350,13 @@ class RetrievalClient:
|
|||
policy_encrypting_key=treasure_map.policy_encrypting_key,
|
||||
bob_encrypting_key=bob_encrypting_key)
|
||||
except Exception as e:
|
||||
# TODO (#2789): at this point we can separate the exceptions to "acceptable"
|
||||
# (Ursula is not reachable) and "unacceptable" (Ursula provided bad results).
|
||||
self.log.warn(f"Ursula {ursula} failed to reencrypt: {e}")
|
||||
exception_message = f"{e.__class__.__name__}: {e}"
|
||||
retrieval_plan.update_errors(
|
||||
work_order, ursula_checksum_address, exception_message
|
||||
)
|
||||
self.log.warn(
|
||||
f"Ursula {ursula} failed to reencrypt; {exception_message}"
|
||||
)
|
||||
continue
|
||||
|
||||
retrieval_plan.update(work_order, cfrags)
|
||||
|
|
|
@ -14,8 +14,7 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import weakref
|
||||
from http import HTTPStatus
|
||||
|
@ -28,11 +27,11 @@ from flask import Flask, Response, jsonify, request
|
|||
from mako import exceptions as mako_exceptions
|
||||
from mako.template import Template
|
||||
from nucypher_core import (
|
||||
ReencryptionRequest,
|
||||
RevocationOrder,
|
||||
MetadataRequest,
|
||||
MetadataResponse,
|
||||
MetadataResponsePayload,
|
||||
ReencryptionRequest,
|
||||
RevocationOrder,
|
||||
)
|
||||
|
||||
from nucypher.config.constants import MAX_UPLOAD_CONTENT_LENGTH
|
||||
|
@ -43,6 +42,13 @@ from nucypher.datastore.models import ReencryptionRequest as ReencryptionRequest
|
|||
from nucypher.network.exceptions import NodeSeemsToBeDown
|
||||
from nucypher.network.nodes import NodeSprout
|
||||
from nucypher.network.protocols import InterfaceInfo
|
||||
from nucypher.policy.conditions.base import ReencryptionCondition
|
||||
from nucypher.policy.conditions.context import (
|
||||
ContextVariableVerificationFailed,
|
||||
InvalidContextVariableData,
|
||||
RequiredContextVariable,
|
||||
)
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
||||
HERE = BASE_DIR = Path(__file__).parent
|
||||
|
@ -168,12 +174,22 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask:
|
|||
from nucypher.characters.lawful import Bob
|
||||
|
||||
# TODO: Cache & Optimize
|
||||
|
||||
reenc_request = ReencryptionRequest.from_bytes(request.data)
|
||||
|
||||
json_lingo = json.loads(str(reenc_request.conditions))
|
||||
lingo = [ConditionLingo.from_list(lingo) if lingo else None for lingo in json_lingo]
|
||||
context = json.loads(str(reenc_request.context)) or dict() # requester-supplied input
|
||||
packets = zip(reenc_request.capsules, lingo)
|
||||
|
||||
# TODO: Detect if we are dealing with PRE or tDec here
|
||||
# TODO: This is for PRE only, relocate HRAC to RE.context
|
||||
hrac = reenc_request.hrac
|
||||
|
||||
# This is now either Bob or the TDec requester "Universal Bob"
|
||||
bob = Bob.from_public_keys(verifying_key=reenc_request.bob_verifying_key)
|
||||
log.info(f"Reencryption request from {bob} for policy {hrac}")
|
||||
|
||||
# TODO: Can this be integrated into reencryption conditions?
|
||||
# Right off the bat, if this HRAC is already known to be revoked, reject the order.
|
||||
if hrac in this_node.revoked_policies:
|
||||
return Response(response=f"Policy with {hrac} has been revoked.", status=HTTPStatus.UNAUTHORIZED)
|
||||
|
@ -188,9 +204,12 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask:
|
|||
# Verify & Decrypt KFrag Payload
|
||||
try:
|
||||
verified_kfrag = this_node._decrypt_kfrag(reenc_request.encrypted_kfrag, hrac, publisher_verifying_key)
|
||||
except DecryptingKeypair.DecryptionFailed:
|
||||
except DecryptingKeypair.DecryptionFailed as e:
|
||||
# TODO: don't we want to record suspicious activities here too?
|
||||
return Response(response="EncryptedKeyFrag decryption failed.", status=HTTPStatus.FORBIDDEN)
|
||||
return Response(
|
||||
response=f"EncryptedKeyFrag decryption failed: {e}",
|
||||
status=HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
except InvalidSignature as e:
|
||||
message = f'{bob_identity_message} Invalid signature for KeyFrag: {e}.'
|
||||
log.info(message)
|
||||
|
@ -199,20 +218,79 @@ def _make_rest_app(datastore: Datastore, this_node, log: Logger) -> Flask:
|
|||
except Exception as e:
|
||||
message = f'{bob_identity_message} Invalid EncryptedKeyFrag: {e}.'
|
||||
log.info(message)
|
||||
# TODO (#567): bucket the node as suspicious
|
||||
# TODO (#567): bucket the node as suspicious.
|
||||
return Response(message, status=HTTPStatus.BAD_REQUEST)
|
||||
|
||||
# Enforce Policy Payment
|
||||
# TODO: Accept multiple payment methods
|
||||
# TODO: Evaluate multiple reencryption prerequisites & enforce policy expiration
|
||||
paid = this_node.payment_method.verify(payee=this_node.checksum_address, request=reenc_request)
|
||||
if not paid:
|
||||
message = f"{bob_identity_message} Policy {bytes(hrac)} is unpaid."
|
||||
return Response(message, status=HTTPStatus.PAYMENT_REQUIRED)
|
||||
# Enforce Reencryption Conditions
|
||||
# TODO: back compatibility for PRE?
|
||||
|
||||
if not this_node.federated_only:
|
||||
# TODO: Detect whether or not a provider is required by introspecting the condition instead.
|
||||
context.update({'provider': this_node.application_agent.blockchain.provider})
|
||||
|
||||
capsules_to_process = list()
|
||||
for capsule, lingo in packets:
|
||||
if lingo is not None:
|
||||
|
||||
# TODO: Enforce policy expiration as a condition
|
||||
try:
|
||||
# TODO: Can conditions return a useful value?
|
||||
log.info(f'Evaluating decryption condition')
|
||||
lingo.eval(**context)
|
||||
except ReencryptionCondition.InvalidCondition as e:
|
||||
message = f"Incorrect value provided for condition: {e}"
|
||||
error = (message, HTTPStatus.BAD_REQUEST)
|
||||
log.info(message)
|
||||
return Response(message, status=error[1])
|
||||
except RequiredContextVariable as e:
|
||||
message = f"Missing required inputs: {e}"
|
||||
# TODO: be more specific and name the missing inputs, etc
|
||||
error = (message, HTTPStatus.BAD_REQUEST)
|
||||
log.info(message)
|
||||
return Response(message, status=error[1])
|
||||
except InvalidContextVariableData as e:
|
||||
message = f"Invalid data provided for context variable: {e}"
|
||||
error = (message, HTTPStatus.BAD_REQUEST)
|
||||
log.info(message)
|
||||
return Response(message, status=error[1])
|
||||
except ContextVariableVerificationFailed as e:
|
||||
message = f"Context variable data could not be verified: {e}"
|
||||
error = (message, HTTPStatus.FORBIDDEN)
|
||||
log.info(message)
|
||||
return Response(message, status=error[1])
|
||||
except ReencryptionCondition.ConditionEvaluationFailed as e:
|
||||
message = f"Decryption condition not evaluated: {e}"
|
||||
error = (message, HTTPStatus.BAD_REQUEST)
|
||||
log.info(message)
|
||||
return Response(message, status=error[1])
|
||||
except lingo.Failed as e:
|
||||
# TODO: Better error reporting
|
||||
message = f"Decryption conditions not satisfied: {e}"
|
||||
error = (message, HTTPStatus.FORBIDDEN)
|
||||
log.info(message)
|
||||
return Response(message, status=error[1])
|
||||
except Exception as e:
|
||||
# TODO: Unsure why we ended up here
|
||||
message = f"Unexpected exception while evaluating " \
|
||||
f"decryption condition ({e.__class__.__name__}): {e}"
|
||||
error = (message, HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
log.warn(message)
|
||||
return Response(message, status=error[1])
|
||||
|
||||
capsules_to_process.append((lingo, capsule))
|
||||
capsules_to_process = tuple(p[1] for p in capsules_to_process)
|
||||
|
||||
# FIXME: DISABLED FOR TDEC ADAPTATION
|
||||
# TODO: Accept multiple payment methods?
|
||||
# Subscription Manager
|
||||
# paid = this_node.payment_method.verify(payee=this_node.checksum_address, request=reenc_request)
|
||||
# if not paid:
|
||||
# message = f"{bob_identity_message} Policy {bytes(hrac)} is unpaid."
|
||||
# return Response(message, status=HTTPStatus.PAYMENT_REQUIRED)
|
||||
|
||||
# Re-encrypt
|
||||
# TODO: return a sensible response if it fails (currently results in 500)
|
||||
response = this_node._reencrypt(kfrag=verified_kfrag, capsules=reenc_request.capsules)
|
||||
response = this_node._reencrypt(kfrag=verified_kfrag, capsules=capsules_to_process)
|
||||
|
||||
# Now, Ursula saves evidence of this workorder to her database...
|
||||
# Note: we give the work order a random ID to store it under.
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
STANDARD_ABIS_FILEPATH = Path(__file__).parent / 'abis.json'
|
||||
with open(STANDARD_ABIS_FILEPATH, 'r') as file:
|
||||
STANDARD_ABIS = json.loads(file.read())
|
||||
|
||||
STANDARD_ABI_CONTRACT_TYPES = set(STANDARD_ABIS)
|
|
@ -0,0 +1,68 @@
|
|||
import json
|
||||
from marshmallow import Schema, post_dump
|
||||
from typing import Union, Type, Dict
|
||||
|
||||
_ETH = 'eth_'
|
||||
|
||||
|
||||
def to_camelcase(s):
|
||||
parts = iter(s.split("_"))
|
||||
return next(parts) + "".join(i.title() for i in parts)
|
||||
|
||||
|
||||
class CamelCaseSchema(Schema):
|
||||
"""Schema that uses camel-case for its external representation
|
||||
and snake-case for its internal representation.
|
||||
"""
|
||||
|
||||
SKIP_VALUES = tuple()
|
||||
|
||||
def on_bind_field(self, field_name, field_obj):
|
||||
field_obj.data_key = to_camelcase(field_obj.data_key or field_name)
|
||||
|
||||
@post_dump
|
||||
def remove_skip_values(self, data, **kwargs):
|
||||
return {
|
||||
key: value for key, value in data.items()
|
||||
if value not in self.SKIP_VALUES
|
||||
}
|
||||
|
||||
|
||||
def _resolve_condition_lingo(json_data) -> Union[Type['Operator'], Type['ReencryptionCondition']]:
|
||||
"""
|
||||
TODO: This feels like a jenky way to resolve data types from JSON blobs, but it works.
|
||||
Inspects a given bloc of JSON and attempts to resolve it's intended datatype within the
|
||||
conditions expression framework.
|
||||
"""
|
||||
# TODO: This is ugly but avoids circular imports :-|
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
from nucypher.policy.conditions.evm import ContractCondition
|
||||
from nucypher.policy.conditions.evm import RPCCondition
|
||||
from nucypher.policy.conditions.lingo import Operator
|
||||
|
||||
# Inspect
|
||||
method = json_data.get('method')
|
||||
operator = json_data.get('operator')
|
||||
contract = json_data.get('contractAddress')
|
||||
|
||||
# Resolve
|
||||
if method:
|
||||
if method == TimeCondition.METHOD:
|
||||
return TimeCondition
|
||||
elif contract:
|
||||
return ContractCondition
|
||||
elif method.startswith(_ETH):
|
||||
return RPCCondition
|
||||
elif operator:
|
||||
return Operator
|
||||
else:
|
||||
raise Exception(f'Cannot resolve condition lingo type from data {json_data}')
|
||||
|
||||
|
||||
def _deserialize_condition_lingo(data: Union[str, Dict[str, str]]) -> Union['Operator', 'ReencryptionCondition']:
|
||||
"""Deserialization helper for condition lingo"""
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
lingo_class = _resolve_condition_lingo(json_data=data)
|
||||
instance = lingo_class.from_dict(data)
|
||||
return instance
|
|
@ -0,0 +1,871 @@
|
|||
{
|
||||
"ERC20": [
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint8"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "balance",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "_owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "_spender",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"payable": true,
|
||||
"stateMutability": "payable",
|
||||
"type": "fallback"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
}
|
||||
],
|
||||
"ERC721": [
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "mint",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "_data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setApprovalForAll",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": false,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"payable": false,
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "approved",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "ApprovalForAll",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "getApproved",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isApprovedForAll",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "tokenId",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "ownerOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"constant": true,
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceId",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "supportsInterface",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"payable": false,
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"ERC1155": [
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "ApprovalForAll",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256[]",
|
||||
"name": "ids",
|
||||
"type": "uint256[]"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256[]",
|
||||
"name": "values",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"name": "TransferBatch",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "id",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "TransferSingle",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "string",
|
||||
"name": "value",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "uint256",
|
||||
"name": "id",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "URI",
|
||||
"type": "event"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "id",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address[]",
|
||||
"name": "accounts",
|
||||
"type": "address[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "ids",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"name": "balanceOfBatch",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "",
|
||||
"type": "uint256[]"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
}
|
||||
],
|
||||
"name": "isApprovedForAll",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "ids",
|
||||
"type": "uint256[]"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256[]",
|
||||
"name": "amounts",
|
||||
"type": "uint256[]"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "safeBatchTransferFrom",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "id",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"internalType": "bytes",
|
||||
"name": "data",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"name": "safeTransferFrom",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "operator",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "approved",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"name": "setApprovalForAll",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "bytes4",
|
||||
"name": "interfaceId",
|
||||
"type": "bytes4"
|
||||
}
|
||||
],
|
||||
"name": "supportsInterface",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "id",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"name": "uri",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from abc import ABC, abstractmethod
|
||||
from base64 import b64decode, b64encode
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from marshmallow import Schema
|
||||
|
||||
|
||||
class _Serializable:
|
||||
class Schema(Schema):
|
||||
field = NotImplemented
|
||||
|
||||
def to_json(self) -> str:
|
||||
schema = self.Schema()
|
||||
data = schema.dumps(self)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data) -> '_Serializable':
|
||||
data = json.loads(data)
|
||||
schema = cls.Schema()
|
||||
instance = schema.load(data)
|
||||
return instance
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
schema = self.Schema()
|
||||
data = schema.dump(self)
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data) -> '_Serializable':
|
||||
schema = cls.Schema()
|
||||
instance = schema.load(data)
|
||||
return instance
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
json_payload = self.to_json().encode()
|
||||
b64_json_payload = b64encode(json_payload)
|
||||
return b64_json_payload
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes) -> '_Serializable':
|
||||
json_payload = b64decode(data).decode()
|
||||
instance = cls.from_json(json_payload)
|
||||
return instance
|
||||
|
||||
|
||||
class ReencryptionCondition(_Serializable, ABC):
|
||||
|
||||
class InvalidCondition(ValueError):
|
||||
"""Invalid value for condition."""
|
||||
|
||||
class ConditionEvaluationFailed(Exception):
|
||||
"""Could not evaluate condition for some reason."""
|
||||
|
||||
class Schema(Schema):
|
||||
name = NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, *args, **kwargs) -> Tuple[bool, Any]:
|
||||
"""Returns the boolean result of the evaluation and the returned value in a two-tuple."""
|
||||
return NotImplemented
|
|
@ -0,0 +1,124 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from typing import Any
|
||||
|
||||
from eip712_structs import Bytes, EIP712Struct, String, Uint
|
||||
from eth_account.account import Account
|
||||
from eth_account.messages import HexBytes, SignableMessage
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
|
||||
USER_ADDRESS_CONTEXT = ":userAddress"
|
||||
|
||||
_CONTEXT_PREFIX = ":"
|
||||
|
||||
_EIP712_VERSION_BYTE = b"\x01"
|
||||
|
||||
|
||||
class RequiredContextVariable(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidContextVariableData(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ContextVariableVerificationFailed(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UserAddress(EIP712Struct):
|
||||
address = String()
|
||||
blockNumber = Uint()
|
||||
blockHash = Bytes(32)
|
||||
signatureText = String()
|
||||
|
||||
|
||||
def _recover_user_address(**context) -> ChecksumAddress:
|
||||
# Expected format:
|
||||
# {
|
||||
# ":userAddress":
|
||||
# {
|
||||
# "signature": "<signature>",
|
||||
# "address": "<address>",
|
||||
# "typedData": "<a complicated EIP712 data structure>"
|
||||
# }
|
||||
# }
|
||||
|
||||
# setup
|
||||
try:
|
||||
user_address_info = context[USER_ADDRESS_CONTEXT]
|
||||
signature = user_address_info["signature"]
|
||||
user_address = to_checksum_address(user_address_info["address"])
|
||||
eip712_message = user_address_info["typedData"]
|
||||
message, domain = UserAddress.from_message(eip712_message)
|
||||
signable_message = SignableMessage(
|
||||
HexBytes(_EIP712_VERSION_BYTE),
|
||||
header=domain.hash_struct(),
|
||||
body=message.hash_struct(),
|
||||
)
|
||||
except Exception as e:
|
||||
# data could not be processed
|
||||
raise InvalidContextVariableData(
|
||||
f'Invalid data provided for "{USER_ADDRESS_CONTEXT}"; {e.__class__.__name__} - {e}'
|
||||
)
|
||||
|
||||
# actual verification
|
||||
try:
|
||||
address_for_signature = Account.recover_message(
|
||||
signable_message=signable_message, signature=signature
|
||||
)
|
||||
if address_for_signature == user_address:
|
||||
return user_address
|
||||
except Exception as e:
|
||||
# exception during verification
|
||||
raise ContextVariableVerificationFailed(
|
||||
f"Could not determine address of signature for '{USER_ADDRESS_CONTEXT}'; {e.__class__.__name__} - {e}"
|
||||
)
|
||||
|
||||
# verification failed - addresses don't match
|
||||
raise ContextVariableVerificationFailed(
|
||||
f"Signer address for '{USER_ADDRESS_CONTEXT}' signature does not match; expected {user_address}"
|
||||
)
|
||||
|
||||
|
||||
_DIRECTIVES = {
|
||||
USER_ADDRESS_CONTEXT: _recover_user_address,
|
||||
}
|
||||
|
||||
|
||||
def is_context_variable(variable) -> bool:
|
||||
return isinstance(variable, str) and variable.startswith(_CONTEXT_PREFIX)
|
||||
|
||||
|
||||
def get_context_value(context_variable: str, **context) -> Any:
|
||||
try:
|
||||
func = _DIRECTIVES[
|
||||
context_variable
|
||||
] # These are special context vars that will pre-processed by ursula
|
||||
except KeyError:
|
||||
# fallback for context variable without directive - assume key,value pair
|
||||
# handles the case for user customized context variables
|
||||
value = context.get(context_variable)
|
||||
if not value:
|
||||
raise RequiredContextVariable(
|
||||
f'"No value provided for unrecognized context variable "{context_variable}"'
|
||||
)
|
||||
else:
|
||||
value = func(**context) # required inputs here
|
||||
|
||||
return value
|
|
@ -0,0 +1,273 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
from marshmallow import fields, post_load
|
||||
from web3 import Web3
|
||||
from web3.contract import ContractFunction
|
||||
from web3.providers import BaseProvider
|
||||
|
||||
from nucypher.blockchain.eth.clients import PUBLIC_CHAINS
|
||||
from nucypher.policy.conditions import STANDARD_ABI_CONTRACT_TYPES, STANDARD_ABIS
|
||||
from nucypher.policy.conditions._utils import CamelCaseSchema
|
||||
from nucypher.policy.conditions.base import ReencryptionCondition
|
||||
from nucypher.policy.conditions.context import get_context_value, is_context_variable
|
||||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
|
||||
# TODO: Move this method to a util function
|
||||
__CHAINS = {
|
||||
60: 'ethereum', # TODO: make a few edits for now
|
||||
131277322940537: 'testerchain', # TODO: this one can be moved to a pytest fixture / setup logic
|
||||
**PUBLIC_CHAINS,
|
||||
}
|
||||
|
||||
|
||||
def _resolve_chain(chain: Union[str, int]) -> Tuple[str, int]:
|
||||
"""Returns the name *and* chain ID given only a name *or* chain ID"""
|
||||
for pair in __CHAINS.items():
|
||||
if chain in pair:
|
||||
chain_id, chain_name = pair
|
||||
return chain_name, chain_id
|
||||
else:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"{chain} is not a known blockchain."
|
||||
)
|
||||
|
||||
|
||||
def _resolve_abi(standard_contract_type: str, method: str, function_abi: List) -> List:
|
||||
"""Resolves the contract an/or function ABI from a standard contract name"""
|
||||
if not (function_abi or standard_contract_type):
|
||||
# TODO: Is this protection needed?
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"Ambiguous ABI - Supply either an ABI or a standard contract type ({STANDARD_ABI_CONTRACT_TYPES})."
|
||||
)
|
||||
|
||||
if standard_contract_type:
|
||||
try:
|
||||
function_abi = STANDARD_ABIS[standard_contract_type]
|
||||
except KeyError:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"Invalid standard contract type {standard_contract_type}; Must be one of {STANDARD_ABI_CONTRACT_TYPES}"
|
||||
)
|
||||
|
||||
if not function_abi:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"No function ABI supplied for '{method}'"
|
||||
)
|
||||
|
||||
# TODO: Verify that the function and ABI pair match?
|
||||
# ABI(function_abi)
|
||||
return function_abi
|
||||
|
||||
|
||||
def camel_case_to_snake(data: str) -> str:
|
||||
data = re.sub(r'(?<!^)(?=[A-Z])', '_', data).lower()
|
||||
return data
|
||||
|
||||
|
||||
def _resolve_any_context_variables(
|
||||
parameters: List[Any], return_value_test: ReturnValueTest, **context
|
||||
):
|
||||
processed_parameters = []
|
||||
for p in parameters:
|
||||
# TODO needs additional support for ERC1155 which has lists of values
|
||||
# context variables can only be strings, but other types of parameters can be passed
|
||||
if is_context_variable(p):
|
||||
p = get_context_value(context_variable=p, **context)
|
||||
processed_parameters.append(p)
|
||||
|
||||
v = return_value_test.value
|
||||
if is_context_variable(return_value_test.value):
|
||||
v = get_context_value(context_variable=v, **context)
|
||||
processed_return_value_test = ReturnValueTest(return_value_test.comparator, value=v)
|
||||
|
||||
return processed_parameters, processed_return_value_test
|
||||
|
||||
|
||||
class RPCCondition(ReencryptionCondition):
|
||||
ALLOWED_METHODS = (
|
||||
|
||||
# Contract
|
||||
'balanceOf',
|
||||
|
||||
# RPC
|
||||
'eth_getBalance',
|
||||
) # TODO other allowed methods (tDEC #64)
|
||||
|
||||
class RPCExecutionFailed(ReencryptionCondition.ConditionEvaluationFailed):
|
||||
"""Raised when an exception is raised from an RPC call."""
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
name = fields.Str()
|
||||
chain = fields.Str()
|
||||
method = fields.Str()
|
||||
parameters = fields.List(fields.Field, attribute='parameters', required=False)
|
||||
return_value_test = fields.Nested(ReturnValueTest.ReturnValueTestSchema())
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return RPCCondition(**data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = f'{self.__class__.__name__}(function={self.method}, chain={self.chain_name})'
|
||||
return r
|
||||
|
||||
def __init__(self,
|
||||
chain: str,
|
||||
method: str,
|
||||
return_value_test: ReturnValueTest,
|
||||
parameters: Optional[List[Any]] = None
|
||||
):
|
||||
|
||||
# Validate input
|
||||
# _validate_parameters(parameters=parameters)
|
||||
# TODO: Additional validation (function is valid for ABI, RVT validity, standard contract name validity, etc.)
|
||||
|
||||
# internal
|
||||
self.chain_name, self.chain_id = _resolve_chain(chain=chain)
|
||||
self.method = self.validate_method(method=method)
|
||||
|
||||
# test
|
||||
self.parameters = parameters # input
|
||||
self.return_value_test = return_value_test # output
|
||||
|
||||
@property
|
||||
def chain(self) -> str:
|
||||
return self.chain_name
|
||||
|
||||
def validate_method(self, method):
|
||||
if method not in self.ALLOWED_METHODS:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"'{method}' is not a permitted RPC endpoint for condition evaluation."
|
||||
)
|
||||
if not method.startswith('eth_'):
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"Only eth RPC methods are accepted for condition evaluation; '{method}' is not permitted"
|
||||
)
|
||||
return method
|
||||
|
||||
def _configure_provider(self, provider: BaseProvider):
|
||||
"""Binds the condition's contract function to a blockchian provider for evaluation"""
|
||||
self.w3 = Web3(provider)
|
||||
provider_chain = self.w3.eth.chain_id
|
||||
if provider_chain != self.chain_id:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"This condition can only be evaluated on {self.chain_id} but the provider's "
|
||||
f"connection is to chain {provider_chain}"
|
||||
)
|
||||
return provider
|
||||
|
||||
def _get_web3_py_function(self, rpc_method: str):
|
||||
web3_py_method = camel_case_to_snake(rpc_method)
|
||||
rpc_function = getattr(
|
||||
self.w3.eth, web3_py_method
|
||||
) # bind contract function (only exposes the eth API)
|
||||
return rpc_function
|
||||
|
||||
def _execute_call(self, parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
rpc_endpoint_, rpc_method = self.method.split("_", 1)
|
||||
rpc_function = self._get_web3_py_function(rpc_method)
|
||||
rpc_result = rpc_function(*parameters) # RPC read
|
||||
return rpc_result
|
||||
|
||||
def verify(self, provider: BaseProvider, **context) -> Tuple[bool, Any]:
|
||||
"""Performs onchain read and return value test"""
|
||||
self._configure_provider(provider=provider)
|
||||
parameters, return_value_test = _resolve_any_context_variables(
|
||||
self.parameters, self.return_value_test, **context
|
||||
)
|
||||
try:
|
||||
result = self._execute_call(parameters=parameters)
|
||||
except Exception as e:
|
||||
raise self.RPCExecutionFailed(f"Contract call '{self.method}' failed: {e}")
|
||||
|
||||
eval_result = return_value_test.eval(result) # test
|
||||
return eval_result, result
|
||||
|
||||
|
||||
class ContractCondition(RPCCondition):
|
||||
class Schema(RPCCondition.Schema):
|
||||
SKIP_VALUES = (None,)
|
||||
standard_contract_type = fields.Str(required=False)
|
||||
contract_address = fields.Str(required=True)
|
||||
function_abi = fields.Str(required=False)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return ContractCondition(**data)
|
||||
|
||||
def __init__(self,
|
||||
contract_address: ChecksumAddress,
|
||||
standard_contract_type: str = None,
|
||||
function_abi: List = None,
|
||||
*args, **kwargs):
|
||||
# internal
|
||||
super().__init__(*args, **kwargs)
|
||||
self.w3 = Web3() # used to instantiate contract function without a provider
|
||||
|
||||
# preprocessing
|
||||
contract_address = to_checksum_address(contract_address)
|
||||
function_abi = _resolve_abi(
|
||||
standard_contract_type=standard_contract_type,
|
||||
method=self.method,
|
||||
function_abi=function_abi
|
||||
)
|
||||
|
||||
# spec
|
||||
self.contract_address = contract_address
|
||||
self.standard_contract_type = standard_contract_type
|
||||
self.function_abi = function_abi
|
||||
self.contract_function = self._get_unbound_contract_function()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = f'{self.__class__.__name__}(function={self.method}, ' \
|
||||
f'contract={self.contract_address[:6]}..., ' \
|
||||
f'chain={self.chain_name})'
|
||||
return r
|
||||
|
||||
def validate_method(self, method):
|
||||
return method
|
||||
|
||||
def _configure_provider(self, *args, **kwargs):
|
||||
super()._configure_provider(*args, **kwargs)
|
||||
self.contract_function.w3 = self.w3
|
||||
|
||||
def _get_unbound_contract_function(self) -> ContractFunction:
|
||||
"""Gets an unbound contract function to evaluate for this condition"""
|
||||
try:
|
||||
contract = self.w3.eth.contract(
|
||||
address=self.contract_address, abi=self.function_abi
|
||||
)
|
||||
contract_function = getattr(contract.functions, self.method)
|
||||
return contract_function
|
||||
except Exception as e:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"Unable to find contract function, '{self.method}', for condition: {e}"
|
||||
)
|
||||
|
||||
def _execute_call(self, parameters: List[Any]) -> Any:
|
||||
"""Execute onchain read and return result."""
|
||||
bound_contract_function = self.contract_function(
|
||||
*parameters
|
||||
) # bind contract function
|
||||
contract_result = bound_contract_function.call() # onchain read
|
||||
return contract_result
|
|
@ -0,0 +1,219 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import ast
|
||||
import base64
|
||||
import json
|
||||
import operator
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from marshmallow import fields, post_load
|
||||
|
||||
from nucypher.policy.conditions._utils import (
|
||||
CamelCaseSchema,
|
||||
_deserialize_condition_lingo,
|
||||
)
|
||||
from nucypher.policy.conditions.base import ReencryptionCondition
|
||||
from nucypher.policy.conditions.context import is_context_variable
|
||||
|
||||
|
||||
class Operator:
|
||||
OPERATORS = ('and', 'or')
|
||||
|
||||
def __init__(self, operator: str):
|
||||
if operator not in self.OPERATORS:
|
||||
raise Exception(f'{operator} is not a valid operator')
|
||||
self.operator = operator
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.operator
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {'operator': self.operator}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Dict[str, str]) -> 'Operator':
|
||||
try:
|
||||
operator = data['operator']
|
||||
except KeyError:
|
||||
raise Exception(f'Invalid operator JSON')
|
||||
instance = cls(operator=operator)
|
||||
return instance
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data) -> 'Operator':
|
||||
data = json.loads(data)
|
||||
instance = cls.from_dict(data)
|
||||
return instance
|
||||
|
||||
def to_json(self) -> str:
|
||||
data = self.to_dict()
|
||||
data = json.dumps(data)
|
||||
return data
|
||||
|
||||
|
||||
class ReturnValueTest:
|
||||
class InvalidExpression(ValueError):
|
||||
pass
|
||||
|
||||
_COMPARATOR_FUNCTIONS = {
|
||||
"==": operator.eq,
|
||||
"!=": operator.ne,
|
||||
">": operator.gt,
|
||||
"<": operator.lt,
|
||||
"<=": operator.le,
|
||||
">=": operator.ge,
|
||||
}
|
||||
COMPARATORS = tuple(_COMPARATOR_FUNCTIONS)
|
||||
|
||||
class ReturnValueTestSchema(CamelCaseSchema):
|
||||
comparator = fields.Str()
|
||||
value = fields.Raw(allow_none=False) # any valid type (excludes None)
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return ReturnValueTest(**data)
|
||||
|
||||
def __init__(self, comparator: str, value):
|
||||
if comparator not in self.COMPARATORS:
|
||||
raise self.InvalidExpression(
|
||||
f'"{comparator}" is not a permitted comparator.'
|
||||
)
|
||||
|
||||
if not is_context_variable(value):
|
||||
# verify that value is valid, but don't set it here so as not to change the value;
|
||||
# it will be sanitized at eval time. Need to maintain serialization/deserialization
|
||||
# consistency
|
||||
self._sanitize_value(value)
|
||||
|
||||
self.comparator = comparator
|
||||
self.value = value
|
||||
|
||||
def _sanitize_value(self, value):
|
||||
try:
|
||||
return ast.literal_eval(str(value))
|
||||
except Exception:
|
||||
raise self.InvalidExpression(f'"{value}" is not a permitted value.')
|
||||
|
||||
def eval(self, data) -> bool:
|
||||
if is_context_variable(self.value):
|
||||
# programming error if we get here
|
||||
raise RuntimeError(
|
||||
f"'{self.value}' is an unprocessed context variable and is not valid "
|
||||
f"for condition evaluation."
|
||||
)
|
||||
left_operand = self._sanitize_value(data)
|
||||
right_operand = self._sanitize_value(self.value)
|
||||
result = self._COMPARATOR_FUNCTIONS[self.comparator](left_operand, right_operand)
|
||||
return result
|
||||
|
||||
|
||||
class ConditionLingo:
|
||||
"""
|
||||
A Collection of re-encryption conditions evaluated as a compound boolean expression.
|
||||
|
||||
This is an alternate implementation of the condition expression format used in the Lit Protocol (https://github.com/LIT-Protocol);
|
||||
credit to the authors for inspiring this work.
|
||||
"""
|
||||
|
||||
class Failed(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, conditions: List[Union[ReencryptionCondition, Operator, Any]]):
|
||||
"""
|
||||
The input list must be structured:
|
||||
condition
|
||||
operator
|
||||
condition
|
||||
...
|
||||
"""
|
||||
self._validate(lingo=conditions)
|
||||
self.conditions = conditions
|
||||
|
||||
@staticmethod
|
||||
def _validate(lingo) -> None:
|
||||
if len(lingo) % 2 == 0:
|
||||
raise ValueError('conditions must be odd length, ever other element being an operator')
|
||||
for index, element in enumerate(lingo):
|
||||
if (not index % 2) and not (isinstance(element, ReencryptionCondition)):
|
||||
raise Exception(f'{index} element must be a condition; Got {type(element)}.')
|
||||
elif (index % 2) and (not isinstance(element, Operator)):
|
||||
raise Exception(f'{index} element must be an operator; Got {type(element)}.')
|
||||
|
||||
@classmethod
|
||||
def from_list(cls, payload: List[Dict[str, str]]) -> 'ConditionLingo':
|
||||
conditions = [_deserialize_condition_lingo(c) for c in payload]
|
||||
instance = cls(conditions=conditions)
|
||||
return instance
|
||||
|
||||
def to_list(self): # TODO: __iter__ ?
|
||||
payload = [c.to_dict() for c in self.conditions]
|
||||
return payload
|
||||
|
||||
def to_json(self) -> str:
|
||||
data = json.dumps(self.to_list())
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: str) -> 'ConditionLingo':
|
||||
data = json.loads(data)
|
||||
instance = cls.from_list(payload=data)
|
||||
return instance
|
||||
|
||||
def to_base64(self) -> bytes:
|
||||
data = base64.b64encode(self.to_json().encode())
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def from_base64(cls, data: bytes) -> 'ConditionLingo':
|
||||
data = base64.b64decode(data).decode()
|
||||
instance = cls.from_json(data)
|
||||
return instance
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
data = self.to_json().encode()
|
||||
return data
|
||||
|
||||
def __eval(self, eval_string: str):
|
||||
# TODO: Additional protection and/or sanitation here
|
||||
result = eval(eval_string)
|
||||
return result
|
||||
|
||||
def __process(self, *args, **kwargs):
|
||||
for task in self.conditions:
|
||||
if isinstance(task, ReencryptionCondition):
|
||||
condition = task
|
||||
result, value = condition.verify(*args, **kwargs)
|
||||
yield result
|
||||
elif isinstance(task, Operator):
|
||||
operator = task
|
||||
yield operator
|
||||
else:
|
||||
raise TypeError(f"Unrecognized type {type(task)} for ConditionLingo")
|
||||
|
||||
def eval(self, *args, **kwargs) -> bool:
|
||||
data = self.__process(*args, **kwargs)
|
||||
# [True, <Operator>, False] -> 'True or False'
|
||||
eval_string = ' '.join(str(e) for e in data)
|
||||
result = self.__eval(eval_string=eval_string)
|
||||
if not result:
|
||||
raise self.Failed
|
||||
return True
|
||||
|
||||
|
||||
OR = Operator('or')
|
||||
AND = Operator('and')
|
|
@ -0,0 +1,61 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Tuple
|
||||
|
||||
from marshmallow import fields, post_load
|
||||
|
||||
from nucypher.policy.conditions._utils import CamelCaseSchema
|
||||
from nucypher.policy.conditions.base import ReencryptionCondition
|
||||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
|
||||
|
||||
class TimeCondition(ReencryptionCondition):
|
||||
METHOD = 'timelock'
|
||||
|
||||
class Schema(CamelCaseSchema):
|
||||
name = fields.Str()
|
||||
method = fields.Str(default='timelock')
|
||||
return_value_test = fields.Nested(ReturnValueTest.ReturnValueTestSchema())
|
||||
|
||||
@post_load
|
||||
def make(self, data, **kwargs):
|
||||
return TimeCondition(**data)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
r = f'{self.__class__.__name__}(timestamp={self.return_value_test.value})'
|
||||
return r
|
||||
|
||||
def __init__(self, return_value_test: ReturnValueTest, method: str = METHOD):
|
||||
if method != self.METHOD:
|
||||
raise ReencryptionCondition.InvalidCondition(
|
||||
f"{self.__class__.__name__} must be instantiated with the {self.METHOD} method."
|
||||
)
|
||||
self.return_value_test = return_value_test
|
||||
|
||||
@property
|
||||
def method(self):
|
||||
return self.METHOD
|
||||
|
||||
@property
|
||||
def timestamp(self):
|
||||
return self.return_value_test.value
|
||||
|
||||
def verify(self, *args, **kwargs) -> Tuple[bool, float]:
|
||||
eval_time = time.time() # system clock
|
||||
return self.return_value_test.eval(data=eval_time), eval_time
|
|
@ -16,13 +16,14 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
"""
|
||||
|
||||
|
||||
from typing import Dict, Set, Union
|
||||
from typing import Dict, Set
|
||||
|
||||
from eth_utils import to_canonical_address
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_canonical_address
|
||||
from nucypher_core.umbral import PublicKey, SecretKey, VerifiedCapsuleFrag
|
||||
|
||||
from nucypher_core import MessageKit, RetrievalKit
|
||||
from nucypher_core.umbral import PublicKey, VerifiedCapsuleFrag, SecretKey
|
||||
from nucypher_core import Address, MessageKit, RetrievalKit
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
|
||||
|
||||
class PolicyMessageKit:
|
||||
|
@ -47,7 +48,11 @@ class PolicyMessageKit:
|
|||
self._result = result
|
||||
|
||||
def as_retrieval_kit(self) -> RetrievalKit:
|
||||
return RetrievalKit(self.message_kit.capsule, self._result.canonical_addresses())
|
||||
return RetrievalKit(
|
||||
capsule=self.message_kit.capsule,
|
||||
queried_addresses=self._result.canonical_addresses(),
|
||||
conditions=self.message_kit.conditions,
|
||||
)
|
||||
|
||||
def decrypt(self, sk: SecretKey) -> bytes:
|
||||
return self.message_kit.decrypt_reencrypted(sk,
|
||||
|
@ -63,6 +68,10 @@ class PolicyMessageKit:
|
|||
result=self._result.with_result(result),
|
||||
message_kit=self.message_kit)
|
||||
|
||||
@property
|
||||
def conditions(self) -> ConditionLingo:
|
||||
return self.message_kit.conditions
|
||||
|
||||
|
||||
# TODO: a better name?
|
||||
class RetrievalResult:
|
||||
|
@ -79,7 +88,7 @@ class RetrievalResult:
|
|||
|
||||
def canonical_addresses(self) -> Set[bytes]:
|
||||
# TODO (#1995): propagate this to use canonical addresses everywhere
|
||||
return set([to_canonical_address(address) for address in self.cfrags])
|
||||
return set([Address(to_canonical_address(address)) for address in self.cfrags])
|
||||
|
||||
def with_result(self, result: 'RetrievalResult') -> 'RetrievalResult':
|
||||
"""
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, NamedTuple, Dict
|
||||
|
||||
|
@ -28,19 +27,7 @@ from nucypher.blockchain.eth.registry import InMemoryContractRegistry, BaseContr
|
|||
from nucypher.policy.policies import BlockchainPolicy, Policy
|
||||
|
||||
|
||||
class ReencryptionPrerequisite(ABC):
|
||||
"""Baseclass for reencryption preconditions relating to a policy."""
|
||||
|
||||
ONCHAIN = NotImplemented
|
||||
NAME = NotImplemented
|
||||
|
||||
@abstractmethod
|
||||
def verify(self, payee: ChecksumAddress, request: ReencryptionRequest) -> bool:
|
||||
"""returns True if reencryption is permitted by the payee (ursula) for the given reencryption request."""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
class PaymentMethod(ReencryptionPrerequisite, ABC):
|
||||
class PaymentMethod(ABC):
|
||||
"""Extends ReencryptionPrerequisite to facilitate policy payment and payment verification."""
|
||||
|
||||
class Quote(NamedTuple):
|
||||
|
@ -115,43 +102,6 @@ class ContractPayment(PaymentMethod, ABC):
|
|||
return self.__agent # set cache
|
||||
|
||||
|
||||
class FreeReencryptions(PaymentMethod):
|
||||
"""Useful for private federations and testing."""
|
||||
|
||||
ONCHAIN = False
|
||||
NAME = 'Free'
|
||||
|
||||
def verify(self, payee: ChecksumAddress, request: ReencryptionRequest) -> bool:
|
||||
return True
|
||||
|
||||
def pay(self, policy: Policy) -> Dict:
|
||||
receipt = f'Receipt for free policy {bytes(policy.hrac).hex()}.'
|
||||
return dict(receipt=receipt.encode())
|
||||
|
||||
@property
|
||||
def rate(self) -> int:
|
||||
return 0
|
||||
|
||||
def quote(self,
|
||||
shares: int,
|
||||
commencement: Optional[Timestamp] = None,
|
||||
expiration: Optional[Timestamp] = None,
|
||||
duration: Optional[int] = None,
|
||||
*args, **kwargs
|
||||
) -> PaymentMethod.Quote:
|
||||
return self.Quote(
|
||||
value=0,
|
||||
rate=0,
|
||||
shares=shares,
|
||||
duration=duration,
|
||||
commencement=commencement,
|
||||
expiration=expiration
|
||||
)
|
||||
|
||||
def validate_price(self, *args, **kwargs) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class SubscriptionManagerPayment(ContractPayment):
|
||||
"""Handle policy payment using the SubscriptionManager contract."""
|
||||
|
||||
|
@ -229,7 +179,44 @@ class SubscriptionManagerPayment(ContractPayment):
|
|||
return True
|
||||
|
||||
|
||||
class FreeReencryptions(PaymentMethod):
|
||||
"""Useful for private federations and testing."""
|
||||
|
||||
ONCHAIN = False
|
||||
NAME = 'Free'
|
||||
|
||||
def verify(self, payee: ChecksumAddress, request: ReencryptionRequest) -> bool:
|
||||
return True
|
||||
|
||||
def pay(self, policy: Policy) -> Dict:
|
||||
receipt = f'Receipt for free policy {bytes(policy.hrac).hex()}.'
|
||||
return dict(receipt=receipt.encode())
|
||||
|
||||
@property
|
||||
def rate(self) -> int:
|
||||
return 0
|
||||
|
||||
def quote(self,
|
||||
shares: int,
|
||||
commencement: Optional[Timestamp] = None,
|
||||
expiration: Optional[Timestamp] = None,
|
||||
duration: Optional[int] = None,
|
||||
*args, **kwargs
|
||||
) -> PaymentMethod.Quote:
|
||||
return self.Quote(
|
||||
value=0,
|
||||
rate=0,
|
||||
shares=shares,
|
||||
duration=duration,
|
||||
commencement=commencement,
|
||||
expiration=expiration
|
||||
)
|
||||
|
||||
def validate_price(self, *args, **kwargs) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
PAYMENT_METHODS = {
|
||||
FreeReencryptions.NAME: FreeReencryptions,
|
||||
SubscriptionManagerPayment.NAME: SubscriptionManagerPayment,
|
||||
FreeReencryptions.NAME: FreeReencryptions
|
||||
}
|
||||
|
|
|
@ -21,10 +21,9 @@ from typing import Sequence, Optional, Iterable, List, Dict
|
|||
|
||||
import maya
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from nucypher_core import HRAC, TreasureMap
|
||||
from nucypher_core import Address, HRAC, TreasureMap
|
||||
from nucypher_core.umbral import PublicKey, VerifiedKeyFrag
|
||||
|
||||
from nucypher.blockchain.eth.utils import calculate_period_duration
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.middleware import RestMiddleware
|
||||
from nucypher.policy.reservoir import (
|
||||
|
@ -171,7 +170,7 @@ class Policy(ABC):
|
|||
self._publish(ursulas=ursulas)
|
||||
|
||||
assigned_kfrags = {
|
||||
ursula.canonical_address: (ursula.public_keys(DecryptingPower), vkfrag)
|
||||
Address(ursula.canonical_address): (ursula.public_keys(DecryptingPower), vkfrag)
|
||||
for ursula, vkfrag in zip(ursulas, self.kfrags)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,10 +15,11 @@ You should have received a copy of the GNU Affero General Public License
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from eth_utils import to_checksum_address, to_canonical_address
|
||||
|
||||
from nucypher_core import RevocationOrder
|
||||
from nucypher_core import Address, RevocationOrder
|
||||
from nucypher.crypto.signing import SignatureStamp
|
||||
|
||||
|
||||
|
@ -29,7 +30,7 @@ class RevocationKit:
|
|||
self.revocations = dict()
|
||||
for staking_provider_address, encrypted_kfrag in treasure_map.destinations.items():
|
||||
self.revocations[staking_provider_address] = RevocationOrder(signer=signer.as_umbral_signer(),
|
||||
staking_provider_address=staking_provider_address,
|
||||
staking_provider_address=Address(staking_provider_address),
|
||||
encrypted_kfrag=encrypted_kfrag)
|
||||
|
||||
def __iter__(self):
|
||||
|
|
|
@ -112,7 +112,7 @@ class EtherchainGasPriceDatafeed(EthereumGasPriceDatafeed):
|
|||
|
||||
def _parse_gas_prices(self):
|
||||
self._probe_feed()
|
||||
self.gas_prices = {self.get_canonical_speed(k): int(Web3.toWei(v, 'gwei')) for k, v in self._raw_data.items()}
|
||||
self.gas_prices = {self.get_canonical_speed(k): int(Web3.to_wei(v, 'gwei')) for k, v in self._raw_data.items()}
|
||||
|
||||
|
||||
class UpvestGasPriceDatafeed(EthereumGasPriceDatafeed):
|
||||
|
@ -130,7 +130,7 @@ class UpvestGasPriceDatafeed(EthereumGasPriceDatafeed):
|
|||
|
||||
def _parse_gas_prices(self):
|
||||
self._probe_feed()
|
||||
self.gas_prices = {self.get_canonical_speed(k): int(Web3.toWei(v, 'gwei'))
|
||||
self.gas_prices = {self.get_canonical_speed(k): int(Web3.to_wei(v, 'gwei'))
|
||||
for k, v in self._raw_data['estimates'].items()}
|
||||
|
||||
|
||||
|
@ -152,5 +152,5 @@ class ZoltuGasPriceDatafeed(EthereumGasPriceDatafeed):
|
|||
self.gas_prices = dict()
|
||||
for canonical_speed_name, zoltu_speed in self._speed_names.items():
|
||||
gwei_price = self._raw_data[zoltu_speed].split(" ")[0]
|
||||
wei_price = int(Web3.toWei(gwei_price, 'gwei'))
|
||||
wei_price = int(Web3.to_wei(gwei_price, 'gwei'))
|
||||
self.gas_prices[canonical_speed_name] = wei_price
|
||||
|
|
|
@ -129,11 +129,11 @@ EXPECTED_CONFIRMATION_TIME_IN_SECONDS = { # TODO: See #2447
|
|||
|
||||
|
||||
def construct_fixed_price_gas_strategy(gas_price, denomination: str = "wei") -> Callable:
|
||||
gas_price_in_wei = Web3.toWei(gas_price, denomination)
|
||||
gas_price_in_wei = Web3.to_wei(gas_price, denomination)
|
||||
|
||||
def _fixed_price_strategy(web3: Web3, transaction_params: TxParams = None) -> Wei:
|
||||
return gas_price_in_wei
|
||||
|
||||
_fixed_price_strategy.name = f"{round(Web3.fromWei(gas_price_in_wei, 'gwei'))}gwei"
|
||||
_fixed_price_strategy.name = f"{round(Web3.from_wei(gas_price_in_wei, 'gwei'))}gwei"
|
||||
|
||||
return _fixed_price_strategy
|
||||
|
|
|
@ -14,11 +14,10 @@
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from eth_typing import ChecksumAddress
|
||||
|
||||
from nucypher_core import TreasureMap, RetrievalKit
|
||||
from nucypher_core import RetrievalKit, TreasureMap
|
||||
from nucypher_core.umbral import PublicKey
|
||||
|
||||
from nucypher.control.interfaces import ControlInterface, attach_schema
|
||||
|
@ -36,12 +35,14 @@ class PorterInterface(ControlInterface):
|
|||
def get_ursulas(self,
|
||||
quantity: int,
|
||||
exclude_ursulas: Optional[List[ChecksumAddress]] = None,
|
||||
include_ursulas: Optional[List[ChecksumAddress]] = None) -> dict:
|
||||
ursulas_info = self.implementer.get_ursulas(quantity=quantity,
|
||||
exclude_ursulas=exclude_ursulas,
|
||||
include_ursulas=include_ursulas)
|
||||
include_ursulas: Optional[List[ChecksumAddress]] = None) -> Dict:
|
||||
ursulas_info = self.implementer.get_ursulas(
|
||||
quantity=quantity,
|
||||
exclude_ursulas=exclude_ursulas,
|
||||
include_ursulas=include_ursulas,
|
||||
)
|
||||
|
||||
response_data = {"ursulas": ursulas_info}
|
||||
response_data = {"ursulas": ursulas_info} # list of UrsulaInfo objects
|
||||
return response_data
|
||||
|
||||
@attach_schema(porter_schema.AliceRevoke)
|
||||
|
@ -59,12 +60,16 @@ class PorterInterface(ControlInterface):
|
|||
alice_verifying_key: PublicKey,
|
||||
bob_encrypting_key: PublicKey,
|
||||
bob_verifying_key: PublicKey,
|
||||
) -> dict:
|
||||
retrieval_results = self.implementer.retrieve_cfrags(treasure_map=treasure_map,
|
||||
retrieval_kits=retrieval_kits,
|
||||
alice_verifying_key=alice_verifying_key,
|
||||
bob_encrypting_key=bob_encrypting_key,
|
||||
bob_verifying_key=bob_verifying_key)
|
||||
results = retrieval_results # list of RetrievalResult objects
|
||||
response_data = {'retrieval_results': results}
|
||||
context: Optional[Dict] = None) -> Dict:
|
||||
retrieval_outcomes = self.implementer.retrieve_cfrags(
|
||||
treasure_map=treasure_map,
|
||||
retrieval_kits=retrieval_kits,
|
||||
alice_verifying_key=alice_verifying_key,
|
||||
bob_encrypting_key=bob_encrypting_key,
|
||||
bob_verifying_key=bob_verifying_key,
|
||||
context=context,
|
||||
)
|
||||
response_data = {
|
||||
"retrieval_results": retrieval_outcomes
|
||||
} # list of RetrievalOutcome objects
|
||||
return response_data
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from marshmallow import fields
|
||||
|
||||
from marshmallow.fields import String
|
||||
from nucypher_core import RetrievalKit as RetrievalKitClass
|
||||
from nucypher_core.umbral import CapsuleFrag as CapsuleFragClass
|
||||
|
||||
|
@ -33,7 +33,7 @@ class RetrievalKit(Base64BytesRepresentation):
|
|||
retrieval_kit_bytes = super()._deserialize(value, attr, data, **kwargs)
|
||||
return RetrievalKitClass.from_bytes(retrieval_kit_bytes)
|
||||
except Exception as e:
|
||||
raise InvalidInputData(f"Could not convert input for {self.name} to a valid checksum address: {e}")
|
||||
raise InvalidInputData(f"Could not convert input for {self.name} to a valid RetrievalKit: {e}")
|
||||
|
||||
|
||||
class CapsuleFrag(Base64BytesRepresentation):
|
||||
|
@ -45,9 +45,10 @@ class CapsuleFrag(Base64BytesRepresentation):
|
|||
raise InvalidInputData(f"Could not parse {self.name}: {e}")
|
||||
|
||||
|
||||
class RetrievalResultSchema(BaseSchema):
|
||||
"""Schema for the result of retrieve_cfrags."""
|
||||
class RetrievalOutcomeSchema(BaseSchema):
|
||||
"""Schema for the result of /retrieve_cfrags endpoint."""
|
||||
cfrags = fields.Dict(keys=UrsulaChecksumAddress(), values=CapsuleFrag())
|
||||
errors = fields.Dict(keys=UrsulaChecksumAddress(), values=String())
|
||||
|
||||
# maintain field declaration ordering
|
||||
class Meta:
|
||||
|
|
|
@ -159,5 +159,21 @@ class BobRetrieveCFrags(BaseSchema):
|
|||
type=click.STRING,
|
||||
required=True))
|
||||
|
||||
# optional
|
||||
context = base_fields.JSON(
|
||||
expected_type=dict,
|
||||
required=False,
|
||||
load_only=True,
|
||||
click=click.option(
|
||||
"--context",
|
||||
"-ctx",
|
||||
help="Context data for retrieval conditions",
|
||||
type=click.STRING,
|
||||
required=False,
|
||||
),
|
||||
)
|
||||
|
||||
# output
|
||||
retrieval_results = marshmallow_fields.List(marshmallow_fields.Nested(fields.RetrievalResultSchema), dump_only=True)
|
||||
retrieval_results = marshmallow_fields.List(
|
||||
marshmallow_fields.Nested(fields.RetrievalOutcomeSchema), dump_only=True
|
||||
)
|
||||
|
|
|
@ -17,28 +17,30 @@
|
|||
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List, NamedTuple, Optional, Sequence
|
||||
from typing import Dict, List, NamedTuple, Optional, Sequence
|
||||
|
||||
from constant_sorrow.constants import NO_BLOCKCHAIN_CONNECTION, NO_CONTROL_PROTOCOL
|
||||
from eth_typing import ChecksumAddress
|
||||
from eth_utils import to_checksum_address
|
||||
from flask import request, Response
|
||||
from nucypher_core import TreasureMap, RetrievalKit
|
||||
from flask import Response, request
|
||||
from nucypher_core import RetrievalKit, TreasureMap
|
||||
from nucypher_core.umbral import PublicKey
|
||||
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, PREApplicationAgent
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import BaseContractRegistry, InMemoryContractRegistry
|
||||
from nucypher.blockchain.eth.registry import (
|
||||
BaseContractRegistry,
|
||||
InMemoryContractRegistry,
|
||||
)
|
||||
from nucypher.characters.lawful import Ursula
|
||||
from nucypher.control.controllers import JSONRPCController, WebController
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.network.nodes import Learner
|
||||
from nucypher.network.retrieval import RetrievalClient
|
||||
from nucypher.policy.kits import RetrievalResult
|
||||
from nucypher.policy.reservoir import (
|
||||
make_federated_staker_reservoir,
|
||||
PrefetchStrategy,
|
||||
make_decentralized_staking_provider_reservoir,
|
||||
PrefetchStrategy
|
||||
make_federated_staker_reservoir,
|
||||
)
|
||||
from nucypher.utilities.concurrency import WorkerPool
|
||||
from nucypher.utilities.logging import Logger
|
||||
|
@ -78,6 +80,15 @@ the Pipe for PRE Application network operations
|
|||
uri: str
|
||||
encrypting_key: PublicKey
|
||||
|
||||
class RetrievalOutcome(NamedTuple):
|
||||
"""
|
||||
Simple object that stores the results and errors of re-encryption operations across
|
||||
one or more Ursulas.
|
||||
"""
|
||||
|
||||
cfrags: Dict
|
||||
errors: Dict
|
||||
|
||||
def __init__(self,
|
||||
domain: str = None,
|
||||
registry: BaseContractRegistry = None,
|
||||
|
@ -164,10 +175,24 @@ the Pipe for PRE Application network operations
|
|||
alice_verifying_key: PublicKey,
|
||||
bob_encrypting_key: PublicKey,
|
||||
bob_verifying_key: PublicKey,
|
||||
) -> List[RetrievalResult]:
|
||||
context: Optional[Dict] = None) -> List[RetrievalOutcome]:
|
||||
client = RetrievalClient(self)
|
||||
return client.retrieve_cfrags(treasure_map, retrieval_kits,
|
||||
alice_verifying_key, bob_encrypting_key, bob_verifying_key)
|
||||
context = context or dict() # must not be None
|
||||
results, errors = client.retrieve_cfrags(
|
||||
treasure_map,
|
||||
retrieval_kits,
|
||||
alice_verifying_key,
|
||||
bob_encrypting_key,
|
||||
bob_verifying_key,
|
||||
**context,
|
||||
)
|
||||
result_outcomes = []
|
||||
for result, error in zip(results, errors):
|
||||
result_outcome = Porter.RetrievalOutcome(
|
||||
cfrags=result.cfrags, errors=error.errors
|
||||
)
|
||||
result_outcomes.append(result_outcome)
|
||||
return result_outcomes
|
||||
|
||||
def _make_reservoir(self,
|
||||
quantity: int,
|
||||
|
|
114
requirements.txt
114
requirements.txt
|
@ -1,108 +1,110 @@
|
|||
-i https://pypi.python.org/simple
|
||||
aiohttp==3.8.1; python_version >= '3.6'
|
||||
aiohttp==3.8.3; python_version >= '3.6'
|
||||
aiosignal==1.2.0; python_version >= '3.6'
|
||||
appdirs==1.4.4
|
||||
async-timeout==4.0.2; python_version >= '3.6'
|
||||
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
autobahn==22.3.2; python_version >= '3.7'
|
||||
attrs==22.1.0; python_version >= '3.5'
|
||||
autobahn==22.7.1; python_version >= '3.7'
|
||||
automat==20.2.0
|
||||
base58==2.1.1; python_version >= '3.5'
|
||||
bitarray==1.2.2
|
||||
bitarray==2.6.0
|
||||
bytestring-splitter==2.4.1
|
||||
cached-property==1.5.2
|
||||
certifi==2021.10.8
|
||||
cffi==1.15.0
|
||||
charset-normalizer==2.0.12; python_version >= '3'
|
||||
click==8.1.2
|
||||
colorama==0.4.4
|
||||
certifi==2022.9.24; python_version >= '3.6'
|
||||
cffi==1.15.1
|
||||
charset-normalizer==2.1.1; python_version >= '3.6'
|
||||
click==8.1.3
|
||||
colorama==0.4.5
|
||||
constant-sorrow==0.1.0a9
|
||||
constantly==15.1.0
|
||||
construct==2.10.68; python_version >= '3.6'
|
||||
cryptography==36.0.2
|
||||
cytoolz==0.11.2; implementation_name == 'cpython'
|
||||
cryptography==38.0.1
|
||||
cytoolz==0.12.0; implementation_name == 'cpython'
|
||||
dateparser==1.1.1; python_version >= '3.5'
|
||||
ecdsa==0.18.0b2; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
|
||||
eth-abi==2.1.1; python_version >= '3.6' and python_version < '4'
|
||||
eth-account==0.5.7; python_version >= '3.6' and python_version < '4'
|
||||
ecdsa==0.18.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'
|
||||
eip712-structs==1.1.0
|
||||
eth-abi==3.0.1; python_version >= '3.7' and python_version < '4'
|
||||
eth-account==0.7.0; python_version >= '3.6' and python_version < '4'
|
||||
eth-bloom==1.0.4; python_version >= '3.6' and python_version < '4'
|
||||
eth-hash[pycryptodome]==0.3.2; python_version >= '3.5' and python_version < '4'
|
||||
eth-keyfile==0.5.1
|
||||
eth-keys==0.3.4
|
||||
eth-rlp==0.2.1; python_version >= '3.6' and python_version < '4'
|
||||
eth-tester==0.6.0b6
|
||||
eth-typing==2.3.0; python_version >= '3.5' and python_version < '4'
|
||||
eth-utils==1.10.0
|
||||
flask==2.1.1
|
||||
frozenlist==1.3.0; python_version >= '3.7'
|
||||
eth-hash==0.3.3; python_version >= '3.5' and python_version < '4'
|
||||
eth-keyfile==0.6.0
|
||||
eth-keys==0.4.0
|
||||
eth-rlp==0.3.0; python_version >= '3.7' and python_version < '4'
|
||||
eth-tester==0.7.0b1
|
||||
eth-typing==3.2.0; python_version >= '3.6' and python_version < '4'
|
||||
eth-utils==2.0.0
|
||||
flask==2.2.2
|
||||
frozenlist==1.3.1; python_version >= '3.7'
|
||||
hendrix==3.4.0
|
||||
hexbytes==0.2.2; python_version >= '3.6' and python_version < '4'
|
||||
humanize==4.0.0; python_version >= '3.7'
|
||||
hexbytes==0.3.0; python_version >= '3.7' and python_version < '4'
|
||||
humanize==4.4.0; python_version >= '3.7'
|
||||
hyperlink==21.0.0
|
||||
idna==3.3; python_version >= '3'
|
||||
idna==3.4; python_version >= '3.5'
|
||||
incremental==21.3.0
|
||||
ipfshttpclient==0.8.0a2; python_full_version >= '3.6.2' and python_full_version not in '3.7.0, 3.7.1'
|
||||
itsdangerous==2.0.1
|
||||
jinja2==3.0.3
|
||||
jsonschema==4.4.0; python_version >= '3.7'
|
||||
jsonschema==4.16.0; python_version >= '3.7'
|
||||
libusb1==3.0.0
|
||||
lmdb==1.3.0
|
||||
lru-dict==1.1.7
|
||||
mako==1.2.0
|
||||
lru-dict==1.1.8
|
||||
mako==1.2.3
|
||||
markupsafe==2.1.1; python_version >= '3.7'
|
||||
marshmallow==3.15.0
|
||||
marshmallow==3.18.0
|
||||
maya==0.6.1
|
||||
mnemonic==0.20; python_version >= '3.5'
|
||||
msgpack-python==0.5.6
|
||||
msgpack==1.0.3
|
||||
msgpack==1.0.4
|
||||
multiaddr==0.0.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
multidict==6.0.2; python_version >= '3.7'
|
||||
mypy-extensions==0.4.3
|
||||
netaddr==0.8.0
|
||||
nucypher-core==0.2.0
|
||||
nucypher-core==0.4.0
|
||||
packaging==21.3; python_version >= '3.6'
|
||||
parsimonious==0.8.1
|
||||
pendulum==2.1.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
pillow==9.1.0; python_version >= '3.7'
|
||||
pillow==9.2.0; python_version >= '3.7'
|
||||
protobuf==3.20.1; python_version >= '3.7'
|
||||
py-ecc==5.2.0; python_version >= '3.5' and python_version < '4'
|
||||
py-evm==0.5.0a3
|
||||
pyasn1-modules==0.2.8
|
||||
pyasn1==0.4.8
|
||||
py-ecc==6.0.0; python_version >= '3.6' and python_version < '4'
|
||||
py-evm==0.6.0a1
|
||||
pyasn1-modules==0.3.0rc1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
pyasn1==0.5.0rc2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'
|
||||
pychalk==2.0.1
|
||||
pycparser==2.21
|
||||
pycryptodome==3.14.1
|
||||
pycryptodome==3.15.0
|
||||
pyethash==0.1.27
|
||||
pynacl==1.5.0
|
||||
pyopenssl==22.0.0
|
||||
pyparsing==3.0.8; python_full_version >= '3.6.8'
|
||||
pyopenssl==22.1.0
|
||||
pyparsing==3.0.9; python_full_version >= '3.6.8'
|
||||
pyrsistent==0.18.1; python_version >= '3.7'
|
||||
pysha3==1.0.2
|
||||
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
pytz==2022.1
|
||||
pytz==2022.4
|
||||
pytzdata==2020.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
qrcode[pil]==7.3.1
|
||||
regex==2022.3.2; python_version >= '3.6'
|
||||
requests==2.27.1
|
||||
rlp==2.0.1
|
||||
semantic-version==2.9.0; python_version >= '2.7'
|
||||
requests==2.28.1
|
||||
rlp==3.0.0
|
||||
semantic-version==2.10.0; python_version >= '2.7'
|
||||
service-identity==21.1.0
|
||||
setuptools==62.1.0; python_version >= '3.7'
|
||||
setuptools==65.4.1; python_version >= '3.7'
|
||||
simple-rlp==0.1.3; python_version >= '3.7'
|
||||
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
|
||||
snaptime==0.2.4
|
||||
sortedcontainers==2.4.0
|
||||
tabulate==0.8.9
|
||||
toolz==0.11.2; python_version >= '3.5'
|
||||
trezor==0.13.0
|
||||
trie==2.0.0a5; python_version >= '3.6' and python_version < '4'
|
||||
twisted==22.4.0; python_full_version >= '3.6.7'
|
||||
tabulate==0.8.10
|
||||
toolz==0.12.0; python_version >= '3.5'
|
||||
trezor==0.13.3
|
||||
trie==2.0.2; python_version >= '3.6' and python_version < '4'
|
||||
twisted==22.8.0; python_full_version >= '3.7.1'
|
||||
txaio==22.2.1; python_version >= '3.6'
|
||||
typing-extensions==3.10.0.2
|
||||
typing-extensions==4.3.0; python_version >= '3.7'
|
||||
tzlocal==2.1
|
||||
urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
|
||||
urllib3==1.26.12; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' and python_version < '4'
|
||||
varint==1.0.2
|
||||
watchdog==2.1.7; python_version >= '3.6'
|
||||
web3==6.0.0b1
|
||||
watchdog==2.1.9; python_version >= '3.6'
|
||||
web3==6.0.0b6
|
||||
websockets==10.3; python_version >= '3.7'
|
||||
werkzeug==2.1.1; python_version >= '3.7'
|
||||
yarl==1.7.2; python_version >= '3.6'
|
||||
werkzeug==2.2.2; python_version >= '3.7'
|
||||
yarl==1.8.1; python_version >= '3.7'
|
||||
zope.interface==5.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
import tests.data
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
ContractAgency,
|
||||
NucypherTokenAgent,
|
||||
SubscriptionManagerAgent,
|
||||
)
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.sol.compile.compile import multiversion_compile
|
||||
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
|
||||
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import AND, OR, ConditionLingo, ReturnValueTest
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
|
||||
VECTORS_FILE = Path(tests.__file__).parent / "data" / "test_conditions.json"
|
||||
|
||||
with open(VECTORS_FILE, 'r') as file:
|
||||
VECTORS = json.loads(file.read())
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC1155_balance_condition_data():
|
||||
data = json.dumps(VECTORS['ERC1155_balance'])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC1155_balance_condition(ERC1155_balance_condition_data):
|
||||
data = ERC1155_balance_condition_data
|
||||
condition = ContractCondition.from_json(data)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC20_balance_condition_data():
|
||||
data = json.dumps(VECTORS['ERC20_balance'])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC20_balance_condition(ERC20_balance_condition_data):
|
||||
data = ERC20_balance_condition_data
|
||||
condition = ContractCondition.from_json(data)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_condition():
|
||||
condition = RPCCondition(
|
||||
method="eth_getBalance",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", Web3.to_wei(1_000_000, "ether")),
|
||||
parameters=[USER_ADDRESS_CONTEXT],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def erc20_evm_condition(test_registry, agency):
|
||||
token = ContractAgency.get_agent(NucypherTokenAgent, registry=test_registry)
|
||||
condition = ContractCondition(
|
||||
contract_address=token.contract.address,
|
||||
method="balanceOf",
|
||||
standard_contract_type="ERC20",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", 0),
|
||||
parameters=[USER_ADDRESS_CONTEXT],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def custom_context_variable_erc20_condition(test_registry, agency):
|
||||
token = ContractAgency.get_agent(NucypherTokenAgent, registry=test_registry)
|
||||
condition = ContractCondition(
|
||||
contract_address=token.contract.address,
|
||||
method="balanceOf",
|
||||
standard_contract_type="ERC20",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", 0),
|
||||
parameters=[":addressToUse"],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def erc721_contract(testerchain, test_registry):
|
||||
solidity_root = Path(__file__).parent / "contracts"
|
||||
source_bundle = SourceBundle(base_path=solidity_root)
|
||||
compiled_constracts = multiversion_compile([source_bundle], True)
|
||||
testerchain._raw_contract_cache = compiled_constracts
|
||||
|
||||
origin, *everybody_else = testerchain.client.accounts
|
||||
transacting_power = TransactingPower(
|
||||
account=origin, signer=Web3Signer(testerchain.client)
|
||||
)
|
||||
contract, receipt = testerchain.deploy_contract(
|
||||
transacting_power=transacting_power,
|
||||
registry=test_registry,
|
||||
contract_name="ConditionNFT",
|
||||
)
|
||||
# mint an NFT with tokenId = 1
|
||||
tx = contract.functions.mint(origin, 1).transact({"from": origin})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
|
||||
return contract
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def erc721_evm_condition_owner(erc721_contract):
|
||||
condition = ContractCondition(
|
||||
contract_address=erc721_contract.address,
|
||||
method="ownerOf",
|
||||
standard_contract_type="ERC721",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", ":userAddress"),
|
||||
parameters=[
|
||||
":tokenId",
|
||||
],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def erc721_evm_condition_balanceof(erc721_contract):
|
||||
condition = ContractCondition(
|
||||
contract_address=erc721_contract.address,
|
||||
method="balanceOf",
|
||||
standard_contract_type="ERC721",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest(">", 0),
|
||||
parameters=[
|
||||
":userAddress",
|
||||
],
|
||||
)
|
||||
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subscription_manager_is_active_policy_condition(test_registry, agency):
|
||||
subscription_manager = ContractAgency.get_agent(SubscriptionManagerAgent, registry=test_registry)
|
||||
condition = ContractCondition(
|
||||
contract_address=subscription_manager.contract.address,
|
||||
function_abi=subscription_manager.contract.abi,
|
||||
method="isPolicyActive",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", True),
|
||||
parameters=[":hrac"],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subscription_manager_get_policy_zeroized_policy_struct_condition(
|
||||
test_registry, agency
|
||||
):
|
||||
subscription_manager = ContractAgency.get_agent(
|
||||
SubscriptionManagerAgent, registry=test_registry
|
||||
)
|
||||
condition = ContractCondition(
|
||||
contract_address=subscription_manager.contract.address,
|
||||
function_abi=subscription_manager.contract.abi,
|
||||
method="getPolicy",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", ":expectedPolicyStruct"),
|
||||
parameters=[":hrac"],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def timelock_condition():
|
||||
condition = TimeCondition(
|
||||
return_value_test=ReturnValueTest('>', 0)
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lingo(erc721_evm_condition_balanceof,
|
||||
timelock_condition,
|
||||
rpc_condition,
|
||||
erc20_evm_condition):
|
||||
lingo = ConditionLingo(
|
||||
conditions=[
|
||||
erc721_evm_condition_balanceof,
|
||||
OR,
|
||||
timelock_condition,
|
||||
OR,
|
||||
rpc_condition,
|
||||
AND,
|
||||
erc20_evm_condition,
|
||||
]
|
||||
)
|
||||
return lingo
|
|
@ -0,0 +1,19 @@
|
|||
// contracts/MyNFT.sol
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "zeppelin/token/ERC721/ERC721.sol";
|
||||
|
||||
contract ConditionNFT is ERC721 {
|
||||
constructor() ERC721("ConditionsNFT", "cNFT") {
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Mints a new NFT.
|
||||
* @param _to The address that will own the minted NFT.
|
||||
* @param _tokenId of the NFT to be minted by the msg.sender.
|
||||
*/
|
||||
function mint(address _to, uint256 _tokenId) external {
|
||||
super._mint(_to, _tokenId);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,521 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/ERC721.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "zeppelin/token/ERC721/IERC721.sol";
|
||||
import "zeppelin/token/ERC721/IERC721Receiver.sol";
|
||||
import "zeppelin/token/ERC721/extensions/IERC721Metadata.sol";
|
||||
import "zeppelin/utils/Address.sol";
|
||||
import "zeppelin/utils/Context.sol";
|
||||
import "zeppelin/utils/Strings.sol";
|
||||
import "zeppelin/utils/introspection/ERC165.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
|
||||
* the Metadata extension, but not including the Enumerable extension, which is available separately as
|
||||
* {ERC721Enumerable}.
|
||||
*/
|
||||
contract ERC721 is Context, ERC165, IERC721, IERC721Metadata {
|
||||
using Address for address;
|
||||
using Strings for uint256;
|
||||
|
||||
// Token name
|
||||
string private _name;
|
||||
|
||||
// Token symbol
|
||||
string private _symbol;
|
||||
|
||||
// Mapping from token ID to owner address
|
||||
mapping(uint256 => address) private _owners;
|
||||
|
||||
// Mapping owner address to token count
|
||||
mapping(address => uint256) private _balances;
|
||||
|
||||
// Mapping from token ID to approved address
|
||||
mapping(uint256 => address) private _tokenApprovals;
|
||||
|
||||
// Mapping from owner to operator approvals
|
||||
mapping(address => mapping(address => bool)) private _operatorApprovals;
|
||||
|
||||
/**
|
||||
* @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
|
||||
*/
|
||||
constructor(string memory name_, string memory symbol_) {
|
||||
_name = name_;
|
||||
_symbol = symbol_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
|
||||
return
|
||||
interfaceId == type(IERC721).interfaceId ||
|
||||
interfaceId == type(IERC721Metadata).interfaceId ||
|
||||
super.supportsInterface(interfaceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-balanceOf}.
|
||||
*/
|
||||
function balanceOf(address owner) public view virtual override returns (uint256) {
|
||||
require(owner != address(0), "ERC721: address zero is not a valid owner");
|
||||
return _balances[owner];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-ownerOf}.
|
||||
*/
|
||||
function ownerOf(uint256 tokenId) public view virtual override returns (address) {
|
||||
address owner = _ownerOf(tokenId);
|
||||
require(owner != address(0), "ERC721: invalid token ID");
|
||||
return owner;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Metadata-name}.
|
||||
*/
|
||||
function name() public view virtual override returns (string memory) {
|
||||
return _name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Metadata-symbol}.
|
||||
*/
|
||||
function symbol() public view virtual override returns (string memory) {
|
||||
return _symbol;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721Metadata-tokenURI}.
|
||||
*/
|
||||
function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
|
||||
_requireMinted(tokenId);
|
||||
|
||||
string memory baseURI = _baseURI();
|
||||
return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
|
||||
* token will be the concatenation of the `baseURI` and the `tokenId`. Empty
|
||||
* by default, can be overridden in child contracts.
|
||||
*/
|
||||
function _baseURI() internal view virtual returns (string memory) {
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-approve}.
|
||||
*/
|
||||
function approve(address to, uint256 tokenId) public virtual override {
|
||||
address owner = ERC721.ownerOf(tokenId);
|
||||
require(to != owner, "ERC721: approval to current owner");
|
||||
|
||||
require(
|
||||
_msgSender() == owner || isApprovedForAll(owner, _msgSender()),
|
||||
"ERC721: approve caller is not token owner or approved for all"
|
||||
);
|
||||
|
||||
_approve(to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-getApproved}.
|
||||
*/
|
||||
function getApproved(uint256 tokenId) public view virtual override returns (address) {
|
||||
_requireMinted(tokenId);
|
||||
|
||||
return _tokenApprovals[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-setApprovalForAll}.
|
||||
*/
|
||||
function setApprovalForAll(address operator, bool approved) public virtual override {
|
||||
_setApprovalForAll(_msgSender(), operator, approved);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-isApprovedForAll}.
|
||||
*/
|
||||
function isApprovedForAll(address owner, address operator) public view virtual override returns (bool) {
|
||||
return _operatorApprovals[owner][operator];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-transferFrom}.
|
||||
*/
|
||||
function transferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) public virtual override {
|
||||
//solhint-disable-next-line max-line-length
|
||||
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
|
||||
|
||||
_transfer(from, to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-safeTransferFrom}.
|
||||
*/
|
||||
function safeTransferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) public virtual override {
|
||||
safeTransferFrom(from, to, tokenId, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev See {IERC721-safeTransferFrom}.
|
||||
*/
|
||||
function safeTransferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory data
|
||||
) public virtual override {
|
||||
require(_isApprovedOrOwner(_msgSender(), tokenId), "ERC721: caller is not token owner or approved");
|
||||
_safeTransfer(from, to, tokenId, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
|
||||
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
|
||||
*
|
||||
* `data` is additional data, it has no specified format and it is sent in call to `to`.
|
||||
*
|
||||
* This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
|
||||
* implement alternative mechanisms to perform token transfer, such as signature-based.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `from` cannot be the zero address.
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must exist and be owned by `from`.
|
||||
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _safeTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory data
|
||||
) internal virtual {
|
||||
_transfer(from, to, tokenId);
|
||||
require(_checkOnERC721Received(from, to, tokenId, data), "ERC721: transfer to non ERC721Receiver implementer");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the owner of the `tokenId`. Does NOT revert if token doesn't exist
|
||||
*/
|
||||
function _ownerOf(uint256 tokenId) internal view virtual returns (address) {
|
||||
return _owners[tokenId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns whether `tokenId` exists.
|
||||
*
|
||||
* Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
|
||||
*
|
||||
* Tokens start existing when they are minted (`_mint`),
|
||||
* and stop existing when they are burned (`_burn`).
|
||||
*/
|
||||
function _exists(uint256 tokenId) internal view virtual returns (bool) {
|
||||
return _ownerOf(tokenId) != address(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns whether `spender` is allowed to manage `tokenId`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must exist.
|
||||
*/
|
||||
function _isApprovedOrOwner(address spender, uint256 tokenId) internal view virtual returns (bool) {
|
||||
address owner = ERC721.ownerOf(tokenId);
|
||||
return (spender == owner || isApprovedForAll(owner, spender) || getApproved(tokenId) == spender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Safely mints `tokenId` and transfers it to `to`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must not exist.
|
||||
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _safeMint(address to, uint256 tokenId) internal virtual {
|
||||
_safeMint(to, tokenId, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
|
||||
* forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
|
||||
*/
|
||||
function _safeMint(
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory data
|
||||
) internal virtual {
|
||||
_mint(to, tokenId);
|
||||
require(
|
||||
_checkOnERC721Received(address(0), to, tokenId, data),
|
||||
"ERC721: transfer to non ERC721Receiver implementer"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Mints `tokenId` and transfers it to `to`.
|
||||
*
|
||||
* WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must not exist.
|
||||
* - `to` cannot be the zero address.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _mint(address to, uint256 tokenId) internal virtual {
|
||||
require(to != address(0), "ERC721: mint to the zero address");
|
||||
require(!_exists(tokenId), "ERC721: token already minted");
|
||||
|
||||
_beforeTokenTransfer(address(0), to, tokenId);
|
||||
|
||||
// Check that tokenId was not minted by `_beforeTokenTransfer` hook
|
||||
require(!_exists(tokenId), "ERC721: token already minted");
|
||||
|
||||
unchecked {
|
||||
// Will not overflow unless all 2**256 token ids are minted to the same owner.
|
||||
// Given that tokens are minted one by one, it is impossible in practice that
|
||||
// this ever happens. Might change if we allow batch minting.
|
||||
// The ERC fails to describe this case.
|
||||
_balances[to] += 1;
|
||||
}
|
||||
|
||||
_owners[tokenId] = to;
|
||||
|
||||
emit Transfer(address(0), to, tokenId);
|
||||
|
||||
_afterTokenTransfer(address(0), to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Destroys `tokenId`.
|
||||
* The approval is cleared when the token is burned.
|
||||
* This is an internal function that does not check if the sender is authorized to operate on the token.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must exist.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _burn(uint256 tokenId) internal virtual {
|
||||
address owner = ERC721.ownerOf(tokenId);
|
||||
|
||||
_beforeTokenTransfer(owner, address(0), tokenId);
|
||||
|
||||
// Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook
|
||||
owner = ERC721.ownerOf(tokenId);
|
||||
|
||||
// Clear approvals
|
||||
delete _tokenApprovals[tokenId];
|
||||
|
||||
unchecked {
|
||||
// Cannot overflow, as that would require more tokens to be burned/transferred
|
||||
// out than the owner initially received through minting and transferring in.
|
||||
_balances[owner] -= 1;
|
||||
}
|
||||
delete _owners[tokenId];
|
||||
|
||||
emit Transfer(owner, address(0), tokenId);
|
||||
|
||||
_afterTokenTransfer(owner, address(0), tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Transfers `tokenId` from `from` to `to`.
|
||||
* As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must be owned by `from`.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function _transfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal virtual {
|
||||
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
|
||||
require(to != address(0), "ERC721: transfer to the zero address");
|
||||
|
||||
_beforeTokenTransfer(from, to, tokenId);
|
||||
|
||||
// Check that tokenId was not transferred by `_beforeTokenTransfer` hook
|
||||
require(ERC721.ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
|
||||
|
||||
// Clear approvals from the previous owner
|
||||
delete _tokenApprovals[tokenId];
|
||||
|
||||
unchecked {
|
||||
// `_balances[from]` cannot overflow for the same reason as described in `_burn`:
|
||||
// `from`'s balance is the number of token held, which is at least one before the current
|
||||
// transfer.
|
||||
// `_balances[to]` could overflow in the conditions described in `_mint`. That would require
|
||||
// all 2**256 token ids to be minted, which in practice is impossible.
|
||||
_balances[from] -= 1;
|
||||
_balances[to] += 1;
|
||||
}
|
||||
_owners[tokenId] = to;
|
||||
|
||||
emit Transfer(from, to, tokenId);
|
||||
|
||||
_afterTokenTransfer(from, to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Approve `to` to operate on `tokenId`
|
||||
*
|
||||
* Emits an {Approval} event.
|
||||
*/
|
||||
function _approve(address to, uint256 tokenId) internal virtual {
|
||||
_tokenApprovals[tokenId] = to;
|
||||
emit Approval(ERC721.ownerOf(tokenId), to, tokenId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Approve `operator` to operate on all of `owner` tokens
|
||||
*
|
||||
* Emits an {ApprovalForAll} event.
|
||||
*/
|
||||
function _setApprovalForAll(
|
||||
address owner,
|
||||
address operator,
|
||||
bool approved
|
||||
) internal virtual {
|
||||
require(owner != operator, "ERC721: approve to caller");
|
||||
_operatorApprovals[owner][operator] = approved;
|
||||
emit ApprovalForAll(owner, operator, approved);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Reverts if the `tokenId` has not been minted yet.
|
||||
*/
|
||||
function _requireMinted(uint256 tokenId) internal view virtual {
|
||||
require(_exists(tokenId), "ERC721: invalid token ID");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
|
||||
* The call is not executed if the target address is not a contract.
|
||||
*
|
||||
* @param from address representing the previous owner of the given token ID
|
||||
* @param to target address that will receive the tokens
|
||||
* @param tokenId uint256 ID of the token to be transferred
|
||||
* @param data bytes optional data to send along with the call
|
||||
* @return bool whether the call correctly returned the expected magic value
|
||||
*/
|
||||
function _checkOnERC721Received(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes memory data
|
||||
) private returns (bool) {
|
||||
if (to.isContract()) {
|
||||
try IERC721Receiver(to).onERC721Received(_msgSender(), from, tokenId, data) returns (bytes4 retval) {
|
||||
return retval == IERC721Receiver.onERC721Received.selector;
|
||||
} catch (bytes memory reason) {
|
||||
if (reason.length == 0) {
|
||||
revert("ERC721: transfer to non ERC721Receiver implementer");
|
||||
} else {
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
revert(add(32, reason), mload(reason))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called before any (single) token transfer. This includes minting and burning.
|
||||
* See {_beforeConsecutiveTokenTransfer}.
|
||||
*
|
||||
* Calling conditions:
|
||||
*
|
||||
* - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
|
||||
* transferred to `to`.
|
||||
* - When `from` is zero, `tokenId` will be minted for `to`.
|
||||
* - When `to` is zero, ``from``'s `tokenId` will be burned.
|
||||
* - `from` and `to` are never both zero.
|
||||
*
|
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
||||
*/
|
||||
function _beforeTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal virtual {}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called after any (single) transfer of tokens. This includes minting and burning.
|
||||
* See {_afterConsecutiveTokenTransfer}.
|
||||
*
|
||||
* Calling conditions:
|
||||
*
|
||||
* - when `from` and `to` are both non-zero.
|
||||
* - `from` and `to` are never both zero.
|
||||
*
|
||||
* To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
|
||||
*/
|
||||
function _afterTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) internal virtual {}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called before consecutive token transfers.
|
||||
* Calling conditions are similar to {_beforeTokenTransfer}.
|
||||
*
|
||||
* The default implementation include balances updates that extensions such as {ERC721Consecutive} cannot perform
|
||||
* directly.
|
||||
*/
|
||||
function _beforeConsecutiveTokenTransfer(
|
||||
address from,
|
||||
address to,
|
||||
uint256, /*first*/
|
||||
uint96 size
|
||||
) internal virtual {
|
||||
if (from != address(0)) {
|
||||
_balances[from] -= size;
|
||||
}
|
||||
if (to != address(0)) {
|
||||
_balances[to] += size;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Hook that is called after consecutive token transfers.
|
||||
* Calling conditions are similar to {_afterTokenTransfer}.
|
||||
*/
|
||||
function _afterConsecutiveTokenTransfer(
|
||||
address, /*from*/
|
||||
address, /*to*/
|
||||
uint256, /*first*/
|
||||
uint96 /*size*/
|
||||
) internal virtual {}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/IERC721.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "zeppelin/utils/introspection/IERC165.sol";
|
||||
|
||||
/**
|
||||
* @dev Required interface of an ERC721 compliant contract.
|
||||
*/
|
||||
interface IERC721 is IERC165 {
|
||||
/**
|
||||
* @dev Emitted when `tokenId` token is transferred from `from` to `to`.
|
||||
*/
|
||||
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
|
||||
|
||||
/**
|
||||
* @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
|
||||
*/
|
||||
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
|
||||
|
||||
/**
|
||||
* @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
|
||||
*/
|
||||
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
|
||||
|
||||
/**
|
||||
* @dev Returns the number of tokens in ``owner``'s account.
|
||||
*/
|
||||
function balanceOf(address owner) external view returns (uint256 balance);
|
||||
|
||||
/**
|
||||
* @dev Returns the owner of the `tokenId` token.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must exist.
|
||||
*/
|
||||
function ownerOf(uint256 tokenId) external view returns (address owner);
|
||||
|
||||
/**
|
||||
* @dev Safely transfers `tokenId` token from `from` to `to`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `from` cannot be the zero address.
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must exist and be owned by `from`.
|
||||
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
|
||||
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function safeTransferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId,
|
||||
bytes calldata data
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
|
||||
* are aware of the ERC721 protocol to prevent tokens from being forever locked.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `from` cannot be the zero address.
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must exist and be owned by `from`.
|
||||
* - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
|
||||
* - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function safeTransferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @dev Transfers `tokenId` token from `from` to `to`.
|
||||
*
|
||||
* WARNING: Note that the caller is responsible to confirm that the recipient is capable of receiving ERC721
|
||||
* or else they may be permanently lost. Usage of {safeTransferFrom} prevents loss, though the caller must
|
||||
* understand this adds an external call which potentially creates a reentrancy vulnerability.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `from` cannot be the zero address.
|
||||
* - `to` cannot be the zero address.
|
||||
* - `tokenId` token must be owned by `from`.
|
||||
* - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
|
||||
*
|
||||
* Emits a {Transfer} event.
|
||||
*/
|
||||
function transferFrom(
|
||||
address from,
|
||||
address to,
|
||||
uint256 tokenId
|
||||
) external;
|
||||
|
||||
/**
|
||||
* @dev Gives permission to `to` to transfer `tokenId` token to another account.
|
||||
* The approval is cleared when the token is transferred.
|
||||
*
|
||||
* Only a single account can be approved at a time, so approving the zero address clears previous approvals.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The caller must own the token or be an approved operator.
|
||||
* - `tokenId` must exist.
|
||||
*
|
||||
* Emits an {Approval} event.
|
||||
*/
|
||||
function approve(address to, uint256 tokenId) external;
|
||||
|
||||
/**
|
||||
* @dev Approve or remove `operator` as an operator for the caller.
|
||||
* Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - The `operator` cannot be the caller.
|
||||
*
|
||||
* Emits an {ApprovalForAll} event.
|
||||
*/
|
||||
function setApprovalForAll(address operator, bool _approved) external;
|
||||
|
||||
/**
|
||||
* @dev Returns the account approved for `tokenId` token.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `tokenId` must exist.
|
||||
*/
|
||||
function getApproved(uint256 tokenId) external view returns (address operator);
|
||||
|
||||
/**
|
||||
* @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
|
||||
*
|
||||
* See {setApprovalForAll}
|
||||
*/
|
||||
function isApprovedForAll(address owner, address operator) external view returns (bool);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @title ERC721 token receiver interface
|
||||
* @dev Interface for any contract that wants to support safeTransfers
|
||||
* from ERC721 asset contracts.
|
||||
*/
|
||||
interface IERC721Receiver {
|
||||
/**
|
||||
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
|
||||
* by `operator` from `from`, this function is called.
|
||||
*
|
||||
* It must return its Solidity selector to confirm the token transfer.
|
||||
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
|
||||
*
|
||||
* The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
|
||||
*/
|
||||
function onERC721Received(
|
||||
address operator,
|
||||
address from,
|
||||
uint256 tokenId,
|
||||
bytes calldata data
|
||||
) external returns (bytes4);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "../IERC721.sol";
|
||||
|
||||
/**
|
||||
* @title ERC-721 Non-Fungible Token Standard, optional metadata extension
|
||||
* @dev See https://eips.ethereum.org/EIPS/eip-721
|
||||
*/
|
||||
interface IERC721Metadata is IERC721 {
|
||||
/**
|
||||
* @dev Returns the token collection name.
|
||||
*/
|
||||
function name() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @dev Returns the token collection symbol.
|
||||
*/
|
||||
function symbol() external view returns (string memory);
|
||||
|
||||
/**
|
||||
* @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
|
||||
*/
|
||||
function tokenURI(uint256 tokenId) external view returns (string memory);
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol)
|
||||
|
||||
pragma solidity ^0.8.1;
|
||||
|
||||
/**
|
||||
* @dev Collection of functions related to the address type
|
||||
*/
|
||||
library Address {
|
||||
/**
|
||||
* @dev Returns true if `account` is a contract.
|
||||
*
|
||||
* [IMPORTANT]
|
||||
* ====
|
||||
* It is unsafe to assume that an address for which this function returns
|
||||
* false is an externally-owned account (EOA) and not a contract.
|
||||
*
|
||||
* Among others, `isContract` will return false for the following
|
||||
* types of addresses:
|
||||
*
|
||||
* - an externally-owned account
|
||||
* - a contract in construction
|
||||
* - an address where a contract will be created
|
||||
* - an address where a contract lived, but was destroyed
|
||||
* ====
|
||||
*
|
||||
* [IMPORTANT]
|
||||
* ====
|
||||
* You shouldn't rely on `isContract` to protect against flash loan attacks!
|
||||
*
|
||||
* Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
|
||||
* like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
|
||||
* constructor.
|
||||
* ====
|
||||
*/
|
||||
function isContract(address account) internal view returns (bool) {
|
||||
// This method relies on extcodesize/address.code.length, which returns 0
|
||||
// for contracts in construction, since the code is only stored at the end
|
||||
// of the constructor execution.
|
||||
|
||||
return account.code.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Replacement for Solidity's `transfer`: sends `amount` wei to
|
||||
* `recipient`, forwarding all available gas and reverting on errors.
|
||||
*
|
||||
* https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
|
||||
* of certain opcodes, possibly making contracts go over the 2300 gas limit
|
||||
* imposed by `transfer`, making them unable to receive funds via
|
||||
* `transfer`. {sendValue} removes this limitation.
|
||||
*
|
||||
* https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
|
||||
*
|
||||
* IMPORTANT: because control is transferred to `recipient`, care must be
|
||||
* taken to not create reentrancy vulnerabilities. Consider using
|
||||
* {ReentrancyGuard} or the
|
||||
* https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
|
||||
*/
|
||||
function sendValue(address payable recipient, uint256 amount) internal {
|
||||
require(address(this).balance >= amount, "Address: insufficient balance");
|
||||
|
||||
(bool success, ) = recipient.call{value: amount}("");
|
||||
require(success, "Address: unable to send value, recipient may have reverted");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Performs a Solidity function call using a low level `call`. A
|
||||
* plain `call` is an unsafe replacement for a function call: use this
|
||||
* function instead.
|
||||
*
|
||||
* If `target` reverts with a revert reason, it is bubbled up by this
|
||||
* function (like regular Solidity function calls).
|
||||
*
|
||||
* Returns the raw returned data. To convert to the expected return value,
|
||||
* use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - `target` must be a contract.
|
||||
* - calling `target` with `data` must not revert.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCall(address target, bytes memory data) internal returns (bytes memory) {
|
||||
return functionCallWithValue(target, data, 0, "Address: low-level call failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
|
||||
* `errorMessage` as a fallback revert reason when `target` reverts.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCall(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory errorMessage
|
||||
) internal returns (bytes memory) {
|
||||
return functionCallWithValue(target, data, 0, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
|
||||
* but also transferring `value` wei to `target`.
|
||||
*
|
||||
* Requirements:
|
||||
*
|
||||
* - the calling contract must have an ETH balance of at least `value`.
|
||||
* - the called Solidity function must be `payable`.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCallWithValue(
|
||||
address target,
|
||||
bytes memory data,
|
||||
uint256 value
|
||||
) internal returns (bytes memory) {
|
||||
return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
|
||||
* with `errorMessage` as a fallback revert reason when `target` reverts.
|
||||
*
|
||||
* _Available since v3.1._
|
||||
*/
|
||||
function functionCallWithValue(
|
||||
address target,
|
||||
bytes memory data,
|
||||
uint256 value,
|
||||
string memory errorMessage
|
||||
) internal returns (bytes memory) {
|
||||
require(address(this).balance >= value, "Address: insufficient balance for call");
|
||||
(bool success, bytes memory returndata) = target.call{value: value}(data);
|
||||
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
|
||||
* but performing a static call.
|
||||
*
|
||||
* _Available since v3.3._
|
||||
*/
|
||||
function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
|
||||
return functionStaticCall(target, data, "Address: low-level static call failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
|
||||
* but performing a static call.
|
||||
*
|
||||
* _Available since v3.3._
|
||||
*/
|
||||
function functionStaticCall(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory errorMessage
|
||||
) internal view returns (bytes memory) {
|
||||
(bool success, bytes memory returndata) = target.staticcall(data);
|
||||
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
|
||||
* but performing a delegate call.
|
||||
*
|
||||
* _Available since v3.4._
|
||||
*/
|
||||
function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
|
||||
return functionDelegateCall(target, data, "Address: low-level delegate call failed");
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
|
||||
* but performing a delegate call.
|
||||
*
|
||||
* _Available since v3.4._
|
||||
*/
|
||||
function functionDelegateCall(
|
||||
address target,
|
||||
bytes memory data,
|
||||
string memory errorMessage
|
||||
) internal returns (bytes memory) {
|
||||
(bool success, bytes memory returndata) = target.delegatecall(data);
|
||||
return verifyCallResultFromTarget(target, success, returndata, errorMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Tool to verify that a low level call to smart-contract was successful, and revert (either by bubbling
|
||||
* the revert reason or using the provided one) in case of unsuccessful call or if target was not a contract.
|
||||
*
|
||||
* _Available since v4.8._
|
||||
*/
|
||||
function verifyCallResultFromTarget(
|
||||
address target,
|
||||
bool success,
|
||||
bytes memory returndata,
|
||||
string memory errorMessage
|
||||
) internal view returns (bytes memory) {
|
||||
if (success) {
|
||||
if (returndata.length == 0) {
|
||||
// only check isContract if the call was successful and the return data is empty
|
||||
// otherwise we already know that it was a contract
|
||||
require(isContract(target), "Address: call to non-contract");
|
||||
}
|
||||
return returndata;
|
||||
} else {
|
||||
_revert(returndata, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Tool to verify that a low level call was successful, and revert if it wasn't, either by bubbling the
|
||||
* revert reason or using the provided one.
|
||||
*
|
||||
* _Available since v4.3._
|
||||
*/
|
||||
function verifyCallResult(
|
||||
bool success,
|
||||
bytes memory returndata,
|
||||
string memory errorMessage
|
||||
) internal pure returns (bytes memory) {
|
||||
if (success) {
|
||||
return returndata;
|
||||
} else {
|
||||
_revert(returndata, errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
function _revert(bytes memory returndata, string memory errorMessage) private pure {
|
||||
// Look for revert reason and bubble it up if present
|
||||
if (returndata.length > 0) {
|
||||
// The easiest way to bubble the revert reason is using memory via assembly
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
let returndata_size := mload(returndata)
|
||||
revert(add(32, returndata), returndata_size)
|
||||
}
|
||||
} else {
|
||||
revert(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @dev Provides information about the current execution context, including the
|
||||
* sender of the transaction and its data. While these are generally available
|
||||
* via msg.sender and msg.data, they should not be accessed in such a direct
|
||||
* manner, since when dealing with meta-transactions the account sending and
|
||||
* paying for execution may not be the actual sender (as far as an application
|
||||
* is concerned).
|
||||
*
|
||||
* This contract is only required for intermediate, library-like contracts.
|
||||
*/
|
||||
abstract contract Context {
|
||||
function _msgSender() internal view virtual returns (address) {
|
||||
return msg.sender;
|
||||
}
|
||||
|
||||
function _msgData() internal view virtual returns (bytes calldata) {
|
||||
return msg.data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "zeppelin/utils/math/Math.sol";
|
||||
|
||||
/**
|
||||
* @dev String operations.
|
||||
*/
|
||||
library Strings {
|
||||
bytes16 private constant _SYMBOLS = "0123456789abcdef";
|
||||
uint8 private constant _ADDRESS_LENGTH = 20;
|
||||
|
||||
/**
|
||||
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
|
||||
*/
|
||||
function toString(uint256 value) internal pure returns (string memory) {
|
||||
unchecked {
|
||||
uint256 length = Math.log10(value) + 1;
|
||||
string memory buffer = new string(length);
|
||||
uint256 ptr;
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
ptr := add(buffer, add(32, length))
|
||||
}
|
||||
while (true) {
|
||||
ptr--;
|
||||
/// @solidity memory-safe-assembly
|
||||
assembly {
|
||||
mstore8(ptr, byte(mod(value, 10), _SYMBOLS))
|
||||
}
|
||||
value /= 10;
|
||||
if (value == 0) break;
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
|
||||
*/
|
||||
function toHexString(uint256 value) internal pure returns (string memory) {
|
||||
unchecked {
|
||||
return toHexString(value, Math.log256(value) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
|
||||
*/
|
||||
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
|
||||
bytes memory buffer = new bytes(2 * length + 2);
|
||||
buffer[0] = "0";
|
||||
buffer[1] = "x";
|
||||
for (uint256 i = 2 * length + 1; i > 1; --i) {
|
||||
buffer[i] = _SYMBOLS[value & 0xf];
|
||||
value >>= 4;
|
||||
}
|
||||
require(value == 0, "Strings: hex length insufficient");
|
||||
return string(buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
|
||||
*/
|
||||
function toHexString(address addr) internal pure returns (string memory) {
|
||||
return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
import "zeppelin/utils/introspection/IERC165.sol";
|
||||
|
||||
/**
|
||||
* @dev Implementation of the {IERC165} interface.
|
||||
*
|
||||
* Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
|
||||
* for the additional interface id that will be supported. For example:
|
||||
*
|
||||
* ```solidity
|
||||
* function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
||||
* return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
|
||||
*/
|
||||
abstract contract ERC165 is IERC165 {
|
||||
/**
|
||||
* @dev See {IERC165-supportsInterface}.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
|
||||
return interfaceId == type(IERC165).interfaceId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @dev Interface of the ERC165 standard, as defined in the
|
||||
* https://eips.ethereum.org/EIPS/eip-165[EIP].
|
||||
*
|
||||
* Implementers can declare support of contract interfaces, which can then be
|
||||
* queried by others ({ERC165Checker}).
|
||||
*
|
||||
* For an implementation, see {ERC165}.
|
||||
*/
|
||||
interface IERC165 {
|
||||
/**
|
||||
* @dev Returns true if this contract implements the interface defined by
|
||||
* `interfaceId`. See the corresponding
|
||||
* https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
|
||||
* to learn more about how these ids are created.
|
||||
*
|
||||
* This function call must use less than 30 000 gas.
|
||||
*/
|
||||
function supportsInterface(bytes4 interfaceId) external view returns (bool);
|
||||
}
|
|
@ -0,0 +1,345 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// OpenZeppelin Contracts (last updated v4.7.0) (utils/math/Math.sol)
|
||||
|
||||
pragma solidity ^0.8.0;
|
||||
|
||||
/**
|
||||
* @dev Standard math utilities missing in the Solidity language.
|
||||
*/
|
||||
library Math {
|
||||
enum Rounding {
|
||||
Down, // Toward negative infinity
|
||||
Up, // Toward infinity
|
||||
Zero // Toward zero
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the largest of two numbers.
|
||||
*/
|
||||
function max(uint256 a, uint256 b) internal pure returns (uint256) {
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the smallest of two numbers.
|
||||
*/
|
||||
function min(uint256 a, uint256 b) internal pure returns (uint256) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the average of two numbers. The result is rounded towards
|
||||
* zero.
|
||||
*/
|
||||
function average(uint256 a, uint256 b) internal pure returns (uint256) {
|
||||
// (a + b) / 2 can overflow.
|
||||
return (a & b) + (a ^ b) / 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the ceiling of the division of two numbers.
|
||||
*
|
||||
* This differs from standard division with `/` in that it rounds up instead
|
||||
* of rounding down.
|
||||
*/
|
||||
function ceilDiv(uint256 a, uint256 b) internal pure returns (uint256) {
|
||||
// (a + b - 1) / b can overflow on addition, so we distribute.
|
||||
return a == 0 ? 0 : (a - 1) / b + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates floor(x * y / denominator) with full precision. Throws if result overflows a uint256 or denominator == 0
|
||||
* @dev Original credit to Remco Bloemen under MIT license (https://xn--2-umb.com/21/muldiv)
|
||||
* with further edits by Uniswap Labs also under MIT license.
|
||||
*/
|
||||
function mulDiv(
|
||||
uint256 x,
|
||||
uint256 y,
|
||||
uint256 denominator
|
||||
) internal pure returns (uint256 result) {
|
||||
unchecked {
|
||||
// 512-bit multiply [prod1 prod0] = x * y. Compute the product mod 2^256 and mod 2^256 - 1, then use
|
||||
// use the Chinese Remainder Theorem to reconstruct the 512 bit result. The result is stored in two 256
|
||||
// variables such that product = prod1 * 2^256 + prod0.
|
||||
uint256 prod0; // Least significant 256 bits of the product
|
||||
uint256 prod1; // Most significant 256 bits of the product
|
||||
assembly {
|
||||
let mm := mulmod(x, y, not(0))
|
||||
prod0 := mul(x, y)
|
||||
prod1 := sub(sub(mm, prod0), lt(mm, prod0))
|
||||
}
|
||||
|
||||
// Handle non-overflow cases, 256 by 256 division.
|
||||
if (prod1 == 0) {
|
||||
return prod0 / denominator;
|
||||
}
|
||||
|
||||
// Make sure the result is less than 2^256. Also prevents denominator == 0.
|
||||
require(denominator > prod1);
|
||||
|
||||
///////////////////////////////////////////////
|
||||
// 512 by 256 division.
|
||||
///////////////////////////////////////////////
|
||||
|
||||
// Make division exact by subtracting the remainder from [prod1 prod0].
|
||||
uint256 remainder;
|
||||
assembly {
|
||||
// Compute remainder using mulmod.
|
||||
remainder := mulmod(x, y, denominator)
|
||||
|
||||
// Subtract 256 bit number from 512 bit number.
|
||||
prod1 := sub(prod1, gt(remainder, prod0))
|
||||
prod0 := sub(prod0, remainder)
|
||||
}
|
||||
|
||||
// Factor powers of two out of denominator and compute largest power of two divisor of denominator. Always >= 1.
|
||||
// See https://cs.stackexchange.com/q/138556/92363.
|
||||
|
||||
// Does not overflow because the denominator cannot be zero at this stage in the function.
|
||||
uint256 twos = denominator & (~denominator + 1);
|
||||
assembly {
|
||||
// Divide denominator by twos.
|
||||
denominator := div(denominator, twos)
|
||||
|
||||
// Divide [prod1 prod0] by twos.
|
||||
prod0 := div(prod0, twos)
|
||||
|
||||
// Flip twos such that it is 2^256 / twos. If twos is zero, then it becomes one.
|
||||
twos := add(div(sub(0, twos), twos), 1)
|
||||
}
|
||||
|
||||
// Shift in bits from prod1 into prod0.
|
||||
prod0 |= prod1 * twos;
|
||||
|
||||
// Invert denominator mod 2^256. Now that denominator is an odd number, it has an inverse modulo 2^256 such
|
||||
// that denominator * inv = 1 mod 2^256. Compute the inverse by starting with a seed that is correct for
|
||||
// four bits. That is, denominator * inv = 1 mod 2^4.
|
||||
uint256 inverse = (3 * denominator) ^ 2;
|
||||
|
||||
// Use the Newton-Raphson iteration to improve the precision. Thanks to Hensel's lifting lemma, this also works
|
||||
// in modular arithmetic, doubling the correct bits in each step.
|
||||
inverse *= 2 - denominator * inverse; // inverse mod 2^8
|
||||
inverse *= 2 - denominator * inverse; // inverse mod 2^16
|
||||
inverse *= 2 - denominator * inverse; // inverse mod 2^32
|
||||
inverse *= 2 - denominator * inverse; // inverse mod 2^64
|
||||
inverse *= 2 - denominator * inverse; // inverse mod 2^128
|
||||
inverse *= 2 - denominator * inverse; // inverse mod 2^256
|
||||
|
||||
// Because the division is now exact we can divide by multiplying with the modular inverse of denominator.
|
||||
// This will give us the correct result modulo 2^256. Since the preconditions guarantee that the outcome is
|
||||
// less than 2^256, this is the final result. We don't need to compute the high bits of the result and prod1
|
||||
// is no longer required.
|
||||
result = prod0 * inverse;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates x * y / denominator with full precision, following the selected rounding direction.
|
||||
*/
|
||||
function mulDiv(
|
||||
uint256 x,
|
||||
uint256 y,
|
||||
uint256 denominator,
|
||||
Rounding rounding
|
||||
) internal pure returns (uint256) {
|
||||
uint256 result = mulDiv(x, y, denominator);
|
||||
if (rounding == Rounding.Up && mulmod(x, y, denominator) > 0) {
|
||||
result += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded down.
|
||||
*
|
||||
* Inspired by Henry S. Warren, Jr.'s "Hacker's Delight" (Chapter 11).
|
||||
*/
|
||||
function sqrt(uint256 a) internal pure returns (uint256) {
|
||||
if (a == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// For our first guess, we get the biggest power of 2 which is smaller than the square root of the target.
|
||||
//
|
||||
// We know that the "msb" (most significant bit) of our target number `a` is a power of 2 such that we have
|
||||
// `msb(a) <= a < 2*msb(a)`. This value can be written `msb(a)=2**k` with `k=log2(a)`.
|
||||
//
|
||||
// This can be rewritten `2**log2(a) <= a < 2**(log2(a) + 1)`
|
||||
// → `sqrt(2**k) <= sqrt(a) < sqrt(2**(k+1))`
|
||||
// → `2**(k/2) <= sqrt(a) < 2**((k+1)/2) <= 2**(k/2 + 1)`
|
||||
//
|
||||
// Consequently, `2**(log2(a) / 2)` is a good first approximation of `sqrt(a)` with at least 1 correct bit.
|
||||
uint256 result = 1 << (log2(a) >> 1);
|
||||
|
||||
// At this point `result` is an estimation with one bit of precision. We know the true value is a uint128,
|
||||
// since it is the square root of a uint256. Newton's method converges quadratically (precision doubles at
|
||||
// every iteration). We thus need at most 7 iteration to turn our partial result with one bit of precision
|
||||
// into the expected uint128 result.
|
||||
unchecked {
|
||||
result = (result + a / result) >> 1;
|
||||
result = (result + a / result) >> 1;
|
||||
result = (result + a / result) >> 1;
|
||||
result = (result + a / result) >> 1;
|
||||
result = (result + a / result) >> 1;
|
||||
result = (result + a / result) >> 1;
|
||||
result = (result + a / result) >> 1;
|
||||
return min(result, a / result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Calculates sqrt(a), following the selected rounding direction.
|
||||
*/
|
||||
function sqrt(uint256 a, Rounding rounding) internal pure returns (uint256) {
|
||||
unchecked {
|
||||
uint256 result = sqrt(a);
|
||||
return result + (rounding == Rounding.Up && result * result < a ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the log in base 2, rounded down, of a positive value.
|
||||
* Returns 0 if given 0.
|
||||
*/
|
||||
function log2(uint256 value) internal pure returns (uint256) {
|
||||
uint256 result = 0;
|
||||
unchecked {
|
||||
if (value >> 128 > 0) {
|
||||
value >>= 128;
|
||||
result += 128;
|
||||
}
|
||||
if (value >> 64 > 0) {
|
||||
value >>= 64;
|
||||
result += 64;
|
||||
}
|
||||
if (value >> 32 > 0) {
|
||||
value >>= 32;
|
||||
result += 32;
|
||||
}
|
||||
if (value >> 16 > 0) {
|
||||
value >>= 16;
|
||||
result += 16;
|
||||
}
|
||||
if (value >> 8 > 0) {
|
||||
value >>= 8;
|
||||
result += 8;
|
||||
}
|
||||
if (value >> 4 > 0) {
|
||||
value >>= 4;
|
||||
result += 4;
|
||||
}
|
||||
if (value >> 2 > 0) {
|
||||
value >>= 2;
|
||||
result += 2;
|
||||
}
|
||||
if (value >> 1 > 0) {
|
||||
result += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the log in base 2, following the selected rounding direction, of a positive value.
|
||||
* Returns 0 if given 0.
|
||||
*/
|
||||
function log2(uint256 value, Rounding rounding) internal pure returns (uint256) {
|
||||
unchecked {
|
||||
uint256 result = log2(value);
|
||||
return result + (rounding == Rounding.Up && 1 << result < value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the log in base 10, rounded down, of a positive value.
|
||||
* Returns 0 if given 0.
|
||||
*/
|
||||
function log10(uint256 value) internal pure returns (uint256) {
|
||||
uint256 result = 0;
|
||||
unchecked {
|
||||
if (value >= 10**64) {
|
||||
value /= 10**64;
|
||||
result += 64;
|
||||
}
|
||||
if (value >= 10**32) {
|
||||
value /= 10**32;
|
||||
result += 32;
|
||||
}
|
||||
if (value >= 10**16) {
|
||||
value /= 10**16;
|
||||
result += 16;
|
||||
}
|
||||
if (value >= 10**8) {
|
||||
value /= 10**8;
|
||||
result += 8;
|
||||
}
|
||||
if (value >= 10**4) {
|
||||
value /= 10**4;
|
||||
result += 4;
|
||||
}
|
||||
if (value >= 10**2) {
|
||||
value /= 10**2;
|
||||
result += 2;
|
||||
}
|
||||
if (value >= 10**1) {
|
||||
result += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
|
||||
* Returns 0 if given 0.
|
||||
*/
|
||||
function log10(uint256 value, Rounding rounding) internal pure returns (uint256) {
|
||||
unchecked {
|
||||
uint256 result = log10(value);
|
||||
return result + (rounding == Rounding.Up && 10**result < value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the log in base 256, rounded down, of a positive value.
|
||||
* Returns 0 if given 0.
|
||||
*
|
||||
* Adding one to the result gives the number of pairs of hex symbols needed to represent `value` as a hex string.
|
||||
*/
|
||||
function log256(uint256 value) internal pure returns (uint256) {
|
||||
uint256 result = 0;
|
||||
unchecked {
|
||||
if (value >> 128 > 0) {
|
||||
value >>= 128;
|
||||
result += 16;
|
||||
}
|
||||
if (value >> 64 > 0) {
|
||||
value >>= 64;
|
||||
result += 8;
|
||||
}
|
||||
if (value >> 32 > 0) {
|
||||
value >>= 32;
|
||||
result += 4;
|
||||
}
|
||||
if (value >> 16 > 0) {
|
||||
value >>= 16;
|
||||
result += 2;
|
||||
}
|
||||
if (value >> 8 > 0) {
|
||||
result += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev Return the log in base 10, following the selected rounding direction, of a positive value.
|
||||
* Returns 0 if given 0.
|
||||
*/
|
||||
function log256(uint256 value, Rounding rounding) internal pure returns (uint256) {
|
||||
unchecked {
|
||||
uint256 result = log256(value);
|
||||
return result + (rounding == Rounding.Up && 1 << (result * 8) < value ? 1 : 0);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,414 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import copy
|
||||
import json
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
from nucypher.policy.conditions.context import (
|
||||
USER_ADDRESS_CONTEXT,
|
||||
ContextVariableVerificationFailed,
|
||||
InvalidContextVariableData,
|
||||
RequiredContextVariable,
|
||||
_recover_user_address,
|
||||
)
|
||||
from nucypher.policy.conditions.evm import RPCCondition, get_context_value
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo, ReturnValueTest
|
||||
from tests.integration.characters.test_bob_handles_frags import _make_message_kits
|
||||
|
||||
VALID_USER_ADDRESS_CONTEXT = {
|
||||
USER_ADDRESS_CONTEXT: {
|
||||
"signature": "0x488a7acefdc6d098eedf73cdfd379777c0f4a4023a660d350d3bf309a51dd4251abaad9cdd11b71c400cfb4625c14ca142f72b39165bd980c8da1ea32892ff071c",
|
||||
"address": "0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E",
|
||||
"typedData": {
|
||||
"primaryType": "Wallet",
|
||||
"types": {
|
||||
"EIP712Domain": [
|
||||
{"name": "name", "type": "string"},
|
||||
{"name": "version", "type": "string"},
|
||||
{"name": "chainId", "type": "uint256"},
|
||||
{"name": "salt", "type": "bytes32"},
|
||||
],
|
||||
"Wallet": [
|
||||
{"name": "address", "type": "string"},
|
||||
{"name": "blockNumber", "type": "uint256"},
|
||||
{"name": "blockHash", "type": "bytes32"},
|
||||
{"name": "signatureText", "type": "string"},
|
||||
],
|
||||
},
|
||||
"domain": {
|
||||
"name": "tDec",
|
||||
"version": "1",
|
||||
"chainId": 80001,
|
||||
"salt": "0x3e6365d35fd4e53cbc00b080b0742b88f8b735352ea54c0534ed6a2e44a83ff0",
|
||||
},
|
||||
"message": {
|
||||
"address": "0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E",
|
||||
"blockNumber": 28117088,
|
||||
"blockHash": "0x104dfae58be4a9b15d59ce447a565302d5658914f1093f10290cd846fbe258b7",
|
||||
"signatureText": "I'm the owner of address 0x5ce9454909639D2D17A3F753ce7d93fa0b9aB12E as of block number 28117088",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def _dont_validate_user_address(context_variable: str, **context):
|
||||
if context_variable == USER_ADDRESS_CONTEXT:
|
||||
return context[USER_ADDRESS_CONTEXT]["address"]
|
||||
return get_context_value(context_variable, **context)
|
||||
|
||||
|
||||
def test_required_context_variable(
|
||||
testerchain, custom_context_variable_erc20_condition
|
||||
):
|
||||
with pytest.raises(RequiredContextVariable):
|
||||
custom_context_variable_erc20_condition.verify(
|
||||
provider=testerchain.provider
|
||||
) # no context
|
||||
|
||||
|
||||
@pytest.mark.parametrize("expected_entry", ["address", "signature", "typedData"])
|
||||
def test_user_address_context_missing_required_entries(expected_entry):
|
||||
context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
|
||||
del context[USER_ADDRESS_CONTEXT][expected_entry]
|
||||
with pytest.raises(InvalidContextVariableData):
|
||||
_recover_user_address(**context)
|
||||
|
||||
|
||||
def test_user_address_context_invalid_eip712_typed_data():
|
||||
# invalid typed data
|
||||
context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
|
||||
context[USER_ADDRESS_CONTEXT]["typedData"] = dict(
|
||||
randomSaying="Comparison is the thief of joy." # -– Theodore Roosevelt
|
||||
)
|
||||
with pytest.raises(InvalidContextVariableData):
|
||||
_recover_user_address(**context)
|
||||
|
||||
|
||||
def test_user_address_context_variable_verification(testerchain):
|
||||
# valid user address context - signature matches address
|
||||
address = _recover_user_address(**VALID_USER_ADDRESS_CONTEXT)
|
||||
assert address == VALID_USER_ADDRESS_CONTEXT[USER_ADDRESS_CONTEXT]["address"]
|
||||
|
||||
# invalid user address context - signature does not match address
|
||||
# internals are mutable - deepcopy
|
||||
mismatch_with_address_context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
|
||||
mismatch_with_address_context[USER_ADDRESS_CONTEXT][
|
||||
"address"
|
||||
] = testerchain.etherbase_account
|
||||
with pytest.raises(ContextVariableVerificationFailed):
|
||||
_recover_user_address(**mismatch_with_address_context)
|
||||
|
||||
# invalid user address context - signature does not match address
|
||||
# internals are mutable - deepcopy
|
||||
mismatch_with_address_context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
|
||||
signature = (
|
||||
"0x93252ddff5f90584b27b5eef1915b23a8b01a703be56c8bf0660647c15cb75e9"
|
||||
"1983bde9877eaad11da5a3ebc9b64957f1c182536931f9844d0c600f0c41293d1b"
|
||||
)
|
||||
mismatch_with_address_context[USER_ADDRESS_CONTEXT]["signature"] = signature
|
||||
with pytest.raises(ContextVariableVerificationFailed):
|
||||
_recover_user_address(**mismatch_with_address_context)
|
||||
|
||||
# invalid signature
|
||||
# internals are mutable - deepcopy
|
||||
invalid_signature_context = copy.deepcopy(VALID_USER_ADDRESS_CONTEXT)
|
||||
invalid_signature_context[USER_ADDRESS_CONTEXT][
|
||||
"signature"
|
||||
] = "0xdeadbeef" # invalid signature
|
||||
with pytest.raises(ContextVariableVerificationFailed):
|
||||
_recover_user_address(**invalid_signature_context)
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"nucypher.policy.conditions.evm.get_context_value",
|
||||
side_effect=_dont_validate_user_address,
|
||||
)
|
||||
def test_rpc_condition_evaluation(get_context_value_mock, testerchain, rpc_condition):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": testerchain.unassigned_accounts[0]}}
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
assert call_result == Web3.to_wei(
|
||||
1_000_000, "ether"
|
||||
) # same value used in rpc_condition fixture
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"nucypher.policy.conditions.evm.get_context_value",
|
||||
side_effect=_dont_validate_user_address,
|
||||
)
|
||||
def test_rpc_condition_evaluation_with_context_var_in_return_value_test(
|
||||
get_context_value_mock, testerchain
|
||||
):
|
||||
account, *other_accounts = testerchain.client.accounts
|
||||
balance = testerchain.client.get_balance(account)
|
||||
|
||||
# we have balance stored, use for rpc condition with context variable
|
||||
rpc_condition = RPCCondition(
|
||||
method="eth_getBalance",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest(
|
||||
"==", ":balanceContextVar"
|
||||
), # user-defined context var
|
||||
parameters=[USER_ADDRESS_CONTEXT],
|
||||
)
|
||||
context = {
|
||||
USER_ADDRESS_CONTEXT: {"address": account},
|
||||
":balanceContextVar": balance,
|
||||
}
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
assert call_result == balance
|
||||
|
||||
# modify balance to make it false
|
||||
invalid_balance = balance + 1
|
||||
context[":balanceContextVar"] = invalid_balance
|
||||
condition_result, call_result = rpc_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is False
|
||||
assert call_result != invalid_balance
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"nucypher.policy.conditions.evm.get_context_value",
|
||||
side_effect=_dont_validate_user_address,
|
||||
)
|
||||
def test_erc20_evm_condition_evaluation(
|
||||
get_context_value_mock, testerchain, erc20_evm_condition
|
||||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": testerchain.unassigned_accounts[0]}}
|
||||
condition_result, call_result = erc20_evm_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
|
||||
context[USER_ADDRESS_CONTEXT]["address"] = testerchain.etherbase_account
|
||||
condition_result, call_result = erc20_evm_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is False
|
||||
|
||||
|
||||
def test_erc20_evm_condition_evaluation_with_custom_context_variable(
|
||||
testerchain, custom_context_variable_erc20_condition
|
||||
):
|
||||
context = {":addressToUse": testerchain.unassigned_accounts[0]}
|
||||
condition_result, call_result = custom_context_variable_erc20_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
|
||||
context[":addressToUse"] = testerchain.etherbase_account
|
||||
condition_result, call_result = custom_context_variable_erc20_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is False
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"nucypher.policy.conditions.evm.get_context_value",
|
||||
side_effect=_dont_validate_user_address,
|
||||
)
|
||||
def test_erc721_evm_condition_owner_evaluation(
|
||||
get_context_value_mock, testerchain, test_registry, erc721_evm_condition_owner
|
||||
):
|
||||
account, *other_accounts = testerchain.client.accounts
|
||||
# valid owner of nft
|
||||
context = {
|
||||
USER_ADDRESS_CONTEXT: {"address": account},
|
||||
":tokenId": 1, # valid token id
|
||||
}
|
||||
condition_result, call_result = erc721_evm_condition_owner.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
assert call_result == account
|
||||
|
||||
# invalid token id
|
||||
with pytest.raises(RPCCondition.RPCExecutionFailed):
|
||||
context[":tokenId"] = 255
|
||||
_, _ = erc721_evm_condition_owner.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
|
||||
# invalid owner of nft
|
||||
other_account = other_accounts[0]
|
||||
context = {
|
||||
USER_ADDRESS_CONTEXT: {"address": other_account},
|
||||
":tokenId": 1, # valid token id
|
||||
}
|
||||
condition_result, call_result = erc721_evm_condition_owner.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is False
|
||||
assert call_result != other_account
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"nucypher.policy.conditions.evm.get_context_value",
|
||||
side_effect=_dont_validate_user_address,
|
||||
)
|
||||
def test_erc721_evm_condition_balanceof_evaluation(
|
||||
get_context_value_mock, testerchain, test_registry, erc721_evm_condition_balanceof
|
||||
):
|
||||
account, *other_accounts = testerchain.client.accounts
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": account}} # owner of NFT
|
||||
condition_result, call_result = erc721_evm_condition_balanceof.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert condition_result is True
|
||||
|
||||
# invalid owner of nft
|
||||
other_account = other_accounts[0] # not an owner of NFT
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": other_account}}
|
||||
condition_result, call_result = erc721_evm_condition_balanceof.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert not condition_result
|
||||
|
||||
|
||||
def test_subscription_manager_is_active_policy_condition_evaluation(
|
||||
testerchain,
|
||||
enacted_blockchain_policy,
|
||||
subscription_manager_is_active_policy_condition,
|
||||
):
|
||||
context = {
|
||||
":hrac": bytes(enacted_blockchain_policy.hrac)
|
||||
} # user-defined context var
|
||||
condition_result, call_result = subscription_manager_is_active_policy_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert call_result
|
||||
assert condition_result is True
|
||||
|
||||
# non-active policy hrac
|
||||
context[":hrac"] = os.urandom(16)
|
||||
condition_result, call_result = subscription_manager_is_active_policy_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert not call_result
|
||||
assert not condition_result
|
||||
|
||||
|
||||
def test_subscription_manager_get_policy_policy_struct_condition_evaluation(
|
||||
testerchain,
|
||||
enacted_blockchain_policy,
|
||||
subscription_manager_get_policy_zeroized_policy_struct_condition,
|
||||
):
|
||||
|
||||
# zeroized policy struct
|
||||
zeroized_policy_struct = (
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
)
|
||||
context = {
|
||||
":hrac": bytes(enacted_blockchain_policy.hrac),
|
||||
":expectedPolicyStruct": zeroized_policy_struct,
|
||||
} # user-defined context vars
|
||||
condition_result, call_result = subscription_manager_get_policy_zeroized_policy_struct_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert call_result != zeroized_policy_struct
|
||||
assert not condition_result # not zeroized policy
|
||||
|
||||
# unknown policy hrac
|
||||
context[":hrac"] = os.urandom(16)
|
||||
condition_result, call_result = subscription_manager_get_policy_zeroized_policy_struct_condition.verify(
|
||||
provider=testerchain.provider, **context
|
||||
)
|
||||
assert call_result == zeroized_policy_struct
|
||||
assert condition_result is True # zeroized policy was indeed returned
|
||||
|
||||
|
||||
def test_time_condition_evaluation(testerchain, timelock_condition):
|
||||
condition_result, call_result = timelock_condition.verify(
|
||||
provider=testerchain.provider
|
||||
)
|
||||
assert condition_result is True
|
||||
|
||||
|
||||
def test_simple_compound_conditions_evaluation(testerchain):
|
||||
# TODO Improve internals of evaluation here (natural vs recursive approach)
|
||||
conditions = [
|
||||
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
|
||||
{'operator': 'and'},
|
||||
{'returnValueTest': {'value': '99999999999999999', 'comparator': '<'}, 'method': 'timelock'},
|
||||
{'operator': 'and'},
|
||||
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'}
|
||||
]
|
||||
conditions = json.dumps(conditions)
|
||||
lingo = ConditionLingo.from_json(conditions)
|
||||
result = lingo.eval()
|
||||
assert result is True
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"nucypher.policy.conditions.evm.get_context_value",
|
||||
side_effect=_dont_validate_user_address,
|
||||
)
|
||||
def test_onchain_conditions_lingo_evaluation(
|
||||
get_context_value_mock,
|
||||
testerchain,
|
||||
lingo,
|
||||
):
|
||||
context = {USER_ADDRESS_CONTEXT: {"address": testerchain.etherbase_account}}
|
||||
result = lingo.eval(provider=testerchain.provider, **context)
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_single_retrieve_with_onchain_conditions(enacted_blockchain_policy, blockchain_bob, blockchain_ursulas):
|
||||
blockchain_bob.start_learning_loop()
|
||||
conditions = [
|
||||
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
|
||||
{'operator': 'and'},
|
||||
{"chain": "testerchain",
|
||||
"method": "eth_getBalance",
|
||||
"parameters": [
|
||||
blockchain_bob.checksum_address,
|
||||
"latest"
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": ">=",
|
||||
"value": "10000000000000"
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
messages, message_kits = _make_message_kits(enacted_blockchain_policy.public_key, conditions)
|
||||
policy_info_kwargs = dict(
|
||||
encrypted_treasure_map=enacted_blockchain_policy.treasure_map,
|
||||
alice_verifying_key=enacted_blockchain_policy.publisher_verifying_key,
|
||||
)
|
||||
|
||||
cleartexts = blockchain_bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
**policy_info_kwargs,
|
||||
)
|
||||
|
||||
assert cleartexts == messages
|
|
@ -22,17 +22,23 @@ from nucypher.blockchain.eth.interfaces import BlockchainDeployerInterface
|
|||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.sol.compile.compile import multiversion_compile
|
||||
from nucypher.blockchain.eth.sol.compile.constants import TEST_MULTIVERSION_CONTRACTS, SOLIDITY_SOURCE_ROOT
|
||||
from nucypher.blockchain.eth.sol.compile.constants import (
|
||||
SOLIDITY_SOURCE_ROOT,
|
||||
TEST_MULTIVERSION_CONTRACTS,
|
||||
)
|
||||
from nucypher.blockchain.eth.sol.compile.types import SourceBundle
|
||||
from nucypher.crypto.powers import TransactingPower
|
||||
from tests.constants import (
|
||||
DEVELOPMENT_ETH_AIRDROP_AMOUNT,
|
||||
INSECURE_DEVELOPMENT_PASSWORD,
|
||||
NUMBER_OF_ETH_TEST_ACCOUNTS,
|
||||
NUMBER_OF_STAKING_PROVIDERS_IN_BLOCKCHAIN_TESTS,
|
||||
NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS, INSECURE_DEVELOPMENT_PASSWORD
|
||||
NUMBER_OF_URSULAS_IN_BLOCKCHAIN_TESTS,
|
||||
)
|
||||
|
||||
# Prevents TesterBlockchain to be picked up by py.test as a test class
|
||||
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain, free_gas_price_strategy
|
||||
from tests.utils.blockchain import TesterBlockchain as _TesterBlockchain
|
||||
from tests.utils.blockchain import free_gas_price_strategy
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
|
@ -53,8 +59,8 @@ def test_testerchain_creation(testerchain, another_testerchain):
|
|||
assert 'tester' in chain.eth_provider_uri
|
||||
|
||||
# ... and that there are already some blocks mined
|
||||
chain.w3.eth.web3.testing.mine(1)
|
||||
assert chain.w3.eth.blockNumber > 0
|
||||
chain.w3.eth.w3.testing.mine(1)
|
||||
assert chain.w3.eth.block_number > 0
|
||||
|
||||
# Check that we have enough test accounts
|
||||
assert len(chain.client.accounts) >= NUMBER_OF_ETH_TEST_ACCOUNTS
|
||||
|
|
|
@ -63,8 +63,8 @@ def test_transacting_power_sign_transaction(testerchain):
|
|||
signer=Web3Signer(testerchain.client),
|
||||
account=eth_address)
|
||||
|
||||
transaction_dict = {'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address),
|
||||
'gasPrice': testerchain.client.w3.eth.gasPrice,
|
||||
transaction_dict = {'nonce': testerchain.client.w3.eth.get_transaction_count(eth_address),
|
||||
'gasPrice': testerchain.client.w3.eth.gas_price,
|
||||
'gas': 100000,
|
||||
'from': eth_address,
|
||||
'to': testerchain.unassigned_accounts[1],
|
||||
|
@ -94,12 +94,12 @@ def test_transacting_power_sign_agent_transaction(testerchain, agency, test_regi
|
|||
contract_function = agent.contract.functions.confirmOperatorAddress()
|
||||
|
||||
payload = {'chainId': int(testerchain.client.chain_id),
|
||||
'nonce': testerchain.client.w3.eth.getTransactionCount(testerchain.etherbase_account),
|
||||
'nonce': testerchain.client.w3.eth.get_transaction_count(testerchain.etherbase_account),
|
||||
'from': testerchain.etherbase_account,
|
||||
'gasPrice': testerchain.client.gas_price,
|
||||
'gas': 500_000}
|
||||
|
||||
unsigned_transaction = contract_function.buildTransaction(payload)
|
||||
unsigned_transaction = contract_function.build_transaction(payload)
|
||||
|
||||
# Sign with Transacting Power
|
||||
transacting_power = TransactingPower(password=INSECURE_DEVELOPMENT_PASSWORD,
|
||||
|
|
|
@ -25,6 +25,7 @@ from nucypher.characters.lawful import Enrico, Ursula
|
|||
from nucypher.characters.unlawful import Amonia
|
||||
|
||||
|
||||
@pytest.mark.skip('FIXME - DISABLED FOR TDEC ADAPTATION DEVELOPMENT')
|
||||
def test_try_to_post_free_service_by_hacking_enact(blockchain_ursulas,
|
||||
blockchain_alice,
|
||||
blockchain_bob,
|
||||
|
@ -34,7 +35,7 @@ def test_try_to_post_free_service_by_hacking_enact(blockchain_ursulas,
|
|||
This time we won't rely on the tabulation in Alice's enact() to catch the problem.
|
||||
"""
|
||||
amonia = Amonia.from_lawful_alice(blockchain_alice)
|
||||
# Setup the policy details
|
||||
# Set up the policy details
|
||||
shares = 3
|
||||
policy_end_datetime = maya.now() + datetime.timedelta(days=35)
|
||||
label = b"another_path"
|
||||
|
|
|
@ -54,8 +54,8 @@ def test_character_transacting_power_signing(testerchain, agency, test_registry)
|
|||
assert is_verified is True
|
||||
|
||||
# Sign Transaction
|
||||
transaction_dict = {'nonce': testerchain.client.w3.eth.getTransactionCount(eth_address),
|
||||
'gasPrice': testerchain.client.w3.eth.gasPrice,
|
||||
transaction_dict = {'nonce': testerchain.client.w3.eth.get_transaction_count(eth_address),
|
||||
'gasPrice': testerchain.client.w3.eth.gas_price,
|
||||
'gas': 100000,
|
||||
'from': eth_address,
|
||||
'to': testerchain.unassigned_accounts[1],
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
|
||||
|
||||
@pytest.fixture(scope='module')
|
||||
|
@ -31,10 +31,8 @@ def ursula(blockchain_ursulas):
|
|||
@pytest.fixture(scope='module')
|
||||
def client(ursula):
|
||||
db_fd, db_path = tempfile.mkstemp()
|
||||
ursula.rest_app.config['DATABASE'] = Path(db_path)
|
||||
ursula.rest_app.config['TESTING'] = True
|
||||
with ursula.rest_app.test_client() as client:
|
||||
yield client
|
||||
ursula.rest_app.config.update({"TESTING": True, "DATABASE": Path(db_path)})
|
||||
yield ursula.rest_app.test_client()
|
||||
os.close(db_fd)
|
||||
ursula.rest_app.config['DATABASE'].unlink()
|
||||
|
||||
|
|
|
@ -30,8 +30,12 @@ from web3 import Web3
|
|||
|
||||
from nucypher.cli.main import nucypher_cli
|
||||
from nucypher.config.characters import AliceConfiguration, BobConfiguration
|
||||
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD, TEMPORARY_DOMAIN, \
|
||||
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD, NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
|
||||
from nucypher.config.constants import (
|
||||
NUCYPHER_ENVVAR_KEYSTORE_PASSWORD,
|
||||
TEMPORARY_DOMAIN,
|
||||
NUCYPHER_ENVVAR_ALICE_ETH_PASSWORD,
|
||||
NUCYPHER_ENVVAR_BOB_ETH_PASSWORD
|
||||
)
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
from tests.constants import INSECURE_DEVELOPMENT_PASSWORD, TEST_ETH_PROVIDER_URI
|
||||
|
||||
|
@ -329,7 +333,7 @@ def run_entire_cli_lifecycle(click_runner,
|
|||
grant_args += ('--federated-only',)
|
||||
else:
|
||||
grant_args += ('--eth-provider', TEST_ETH_PROVIDER_URI,
|
||||
'--value', Web3.toWei(9, 'gwei'))
|
||||
'--value', Web3.to_wei(9, 'gwei'))
|
||||
|
||||
grant_result = click_runner.invoke(nucypher_cli, grant_args, catch_exceptions=False, env=envvars)
|
||||
assert grant_result.exit_code == 0, (grant_result.output, grant_result.exception)
|
||||
|
|
|
@ -55,10 +55,10 @@ def mock_funded_account_password_keystore(tmp_path_factory, testerchain, thresho
|
|||
json.dump(account.encrypt(password), open(path, 'x+t'))
|
||||
|
||||
testerchain.wait_for_receipt(
|
||||
testerchain.client.w3.eth.sendTransaction({
|
||||
testerchain.client.w3.eth.send_transaction({
|
||||
'to': account.address,
|
||||
'from': testerchain.etherbase_account,
|
||||
'value': Web3.toWei('1', 'ether')}))
|
||||
'value': Web3.to_wei('1', 'ether')}))
|
||||
|
||||
# initialize threshold stake
|
||||
provider_address = testerchain.unassigned_accounts[0]
|
||||
|
@ -151,7 +151,7 @@ def test_ursula_and_local_keystore_signer_integration(click_runner,
|
|||
# ...and that transactions are signed by the keystore signer
|
||||
txhash = ursula.confirm_address()
|
||||
receipt = testerchain.wait_for_receipt(txhash)
|
||||
transaction_data = testerchain.client.w3.eth.getTransaction(receipt['transactionHash'])
|
||||
transaction_data = testerchain.client.w3.eth.get_transaction(receipt['transactionHash'])
|
||||
assert transaction_data['from'] == worker_account.address
|
||||
finally:
|
||||
ursula.stop()
|
||||
|
|
|
@ -77,7 +77,8 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
blockchain_porter_rpc_controller,
|
||||
random_blockchain_policy,
|
||||
blockchain_bob,
|
||||
blockchain_alice):
|
||||
blockchain_alice,
|
||||
random_context):
|
||||
method = 'retrieve_cfrags'
|
||||
|
||||
# Setup
|
||||
|
@ -102,6 +103,19 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
expected_results = blockchain_porter.retrieve_cfrags(**retrieve_args)
|
||||
assert len(retrieval_results) == len(expected_results)
|
||||
|
||||
# Use context
|
||||
retrieve_cfrags_params_with_context, _ = retrieval_request_setup(enacted_policy,
|
||||
blockchain_bob,
|
||||
blockchain_alice,
|
||||
context=random_context,
|
||||
encode_for_rest=True)
|
||||
request_data = {'method': method, 'params': retrieve_cfrags_params_with_context}
|
||||
response = blockchain_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
|
||||
retrieval_results = response.data['result']['retrieval_results']
|
||||
assert retrieval_results
|
||||
|
||||
# Failure - use encrypted treasure map
|
||||
failure_retrieve_cfrags_params = dict(retrieve_cfrags_params)
|
||||
failure_retrieve_cfrags_params['treasure_map'] = b64encode(os.urandom(32)).decode()
|
||||
|
|
|
@ -18,16 +18,24 @@
|
|||
import json
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from nucypher_core import RetrievalKit
|
||||
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from nucypher.control.specifications.fields import JSON
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.policy.kits import PolicyMessageKit, RetrievalResult
|
||||
from nucypher.utilities.porter.control.specifications.fields import RetrievalResultSchema, RetrievalKit as RetrievalKitField
|
||||
from nucypher.utilities.porter.control.specifications.fields import (
|
||||
RetrievalKit as RetrievalKitField,
|
||||
)
|
||||
from nucypher.utilities.porter.control.specifications.fields import (
|
||||
RetrievalOutcomeSchema,
|
||||
)
|
||||
from tests.utils.middleware import MockRestMiddleware
|
||||
from tests.utils.policy import retrieval_request_setup, retrieval_params_decode_from_rest
|
||||
from tests.utils.policy import (
|
||||
retrieval_params_decode_from_rest,
|
||||
retrieval_request_setup,
|
||||
)
|
||||
|
||||
|
||||
def test_get_ursulas(blockchain_porter_web_controller, blockchain_ursulas):
|
||||
|
@ -91,7 +99,8 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
blockchain_porter_web_controller,
|
||||
random_blockchain_policy,
|
||||
blockchain_bob,
|
||||
blockchain_alice):
|
||||
blockchain_alice,
|
||||
random_context):
|
||||
# Send bad data to assert error return
|
||||
response = blockchain_porter_web_controller.post('/retrieve_cfrags', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
@ -102,11 +111,13 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware)
|
||||
|
||||
original_message = b"Those who say it can't be done are usually interrupted by others doing it." # - James Baldwin
|
||||
retrieve_cfrags_params, message_kit = retrieval_request_setup(enacted_policy,
|
||||
retrieve_cfrags_params, message_kits = retrieval_request_setup(enacted_policy,
|
||||
blockchain_bob,
|
||||
blockchain_alice,
|
||||
original_message=original_message,
|
||||
specific_messages=[original_message],
|
||||
encode_for_rest=True)
|
||||
assert len(message_kits) == 1
|
||||
message_kit = message_kits[0]
|
||||
|
||||
#
|
||||
# Success
|
||||
|
@ -129,7 +140,7 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
policy_encrypting_key=enacted_policy.public_key,
|
||||
threshold=treasure_map.threshold)
|
||||
assert len(retrieval_results) == 1
|
||||
field = RetrievalResultSchema()
|
||||
field = RetrievalOutcomeSchema()
|
||||
cfrags = field.load(retrieval_results[0])['cfrags']
|
||||
verified_cfrags = {}
|
||||
for ursula, cfrag in cfrags.items():
|
||||
|
@ -167,16 +178,20 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
retrieval_results = response_data['result']['retrieval_results']
|
||||
assert retrieval_results
|
||||
assert len(retrieval_results) == 2
|
||||
for i in range(0, 2):
|
||||
assert len(retrieval_results[i]["cfrags"]) > 0
|
||||
assert len(retrieval_results[i]["errors"]) == 0
|
||||
|
||||
#
|
||||
# Try same retrieval (with multiple retrieval kits) using query parameters
|
||||
# Use context
|
||||
#
|
||||
url_retrieve_params = dict(multiple_retrieval_kits_params) # use multiple kit params from above
|
||||
# adjust parameter for url query parameter list format
|
||||
url_retrieve_params['retrieval_kits'] = ",".join(url_retrieve_params['retrieval_kits']) # adjust for list
|
||||
response = blockchain_porter_web_controller.post(f'/retrieve_cfrags'
|
||||
f'?{urlencode(url_retrieve_params)}')
|
||||
context_field = JSON()
|
||||
multiple_retrieval_kits_params['context'] = context_field._serialize(random_context, attr=None, obj=None)
|
||||
|
||||
response = blockchain_porter_web_controller.post('/retrieve_cfrags', data=json.dumps(
|
||||
multiple_retrieval_kits_params))
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
retrieval_results = response_data['result']['retrieval_results']
|
||||
assert retrieval_results
|
||||
|
|
|
@ -76,3 +76,22 @@ def test_retrieve_cfrags(blockchain_porter,
|
|||
# use porter
|
||||
result = blockchain_porter.retrieve_cfrags(**retrieval_args)
|
||||
assert result
|
||||
|
||||
|
||||
def test_retrieve_cfrags_with_context(blockchain_porter,
|
||||
random_blockchain_policy,
|
||||
blockchain_bob,
|
||||
blockchain_alice,
|
||||
random_context):
|
||||
# Setup
|
||||
network_middleware = MockRestMiddleware()
|
||||
# enact new random policy since idle_blockchain_policy/enacted_blockchain_policy already modified in previous tests
|
||||
enacted_policy = random_blockchain_policy.enact(network_middleware=network_middleware)
|
||||
retrieval_args, _ = retrieval_request_setup(enacted_policy,
|
||||
blockchain_bob,
|
||||
blockchain_alice,
|
||||
context=random_context)
|
||||
|
||||
# use porter
|
||||
result = blockchain_porter.retrieve_cfrags(**retrieval_args)
|
||||
assert result
|
|
@ -65,7 +65,7 @@ MIN_STAKE_FOR_TESTS = NU(750_000, 'NU').to_units()
|
|||
|
||||
BONUS_TOKENS_FOR_TESTS = NU(150_000, 'NU').to_units()
|
||||
|
||||
DEVELOPMENT_ETH_AIRDROP_AMOUNT = int(Web3().toWei(100, 'ether'))
|
||||
DEVELOPMENT_ETH_AIRDROP_AMOUNT = int(Web3().to_wei(100, 'ether'))
|
||||
|
||||
NUMBER_OF_ALLOCATIONS_IN_TESTS = 50 # TODO: Move to constants
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
|
|||
# Staking provider bonds operator and now operator can make a confirmation
|
||||
tx = pre_application.functions.bondOperator(staking_provider_3, operator1).transact({'from': owner3})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_3).call() == operator1
|
||||
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == staking_provider_3
|
||||
assert not pre_application.functions.stakingProviderInfo(staking_provider_3).call()[CONFIRMATION_SLOT]
|
||||
|
@ -151,7 +151,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
|
|||
testerchain.time_travel(seconds=min_operator_seconds)
|
||||
tx = pre_application.functions.bondOperator(staking_provider_3, NULL_ADDRESS).transact({'from': staking_provider_3})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_3).call() == NULL_ADDRESS
|
||||
assert pre_application.functions.stakingProviderFromOperator(staking_provider_3).call() == NULL_ADDRESS
|
||||
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == NULL_ADDRESS
|
||||
|
@ -178,7 +178,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
|
|||
# The staking provider can bond now a new operator, without waiting additional time.
|
||||
tx = pre_application.functions.bondOperator(staking_provider_3, operator2).transact({'from': staking_provider_3})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_3).call() == operator2
|
||||
assert pre_application.functions.stakingProviderFromOperator(operator2).call() == staking_provider_3
|
||||
assert not pre_application.functions.stakingProviderInfo(staking_provider_3).call()[CONFIRMATION_SLOT]
|
||||
|
@ -208,7 +208,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
|
|||
# Another staker can bond a free operator
|
||||
tx = pre_application.functions.bondOperator(staking_provider_4, operator1).transact({'from': staking_provider_4})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_4).call() == operator1
|
||||
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == staking_provider_4
|
||||
assert not pre_application.functions.isOperatorConfirmed(operator1).call()
|
||||
|
@ -242,7 +242,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
|
|||
testerchain.time_travel(seconds=min_operator_seconds)
|
||||
tx = pre_application.functions.bondOperator(staking_provider_4, operator3).transact({'from': staking_provider_4})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_4).call() == operator3
|
||||
assert pre_application.functions.stakingProviderFromOperator(operator3).call() == staking_provider_4
|
||||
assert pre_application.functions.stakingProviderFromOperator(operator1).call() == NULL_ADDRESS
|
||||
|
@ -289,7 +289,7 @@ def test_bond_operator(testerchain, threshold_staking, pre_application, applicat
|
|||
tx = pre_application.functions.bondOperator(
|
||||
staking_provider_1, staking_provider_1).transact({'from': staking_provider_1})
|
||||
testerchain.wait_for_receipt(tx)
|
||||
timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
assert pre_application.functions.getOperatorFromStakingProvider(staking_provider_1).call() == staking_provider_1
|
||||
assert pre_application.functions.stakingProviderFromOperator(staking_provider_1).call() == staking_provider_1
|
||||
assert pre_application.functions.getStakingProvidersLength().call() == 3
|
||||
|
|
|
@ -308,7 +308,7 @@ def test_withdraw(testerchain, token, worklock, threshold_staking, escrow):
|
|||
# Set vesting for the staker
|
||||
tx = threshold_staking.functions.setStakedNu(staking_provider, value // 2).transact()
|
||||
testerchain.wait_for_receipt(tx)
|
||||
now = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
now = testerchain.w3.eth.get_block('latest').timestamp
|
||||
release_timestamp = now + ONE_HOUR
|
||||
rate = 2 * value // ONE_HOUR
|
||||
tx = escrow.functions.setupVesting([staker], [release_timestamp], [rate]).transact({'from': creator})
|
||||
|
@ -397,7 +397,7 @@ def test_vesting(testerchain, token, worklock, escrow):
|
|||
tx = worklock.functions.depositFromWorkLock(staker1, value, 0).transact()
|
||||
testerchain.wait_for_receipt(tx)
|
||||
|
||||
now = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
now = testerchain.w3.eth.get_block('latest').timestamp
|
||||
release_timestamp = now + ONE_HOUR
|
||||
rate = 2 * value // ONE_HOUR
|
||||
|
||||
|
@ -455,7 +455,7 @@ def test_vesting(testerchain, token, worklock, escrow):
|
|||
assert event_args['releaseRate'] == rate
|
||||
|
||||
testerchain.time_travel(seconds=40 * 60)
|
||||
now = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
now = testerchain.w3.eth.get_block('latest').timestamp
|
||||
vested = (release_timestamp - now) * rate
|
||||
assert escrow.functions.getUnvestedTokens(staker1).call() == vested
|
||||
|
||||
|
@ -476,7 +476,7 @@ def test_vesting(testerchain, token, worklock, escrow):
|
|||
tx = worklock.functions.depositFromWorkLock(staker4, value, 0).transact()
|
||||
testerchain.wait_for_receipt(tx)
|
||||
|
||||
now = testerchain.w3.eth.getBlock('latest').timestamp + 1 # +1 sec because tx will be executed in new block
|
||||
now = testerchain.w3.eth.get_block('latest').timestamp + 1 # +1 sec because tx will be executed in new block
|
||||
release_timestamp_2 = now + ONE_HOUR
|
||||
release_timestamp_3 = now + 2 * ONE_HOUR
|
||||
release_timestamp_4 = now + 2 * ONE_HOUR
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
{
|
||||
"SubscriptionManagerPayment": {
|
||||
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
"chain": "polygon",
|
||||
"method": "isValidPolicy",
|
||||
"parameters": [
|
||||
":hrac"
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": "==",
|
||||
"value": true
|
||||
}
|
||||
},
|
||||
"ERC1155_balance": {
|
||||
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
"standardContractType": "ERC1155",
|
||||
"chain": "ethereum",
|
||||
"method": "balanceOf",
|
||||
"parameters": [
|
||||
":userAddress",
|
||||
9541
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": ">",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ERC1155_balance_batch": {
|
||||
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
"standardContractType": "ERC1155",
|
||||
"chain": "ethereum",
|
||||
"method": "balanceOfBatch",
|
||||
"parameters": [
|
||||
[":userAddress",":userAddress",":userAddress",":userAddress"],
|
||||
[1,2,10003,10004]
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": ">",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ERC721_ownership": {
|
||||
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
"standardContractType": "ERC721",
|
||||
"chain": "ethereum",
|
||||
"method": "ownerOf",
|
||||
"parameters": [
|
||||
5954
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": "=",
|
||||
"value": ":userAddress"
|
||||
}
|
||||
},
|
||||
"ERC721_balance": {
|
||||
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
"standardContractType": "ERC721",
|
||||
"chain": "ethereum",
|
||||
"method": "balanceOf",
|
||||
"parameters": [
|
||||
":userAddress"
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": ">",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ERC20_balance": {
|
||||
"contractAddress": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
"standardContractType": "ERC20",
|
||||
"chain": "ethereum",
|
||||
"method": "balanceOf",
|
||||
"parameters": [
|
||||
":userAddress"
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": ">",
|
||||
"value": 0
|
||||
}
|
||||
},
|
||||
"ETH_balance": {
|
||||
"contractAddress": "",
|
||||
"standardContractType": "",
|
||||
"chain": "ethereum",
|
||||
"method": "eth_getBalance",
|
||||
"parameters": [
|
||||
":userAddress",
|
||||
"latest"
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": ">=",
|
||||
"value": 10000000000000
|
||||
}
|
||||
},
|
||||
"specific_wallet_address": {
|
||||
"contractAddress": "",
|
||||
"standardContractType": "",
|
||||
"chain": "ethereum",
|
||||
"method": "",
|
||||
"parameters": [
|
||||
":userAddress"
|
||||
],
|
||||
"returnValueTest": {
|
||||
"comparator": "=",
|
||||
"value": "0xaDD9D957170dF6F33982001E4c22eCCdd5539118"
|
||||
}
|
||||
},
|
||||
"timestamp": {
|
||||
"contractAddress": "",
|
||||
"standardContractType": "timestamp",
|
||||
"chain": "ethereum",
|
||||
"method": "eth_getBlockByNumber",
|
||||
"parameters": ["latest"],
|
||||
"returnValueTest": {
|
||||
"comparator": ">=",
|
||||
"value": 1234567890
|
||||
}
|
||||
}
|
||||
}
|
|
@ -36,20 +36,28 @@ from web3.types import TxReceipt
|
|||
|
||||
from nucypher.blockchain.economics import Economics
|
||||
from nucypher.blockchain.eth.actors import Operator
|
||||
from nucypher.blockchain.eth.agents import ContractAgency, NucypherTokenAgent, PREApplicationAgent
|
||||
from nucypher.blockchain.eth.agents import (
|
||||
ContractAgency,
|
||||
NucypherTokenAgent,
|
||||
PREApplicationAgent,
|
||||
)
|
||||
from nucypher.blockchain.eth.deployers import (
|
||||
NucypherTokenDeployer,
|
||||
PREApplicationDeployer,
|
||||
SubscriptionManagerDeployer, NucypherTokenDeployer
|
||||
SubscriptionManagerDeployer,
|
||||
)
|
||||
from nucypher.blockchain.eth.interfaces import BlockchainInterfaceFactory
|
||||
from nucypher.blockchain.eth.registry import InMemoryContractRegistry, LocalContractRegistry
|
||||
from nucypher.blockchain.eth.registry import (
|
||||
InMemoryContractRegistry,
|
||||
LocalContractRegistry,
|
||||
)
|
||||
from nucypher.blockchain.eth.signers.software import Web3Signer
|
||||
from nucypher.blockchain.eth.token import NU
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from nucypher.config.characters import (
|
||||
AliceConfiguration,
|
||||
BobConfiguration,
|
||||
UrsulaConfiguration
|
||||
UrsulaConfiguration,
|
||||
)
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.control.emitters import StdoutEmitter
|
||||
|
@ -57,6 +65,7 @@ from nucypher.crypto.keystore import Keystore
|
|||
from nucypher.crypto.powers import TransactingPower
|
||||
from nucypher.datastore import datastore
|
||||
from nucypher.network.nodes import TEACHER_NODES
|
||||
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings, Logger
|
||||
from nucypher.utilities.porter.porter import Porter
|
||||
from tests.constants import (
|
||||
|
@ -65,7 +74,6 @@ from tests.constants import (
|
|||
BONUS_TOKENS_FOR_TESTS,
|
||||
DATETIME_FORMAT,
|
||||
DEVELOPMENT_ETH_AIRDROP_AMOUNT,
|
||||
DEVELOPMENT_TOKEN_AIRDROP_AMOUNT,
|
||||
INSECURE_DEVELOPMENT_PASSWORD,
|
||||
MIN_STAKE_FOR_TESTS,
|
||||
MOCK_CUSTOM_INSTALLATION_PATH,
|
||||
|
@ -73,8 +81,8 @@ from tests.constants import (
|
|||
MOCK_POLICY_DEFAULT_THRESHOLD,
|
||||
MOCK_REGISTRY_FILEPATH,
|
||||
NUMBER_OF_URSULAS_IN_DEVELOPMENT_NETWORK,
|
||||
TEST_ETH_PROVIDER_URI,
|
||||
TEST_GAS_LIMIT,
|
||||
TEST_ETH_PROVIDER_URI
|
||||
)
|
||||
from tests.mock.interfaces import MockBlockchain, mock_registry_source_manager
|
||||
from tests.mock.performance_mocks import (
|
||||
|
@ -86,21 +94,24 @@ from tests.mock.performance_mocks import (
|
|||
mock_record_fleet_state,
|
||||
mock_remember_node,
|
||||
mock_rest_app_creation,
|
||||
mock_verify_node
|
||||
mock_verify_node,
|
||||
)
|
||||
from tests.utils.blockchain import TesterBlockchain, token_airdrop
|
||||
from tests.utils.config import (
|
||||
make_alice_test_configuration,
|
||||
make_bob_test_configuration,
|
||||
make_ursula_test_configuration
|
||||
make_ursula_test_configuration,
|
||||
)
|
||||
from tests.utils.middleware import (
|
||||
MockRestMiddleware,
|
||||
MockRestMiddlewareForLargeFleetTests,
|
||||
)
|
||||
from tests.utils.middleware import MockRestMiddleware, MockRestMiddlewareForLargeFleetTests
|
||||
from tests.utils.policy import generate_random_label
|
||||
from tests.utils.ursula import (
|
||||
MOCK_KNOWN_URSULAS_CACHE,
|
||||
MOCK_URSULA_STARTING_PORT,
|
||||
make_decentralized_ursulas,
|
||||
make_federated_ursulas
|
||||
make_federated_ursulas,
|
||||
)
|
||||
|
||||
test_logger = Logger("test-logger")
|
||||
|
@ -294,7 +305,7 @@ def blockchain_treasure_map(enacted_blockchain_policy, blockchain_bob):
|
|||
def random_blockchain_policy(testerchain, blockchain_alice, blockchain_bob, application_economics):
|
||||
random_label = generate_random_label()
|
||||
seconds = 60 * 60 * 24 # TODO This needs to be better thought out...?
|
||||
now = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
now = testerchain.w3.eth.get_block('latest').timestamp
|
||||
expiration = maya.MayaDT(now).add(seconds=seconds)
|
||||
shares = 3
|
||||
threshold = 2
|
||||
|
@ -544,10 +555,10 @@ def testerchain(_testerchain) -> TesterBlockchain:
|
|||
|
||||
if spent > 0:
|
||||
tx = {'to': address, 'from': coinbase, 'value': spent}
|
||||
txhash = testerchain.w3.eth.sendTransaction(tx)
|
||||
txhash = testerchain.w3.eth.send_transaction(tx)
|
||||
|
||||
_receipt = testerchain.wait_for_receipt(txhash)
|
||||
eth_amount = Web3().fromWei(spent, 'ether')
|
||||
eth_amount = Web3().from_wei(spent, 'ether')
|
||||
testerchain.log.info("Airdropped {} ETH {} -> {}".format(eth_amount, tx['from'], tx['to']))
|
||||
|
||||
BlockchainInterfaceFactory.register_interface(interface=testerchain, force=True)
|
||||
|
@ -696,7 +707,7 @@ def blockchain_ursulas(testerchain, staking_providers, ursula_decentralized_test
|
|||
|
||||
@pytest.fixture(scope='module')
|
||||
def policy_rate():
|
||||
rate = Web3.toWei(21, 'gwei')
|
||||
rate = Web3.to_wei(21, 'gwei')
|
||||
return rate
|
||||
|
||||
|
||||
|
@ -755,9 +766,9 @@ def software_stakeholder(testerchain, agency, stakeholder_config_file_location,
|
|||
|
||||
tx = {'to': address,
|
||||
'from': testerchain.etherbase_account,
|
||||
'value': Web3.toWei('1', 'ether')}
|
||||
'value': Web3.to_wei('1', 'ether')}
|
||||
|
||||
txhash = testerchain.client.w3.eth.sendTransaction(tx)
|
||||
txhash = testerchain.client.w3.eth.send_transaction(tx)
|
||||
_receipt = testerchain.wait_for_receipt(txhash)
|
||||
|
||||
# Mock TransactingPower consumption (Etherbase)
|
||||
|
@ -798,9 +809,9 @@ def manual_operator(testerchain):
|
|||
|
||||
tx = {'to': address,
|
||||
'from': testerchain.etherbase_account,
|
||||
'value': Web3.toWei('1', 'ether')}
|
||||
'value': Web3.to_wei('1', 'ether')}
|
||||
|
||||
txhash = testerchain.client.w3.eth.sendTransaction(tx)
|
||||
txhash = testerchain.client.w3.eth.send_transaction(tx)
|
||||
_receipt = testerchain.wait_for_receipt(txhash)
|
||||
yield address
|
||||
|
||||
|
@ -1034,3 +1045,32 @@ def basic_auth_file(temp_dir_path):
|
|||
f.write("admin:$apr1$hlEpWVoI$0qjykXrvdZ0yO2TnBggQO0\n")
|
||||
yield basic_auth
|
||||
basic_auth.unlink()
|
||||
|
||||
|
||||
#
|
||||
# Condition Context
|
||||
#
|
||||
@pytest.fixture(scope='module')
|
||||
def random_context():
|
||||
context = {
|
||||
USER_ADDRESS_CONTEXT: {
|
||||
"signature": "16b15f88bbd2e0a22d1d0084b8b7080f2003ea83eab1a00f80d8c18446c9c1b6224f17aa09eaf167717ca4f355bb6dc94356e037edf3adf6735a86fc3741f5231b",
|
||||
"address": "0x03e75d7DD38CCE2e20FfEE35EC914C57780A8e29",
|
||||
"typedMessage": {
|
||||
"domain": {
|
||||
"name": "tDec",
|
||||
"version": "1",
|
||||
"chainId": 1,
|
||||
"salt": "0xf2d857f4a3edcb9b78b4d503bfe733db1e3f6cdc2b7971ee739626c97e86a558",
|
||||
},
|
||||
"message": {
|
||||
"address": "0x03e75d7DD38CCE2e20FfEE35EC914C57780A8e29",
|
||||
"blockNumber": 15440685,
|
||||
"blockHash": "0x2220da8b777767df526acffd5375ebb340fc98e53c1040b25ad1a8119829e3bd",
|
||||
"signatureText": "I'm the owner of address 0x03e75d7dd38cce2e20ffee35ec914c57780a8e29 as of block number 15440685",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return context
|
||||
|
|
|
@ -15,17 +15,13 @@ You should have received a copy of the GNU Affero General Public License
|
|||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_twisted
|
||||
from twisted.internet import threads
|
||||
|
||||
from nucypher_core import RetrievalKit
|
||||
import json
|
||||
|
||||
from nucypher.characters.lawful import Enrico, Bob
|
||||
from nucypher.config.constants import TEMPORARY_DOMAIN
|
||||
from nucypher.network.retrieval import RetrievalClient
|
||||
from nucypher_core import Address, RetrievalKit, Conditions
|
||||
|
||||
from tests.utils.middleware import MockRestMiddleware, NodeIsDownMiddleware
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from tests.utils.middleware import NodeIsDownMiddleware
|
||||
|
||||
|
||||
def _policy_info_kwargs(enacted_policy):
|
||||
|
@ -35,14 +31,14 @@ def _policy_info_kwargs(enacted_policy):
|
|||
)
|
||||
|
||||
|
||||
def _make_message_kits(policy_pubkey):
|
||||
def _make_message_kits(policy_pubkey, conditions=None):
|
||||
messages = [b"plaintext1", b"plaintext2", b"plaintext3"]
|
||||
|
||||
message_kits = []
|
||||
for message in messages:
|
||||
# Using different Enricos, because why not.
|
||||
enrico = Enrico(policy_encrypting_key=policy_pubkey)
|
||||
message_kit = enrico.encrypt_message(message)
|
||||
message_kit = enrico.encrypt_message(message, conditions=conditions)
|
||||
message_kits.append(message_kit)
|
||||
|
||||
return messages, message_kits
|
||||
|
@ -52,7 +48,7 @@ def test_retrieval_kit(enacted_federated_policy, federated_ursulas):
|
|||
messages, message_kits = _make_message_kits(enacted_federated_policy.public_key)
|
||||
|
||||
capsule = message_kits[0].capsule
|
||||
addresses = {ursula.canonical_address for ursula in list(federated_ursulas)[:2]}
|
||||
addresses = {Address(ursula.canonical_address) for ursula in list(federated_ursulas)[:2]}
|
||||
|
||||
retrieval_kit = RetrievalKit(capsule, addresses)
|
||||
serialized = bytes(retrieval_kit)
|
||||
|
@ -75,6 +71,34 @@ def test_single_retrieve(enacted_federated_policy, federated_bob, federated_ursu
|
|||
assert cleartexts == messages
|
||||
|
||||
|
||||
# TODO: MOVE ME
|
||||
def test_single_retrieve_with_conditions(enacted_federated_policy, federated_bob, federated_ursulas):
|
||||
from nucypher_core import MessageKit
|
||||
|
||||
federated_bob.start_learning_loop()
|
||||
conditions = [
|
||||
{'returnValueTest': {'value': '0', 'comparator': '>'}, 'method': 'timelock'},
|
||||
{'operator': 'and'},
|
||||
{'returnValueTest': {'value': '99999999999999999', 'comparator': '<'}, 'method': 'timelock'},
|
||||
]
|
||||
json_conditions = json.dumps(conditions)
|
||||
rust_conditions = Conditions(json_conditions)
|
||||
message_kits = [
|
||||
MessageKit(
|
||||
enacted_federated_policy.public_key,
|
||||
b'lab',
|
||||
rust_conditions
|
||||
)
|
||||
]
|
||||
|
||||
cleartexts = federated_bob.retrieve_and_decrypt(
|
||||
message_kits=message_kits,
|
||||
**_policy_info_kwargs(enacted_federated_policy),
|
||||
)
|
||||
|
||||
assert b'lab' in cleartexts
|
||||
|
||||
|
||||
def test_use_external_cache(enacted_federated_policy, federated_bob, federated_ursulas):
|
||||
|
||||
federated_bob.start_learning_loop()
|
||||
|
|
|
@ -207,7 +207,7 @@ def test_select_client_account_with_balance_display(mock_stdin,
|
|||
|
||||
if show_eth:
|
||||
balance = mock_testerchain.client.get_balance(account=account)
|
||||
assert str(Web3.fromWei(balance, 'ether')) in captured.out
|
||||
assert str(Web3.from_wei(balance, 'ether')) in captured.out
|
||||
|
||||
if show_staking:
|
||||
if len(stake_info) == 0:
|
||||
|
|
|
@ -14,11 +14,10 @@
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher_core import HRAC, TreasureMap
|
||||
from nucypher_core import Address, HRAC, TreasureMap
|
||||
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
|
||||
|
@ -35,7 +34,7 @@ def random_federated_treasure_map_data(federated_alice, federated_bob, federated
|
|||
label=label)
|
||||
|
||||
assigned_kfrags = {
|
||||
ursula.canonical_address: (ursula.public_keys(DecryptingPower), vkfrag)
|
||||
Address(ursula.canonical_address): (ursula.public_keys(DecryptingPower), vkfrag)
|
||||
for ursula, vkfrag in zip(list(federated_ursulas)[:shares], kfrags)}
|
||||
|
||||
random_treasure_map = TreasureMap(signer=federated_alice.stamp.as_umbral_signer(),
|
||||
|
|
|
@ -75,7 +75,8 @@ def test_retrieve_cfrags(federated_porter,
|
|||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
random_federated_treasure_map_data):
|
||||
random_federated_treasure_map_data,
|
||||
random_context):
|
||||
method = 'retrieve_cfrags'
|
||||
|
||||
# Setup
|
||||
|
@ -97,6 +98,19 @@ def test_retrieve_cfrags(federated_porter,
|
|||
expected_results = federated_porter.retrieve_cfrags(**retrieve_args)
|
||||
assert len(retrieval_results) == len(expected_results)
|
||||
|
||||
# Use context
|
||||
retrieve_cfrags_params_with_context, _ = retrieval_request_setup(enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
context=random_context,
|
||||
encode_for_rest=True)
|
||||
request_data = {'method': method, 'params': retrieve_cfrags_params_with_context}
|
||||
response = federated_porter_rpc_controller.send(request_data)
|
||||
assert response.success
|
||||
|
||||
retrieval_results = response.data['result']['retrieval_results']
|
||||
assert retrieval_results
|
||||
|
||||
# Failure - use encrypted treasure map
|
||||
failure_retrieve_cfrags_params = dict(retrieve_cfrags_params)
|
||||
_, random_treasure_map = random_federated_treasure_map_data
|
||||
|
|
|
@ -18,15 +18,23 @@
|
|||
|
||||
import json
|
||||
from base64 import b64encode
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from nucypher_core import RetrievalKit
|
||||
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from nucypher.control.specifications.fields import JSON
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.policy.kits import PolicyMessageKit, RetrievalResult
|
||||
from nucypher.utilities.porter.control.specifications.fields import RetrievalResultSchema, RetrievalKit as RetrievalKitField
|
||||
from tests.utils.policy import retrieval_request_setup, retrieval_params_decode_from_rest
|
||||
from nucypher.utilities.porter.control.specifications.fields import (
|
||||
RetrievalKit as RetrievalKitField,
|
||||
)
|
||||
from nucypher.utilities.porter.control.specifications.fields import (
|
||||
RetrievalOutcomeSchema,
|
||||
)
|
||||
from tests.utils.policy import (
|
||||
retrieval_params_decode_from_rest,
|
||||
retrieval_request_setup,
|
||||
)
|
||||
|
||||
|
||||
def test_get_ursulas(federated_porter_web_controller, federated_ursulas):
|
||||
|
@ -90,7 +98,8 @@ def test_retrieve_cfrags(federated_porter,
|
|||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
random_federated_treasure_map_data):
|
||||
random_federated_treasure_map_data,
|
||||
random_context):
|
||||
# Send bad data to assert error return
|
||||
response = federated_porter_web_controller.post('/retrieve_cfrags', data=json.dumps({'bad': 'input'}))
|
||||
assert response.status_code == 400
|
||||
|
@ -99,11 +108,13 @@ def test_retrieve_cfrags(federated_porter,
|
|||
original_message = b'The paradox of education is precisely this - that as one begins to become ' \
|
||||
b'conscious one begins to examine the society in which ' \
|
||||
b'he is (they are) being educated.' # - James Baldwin
|
||||
retrieve_cfrags_params, message_kit = retrieval_request_setup(enacted_federated_policy,
|
||||
retrieve_cfrags_params, message_kits = retrieval_request_setup(enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
original_message=original_message,
|
||||
specific_messages=[original_message],
|
||||
encode_for_rest=True)
|
||||
assert len(message_kits) == 1
|
||||
message_kit = message_kits[0]
|
||||
|
||||
#
|
||||
# Success
|
||||
|
@ -126,7 +137,7 @@ def test_retrieve_cfrags(federated_porter,
|
|||
policy_encrypting_key=enacted_federated_policy.public_key,
|
||||
threshold=treasure_map.threshold)
|
||||
assert len(retrieval_results) == 1
|
||||
field = RetrievalResultSchema()
|
||||
field = RetrievalOutcomeSchema()
|
||||
cfrags = field.load(retrieval_results[0])['cfrags']
|
||||
verified_cfrags = {}
|
||||
for ursula, cfrag in cfrags.items():
|
||||
|
@ -168,16 +179,18 @@ def test_retrieve_cfrags(federated_porter,
|
|||
retrieval_results = response_data['result']['retrieval_results']
|
||||
assert retrieval_results
|
||||
assert len(retrieval_results) == 4
|
||||
for i in range(0, 4):
|
||||
assert len(retrieval_results[i]["cfrags"]) > 0
|
||||
assert len(retrieval_results[i]["errors"]) == 0
|
||||
|
||||
#
|
||||
# Try same retrieval (with multiple retrieval kits) using query parameters
|
||||
# Use context
|
||||
#
|
||||
url_retrieve_params = dict(multiple_retrieval_kits_params) # use multiple kit params from above
|
||||
# adjust parameter for url query parameter list format
|
||||
url_retrieve_params['retrieval_kits'] = ",".join(url_retrieve_params['retrieval_kits'])
|
||||
response = federated_porter_web_controller.post(f'/retrieve_cfrags'
|
||||
f'?{urlencode(url_retrieve_params)}')
|
||||
context_field = JSON()
|
||||
multiple_retrieval_kits_params['context'] = context_field._serialize(random_context, attr=None, obj=None)
|
||||
response = federated_porter_web_controller.post('/retrieve_cfrags', data=json.dumps(multiple_retrieval_kits_params))
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
retrieval_results = response_data['result']['retrieval_results']
|
||||
assert retrieval_results
|
||||
|
|
|
@ -74,3 +74,18 @@ def test_retrieve_cfrags(federated_porter,
|
|||
result = federated_porter.retrieve_cfrags(**retrieval_args)
|
||||
|
||||
assert result, "valid result returned"
|
||||
|
||||
|
||||
def test_retrieve_cfrags_with_context(federated_porter,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
enacted_federated_policy,
|
||||
random_context):
|
||||
# Setup
|
||||
retrieval_args, _ = retrieval_request_setup(enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
context=random_context)
|
||||
|
||||
result = federated_porter.retrieve_cfrags(**retrieval_args)
|
||||
assert result, "valid result returned"
|
||||
|
|
|
@ -17,15 +17,19 @@
|
|||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher_core.umbral import SecretKey
|
||||
|
||||
from nucypher.characters.control.specifications.fields import Key
|
||||
from nucypher.control.specifications.exceptions import InvalidArgumentCombo, InvalidInputData
|
||||
from nucypher.utilities.porter.control.specifications.fields import UrsulaInfoSchema, RetrievalResultSchema
|
||||
from nucypher.control.specifications.exceptions import (
|
||||
InvalidArgumentCombo,
|
||||
InvalidInputData,
|
||||
)
|
||||
from nucypher.utilities.porter.control.specifications.fields import (
|
||||
RetrievalOutcomeSchema,
|
||||
UrsulaInfoSchema,
|
||||
)
|
||||
from nucypher.utilities.porter.control.specifications.porter_schema import (
|
||||
AliceGetUrsulas,
|
||||
BobRetrieveCFrags
|
||||
BobRetrieveCFrags,
|
||||
)
|
||||
from nucypher.utilities.porter.porter import Porter
|
||||
from tests.utils.policy import retrieval_request_setup
|
||||
|
@ -163,22 +167,47 @@ def test_alice_revoke():
|
|||
def test_bob_retrieve_cfrags(federated_porter,
|
||||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice):
|
||||
federated_alice,
|
||||
random_context,
|
||||
get_random_checksum_address):
|
||||
bob_retrieve_cfrags_schema = BobRetrieveCFrags()
|
||||
|
||||
# no args
|
||||
with pytest.raises(InvalidInputData):
|
||||
bob_retrieve_cfrags_schema.load({})
|
||||
|
||||
# Setup
|
||||
# Setup - no context
|
||||
retrieval_args, _ = retrieval_request_setup(enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
encode_for_rest=True)
|
||||
bob_retrieve_cfrags_schema.load(retrieval_args)
|
||||
|
||||
# simple schema load w/ optional context
|
||||
retrieval_args, _ = retrieval_request_setup(
|
||||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
encode_for_rest=True,
|
||||
context=random_context,
|
||||
)
|
||||
bob_retrieve_cfrags_schema.load(retrieval_args)
|
||||
|
||||
# invalid context specified
|
||||
retrieval_args, _ = retrieval_request_setup(
|
||||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
encode_for_rest=True,
|
||||
context=[1, 2, 3], # list instead of dict
|
||||
)
|
||||
with pytest.raises(InvalidInputData):
|
||||
# invalid context type
|
||||
bob_retrieve_cfrags_schema.load(retrieval_args)
|
||||
|
||||
# missing required argument
|
||||
updated_data = dict(retrieval_args)
|
||||
updated_data.pop("context") # context is not a required param
|
||||
key_to_remove = random.choice(list(updated_data.keys()))
|
||||
del updated_data[key_to_remove]
|
||||
with pytest.raises(InvalidInputData):
|
||||
|
@ -186,19 +215,114 @@ def test_bob_retrieve_cfrags(federated_porter,
|
|||
bob_retrieve_cfrags_schema.load(updated_data)
|
||||
|
||||
#
|
||||
# Output i.e. dump
|
||||
# Retrieval output for 1 retrieval kit
|
||||
#
|
||||
non_encoded_retrieval_args, _ = retrieval_request_setup(enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
encode_for_rest=False)
|
||||
retrieval_results = federated_porter.retrieve_cfrags(**non_encoded_retrieval_args)
|
||||
non_encoded_retrieval_args, _ = retrieval_request_setup(
|
||||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
encode_for_rest=False,
|
||||
context=random_context,
|
||||
)
|
||||
retrieval_outcomes = federated_porter.retrieve_cfrags(**non_encoded_retrieval_args)
|
||||
expected_retrieval_results_json = []
|
||||
retrieval_result_schema = RetrievalResultSchema()
|
||||
retrieval_outcome_schema = RetrievalOutcomeSchema()
|
||||
|
||||
for result in retrieval_results:
|
||||
data = retrieval_result_schema.dump(result)
|
||||
assert len(retrieval_outcomes) == 1
|
||||
assert len(retrieval_outcomes[0].cfrags) > 0
|
||||
assert len(retrieval_outcomes[0].errors) == 0
|
||||
for outcome in retrieval_outcomes:
|
||||
data = retrieval_outcome_schema.dump(outcome)
|
||||
expected_retrieval_results_json.append(data)
|
||||
|
||||
output = bob_retrieve_cfrags_schema.dump(obj={'retrieval_results': retrieval_results})
|
||||
output = bob_retrieve_cfrags_schema.dump(
|
||||
obj={"retrieval_results": retrieval_outcomes}
|
||||
)
|
||||
assert output == {"retrieval_results": expected_retrieval_results_json}
|
||||
assert len(output["retrieval_results"]) == 1
|
||||
assert len(output["retrieval_results"][0]["cfrags"]) > 0
|
||||
assert len(output["retrieval_results"][0]["errors"]) == 0
|
||||
|
||||
# now include errors
|
||||
errors = {
|
||||
get_random_checksum_address(): "Error Message 1",
|
||||
get_random_checksum_address(): "Error Message 2",
|
||||
get_random_checksum_address(): "Error Message 3",
|
||||
}
|
||||
new_retrieval_outcome = Porter.RetrievalOutcome(
|
||||
cfrags=retrieval_outcomes[0].cfrags, errors=errors
|
||||
)
|
||||
expected_retrieval_results_json = [
|
||||
retrieval_outcome_schema.dump(new_retrieval_outcome)
|
||||
]
|
||||
output = bob_retrieve_cfrags_schema.dump(
|
||||
obj={"retrieval_results": [new_retrieval_outcome]}
|
||||
)
|
||||
assert output == {"retrieval_results": expected_retrieval_results_json}
|
||||
assert len(output["retrieval_results"]) == 1
|
||||
assert len(output["retrieval_results"][0]["cfrags"]) > 0
|
||||
assert len(output["retrieval_results"][0]["errors"]) == len(errors)
|
||||
|
||||
#
|
||||
# Retrieval output for multiple retrieval kits
|
||||
#
|
||||
num_retrieval_kits = 4
|
||||
non_encoded_retrieval_args, _ = retrieval_request_setup(
|
||||
enacted_federated_policy,
|
||||
federated_bob,
|
||||
federated_alice,
|
||||
encode_for_rest=False,
|
||||
context=random_context,
|
||||
num_random_messages=num_retrieval_kits,
|
||||
)
|
||||
retrieval_outcomes = federated_porter.retrieve_cfrags(**non_encoded_retrieval_args)
|
||||
expected_retrieval_results_json = []
|
||||
retrieval_outcome_schema = RetrievalOutcomeSchema()
|
||||
|
||||
assert len(retrieval_outcomes) == num_retrieval_kits
|
||||
for i in range(num_retrieval_kits):
|
||||
assert len(retrieval_outcomes[i].cfrags) > 0
|
||||
assert len(retrieval_outcomes[i].errors) == 0
|
||||
for outcome in retrieval_outcomes:
|
||||
data = retrieval_outcome_schema.dump(outcome)
|
||||
expected_retrieval_results_json.append(data)
|
||||
|
||||
output = bob_retrieve_cfrags_schema.dump(
|
||||
obj={"retrieval_results": retrieval_outcomes}
|
||||
)
|
||||
assert output == {"retrieval_results": expected_retrieval_results_json}
|
||||
|
||||
# now include errors
|
||||
error_message_template = "Retrieval Kit {} - Error Message {}"
|
||||
new_retrieval_outcomes_with_errors = []
|
||||
for i in range(num_retrieval_kits):
|
||||
specific_kit_errors = dict()
|
||||
for j in range(i):
|
||||
# different number of errors for each kit; 1 error for kit 1, 2 errors for kit 2 etc.
|
||||
specific_kit_errors[
|
||||
get_random_checksum_address()
|
||||
] = error_message_template.format(i, j)
|
||||
new_retrieval_outcomes_with_errors.append(
|
||||
Porter.RetrievalOutcome(
|
||||
cfrags=retrieval_outcomes[i].cfrags, errors=specific_kit_errors
|
||||
)
|
||||
)
|
||||
|
||||
expected_retrieval_results_json = []
|
||||
for outcome in new_retrieval_outcomes_with_errors:
|
||||
data = retrieval_outcome_schema.dump(outcome)
|
||||
expected_retrieval_results_json.append(data)
|
||||
|
||||
output = bob_retrieve_cfrags_schema.dump(
|
||||
obj={"retrieval_results": new_retrieval_outcomes_with_errors}
|
||||
)
|
||||
assert output == {"retrieval_results": expected_retrieval_results_json}
|
||||
assert len(output["retrieval_results"]) == num_retrieval_kits
|
||||
for i in range(num_retrieval_kits):
|
||||
assert len(output["retrieval_results"][i]["cfrags"]) > 0
|
||||
# ensures errors are associated appropriately
|
||||
kit_errors = output["retrieval_results"][i]["errors"]
|
||||
assert len(kit_errors) == i
|
||||
values = kit_errors.values() # ordered?
|
||||
for j in range(i):
|
||||
assert error_message_template.format(i, j) in values
|
||||
|
|
|
@ -202,7 +202,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
|
|||
print("********* Estimating Gas *********")
|
||||
|
||||
def transact_and_log(label, function, transaction):
|
||||
estimates = function.estimateGas(transaction)
|
||||
estimates = function.estimate_gas(transaction)
|
||||
transaction.update(gas=estimates)
|
||||
tx = function.transact(transaction)
|
||||
receipt = testerchain.wait_for_receipt(tx)
|
||||
|
@ -288,7 +288,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
|
|||
rate = 100
|
||||
one_period = economics.hours_per_period * 60 * 60
|
||||
value = number_of_periods * rate
|
||||
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
|
||||
transact_and_log("Creating policy (1 node, 10 periods, pre-committed), first",
|
||||
policy_functions.createPolicy(policy_id_1, alice1, end_timestamp, [staker1]),
|
||||
|
@ -363,7 +363,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
|
|||
policy_id_3 = os.urandom(int(POLICY_ID_LENGTH))
|
||||
number_of_periods = 100
|
||||
value = 3 * number_of_periods * rate
|
||||
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
|
||||
transact_and_log("Creating policy (3 nodes, 100 periods, pre-committed), first",
|
||||
policy_functions.createPolicy(policy_id_1, alice1, end_timestamp, [staker1, staker2, staker3]),
|
||||
|
@ -392,13 +392,13 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
|
|||
policy_id_3 = os.urandom(int(POLICY_ID_LENGTH))
|
||||
number_of_periods = 100
|
||||
value = number_of_periods * rate
|
||||
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
|
||||
transact_and_log("Creating policy (1 node, 100 periods)",
|
||||
policy_functions.createPolicy(policy_id_1, alice2, end_timestamp, [staker2]),
|
||||
{'from': alice1, 'value': value})
|
||||
testerchain.time_travel(periods=1)
|
||||
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
|
||||
transact_and_log("Creating policy (1 node, 100 periods), next period",
|
||||
policy_functions.createPolicy(policy_id_2, alice2, end_timestamp, [staker2]),
|
||||
|
@ -440,7 +440,7 @@ def estimate_gas(analyzer: AnalyzeGas = None) -> None:
|
|||
#
|
||||
policy_id_1 = os.urandom(int(POLICY_ID_LENGTH))
|
||||
policy_id_2 = os.urandom(int(POLICY_ID_LENGTH))
|
||||
current_timestamp = testerchain.w3.eth.getBlock('latest').timestamp
|
||||
current_timestamp = testerchain.w3.eth.get_block('latest').timestamp
|
||||
end_timestamp = current_timestamp + (number_of_periods - 1) * one_period
|
||||
value = 3 * number_of_periods * rate
|
||||
transact_and_log("Creating 2 policies (3 nodes, 100 periods, pre-committed)",
|
||||
|
|
|
@ -16,11 +16,6 @@
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from nucypher.blockchain.eth.signers import Signer
|
||||
from nucypher.network.nodes import TEACHER_NODES
|
||||
from nucypher.policy.payment import SubscriptionManagerPayment
|
||||
|
||||
"""
|
||||
WARNING: This script makes automatic transactions.
|
||||
|
@ -29,22 +24,28 @@ are doing and intend to spend ETH measuring live
|
|||
policy availability.
|
||||
"""
|
||||
|
||||
|
||||
import datetime
|
||||
import maya
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from pathlib import Path
|
||||
from typing import Set, Optional, List, Tuple
|
||||
|
||||
import maya
|
||||
from eth_typing.evm import ChecksumAddress
|
||||
from nucypher_core.umbral import SecretKey
|
||||
from web3 import Web3
|
||||
from web3.types import Wei
|
||||
|
||||
from nucypher.blockchain.eth.signers import Signer
|
||||
from nucypher.characters.lawful import Bob, Ursula, Alice
|
||||
from nucypher.config.characters import AliceConfiguration
|
||||
from nucypher.network.nodes import TEACHER_NODES
|
||||
from nucypher.policy.payment import SubscriptionManagerPayment
|
||||
from nucypher.policy.policies import Policy
|
||||
from nucypher.utilities.logging import GlobalLoggerSettings
|
||||
|
||||
|
||||
# Signer Configuration
|
||||
# In order to use this script, you must configure a wallet for alice
|
||||
ADDRESS_ENVVAR: str = 'NUCYPHER_GRANT_METRICS_ADDRESS'
|
||||
|
@ -76,8 +77,9 @@ INSECURE_PASSWORD: str = "METRICS_INSECURE_DEVELOPMENT_PASSWORD"
|
|||
TEMP_ALICE_DIR: Path = Path('/', 'tmp', 'grant-metrics')
|
||||
|
||||
# Policy Parameters
|
||||
THRESHOLD: int = 2
|
||||
SHARES: int = 3
|
||||
THRESHOLD: int = 1
|
||||
SHARES: int = 1
|
||||
RATE: Wei = Web3.to_wei(50, 'gwei')
|
||||
DURATION: datetime.timedelta = datetime.timedelta(days=1)
|
||||
|
||||
# Tuning
|
||||
|
|
|
@ -33,7 +33,7 @@ MOCK_TESTERCHAIN = MockBlockchain()
|
|||
CACHED_MOCK_TESTERCHAIN = BlockchainInterfaceFactory.CachedInterface(interface=MOCK_TESTERCHAIN, emitter=None)
|
||||
BlockchainInterfaceFactory._interfaces[MOCK_ETH_PROVIDER_URI] = CACHED_MOCK_TESTERCHAIN
|
||||
|
||||
CURRENT_BLOCK = MOCK_TESTERCHAIN.w3.eth.getBlock('latest')
|
||||
CURRENT_BLOCK = MOCK_TESTERCHAIN.w3.eth.get_block('latest')
|
||||
|
||||
|
||||
class MockContractAgent:
|
||||
|
|
|
@ -0,0 +1,150 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from web3 import Web3
|
||||
|
||||
import tests
|
||||
from nucypher.policy.conditions.context import USER_ADDRESS_CONTEXT
|
||||
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import AND, OR, ConditionLingo, ReturnValueTest
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
|
||||
VECTORS_FILE = Path(tests.__file__).parent / "data" / "test_conditions.json"
|
||||
|
||||
with open(VECTORS_FILE, 'r') as file:
|
||||
VECTORS = json.loads(file.read())
|
||||
|
||||
|
||||
# ERC1155
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC1155_balance_condition_data():
|
||||
data = json.dumps(VECTORS['ERC1155_balance'])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC1155_balance_condition(ERC1155_balance_condition_data):
|
||||
data = ERC1155_balance_condition_data
|
||||
condition = ContractCondition.from_json(data)
|
||||
return condition
|
||||
|
||||
|
||||
# ERC20
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC20_balance_condition_data():
|
||||
data = json.dumps(VECTORS['ERC20_balance'])
|
||||
return data
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def ERC20_balance_condition(ERC20_balance_condition_data):
|
||||
data = ERC20_balance_condition_data
|
||||
condition = ContractCondition.from_json(data)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rpc_condition():
|
||||
condition = RPCCondition(
|
||||
method="eth_getBalance",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", Web3.to_wei(1_000_000, "ether")),
|
||||
parameters=[USER_ADDRESS_CONTEXT],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def erc20_evm_condition(test_registry):
|
||||
condition = ContractCondition(
|
||||
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
method="balanceOf",
|
||||
standard_contract_type="ERC20",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", 0),
|
||||
parameters=[
|
||||
USER_ADDRESS_CONTEXT,
|
||||
],
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def erc721_evm_condition(test_registry):
|
||||
condition = ContractCondition(
|
||||
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
method="ownerOf",
|
||||
standard_contract_type="ERC721",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", ":userAddress"),
|
||||
parameters=[
|
||||
5954,
|
||||
]
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sm_condition(test_registry):
|
||||
zeroized_policy_struct = (
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
)
|
||||
condition = ContractCondition(
|
||||
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
method="getPolicy",
|
||||
chain="testerchain",
|
||||
function_abi=ABI,
|
||||
return_value_test=ReturnValueTest("!=", zeroized_policy_struct),
|
||||
parameters=[
|
||||
':hrac',
|
||||
]
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def timelock_condition():
|
||||
condition = TimeCondition(
|
||||
return_value_test=ReturnValueTest('>', 0)
|
||||
)
|
||||
return condition
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lingo(timelock_condition, rpc_condition, erc20_evm_condition, erc721_evm_condition):
|
||||
lingo = ConditionLingo(
|
||||
conditions=[
|
||||
erc721_evm_condition,
|
||||
OR,
|
||||
timelock_condition,
|
||||
OR,
|
||||
rpc_condition,
|
||||
AND,
|
||||
erc20_evm_condition,
|
||||
]
|
||||
)
|
||||
return lingo
|
|
@ -0,0 +1,35 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
|
||||
|
||||
def test_compound_condition_timelock():
|
||||
conditions = [
|
||||
{
|
||||
"returnValueTest": {"value": 0, "comparator": ">"},
|
||||
"method": "timelock"
|
||||
},
|
||||
{"operator": "and"},
|
||||
{
|
||||
"returnValueTest": {"value": 99999999999999999, "comparator": "<"},
|
||||
"method": "timelock",
|
||||
},
|
||||
]
|
||||
|
||||
clingo = ConditionLingo.from_list(conditions)
|
||||
assert clingo.eval()
|
|
@ -0,0 +1,99 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from nucypher.policy.conditions.base import ReencryptionCondition
|
||||
from nucypher.policy.conditions.evm import ContractCondition, RPCCondition
|
||||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
from nucypher.policy.conditions.time import TimeCondition
|
||||
|
||||
|
||||
def test_invalid_time_condition():
|
||||
with pytest.raises(ReencryptionCondition.InvalidCondition):
|
||||
_ = TimeCondition(
|
||||
return_value_test=ReturnValueTest('>', 0),
|
||||
method="time_after_time",
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_rpc_condition():
|
||||
# no eth_ prefix for method
|
||||
with pytest.raises(ReencryptionCondition.InvalidCondition):
|
||||
_ = RPCCondition(
|
||||
method="no_eth_prefix_eth_getBalance",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", 0),
|
||||
parameters=["0xaDD9D957170dF6F33982001E4c22eCCdd5539118"],
|
||||
)
|
||||
|
||||
# non-existent method
|
||||
with pytest.raises(RPCCondition.InvalidCondition):
|
||||
_ = RPCCondition(
|
||||
method="eth_randoMethod",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest("==", 0),
|
||||
parameters=["0xaDD9D957170dF6F33982001E4c22eCCdd5539118"],
|
||||
)
|
||||
|
||||
# invalid chain id
|
||||
with pytest.raises(RPCCondition.InvalidCondition):
|
||||
_ = RPCCondition(
|
||||
method="eth_getBalance",
|
||||
chain="90210", # Beverly Hills Chain :)
|
||||
return_value_test=ReturnValueTest("==", 0),
|
||||
parameters=["0xaDD9D957170dF6F33982001E4c22eCCdd5539118"],
|
||||
)
|
||||
|
||||
|
||||
def test_invalid_contract_condition():
|
||||
# no abi or contract type
|
||||
with pytest.raises(ReencryptionCondition.InvalidCondition):
|
||||
_ = ContractCondition(
|
||||
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
method="getPolicy",
|
||||
chain="testerchain",
|
||||
return_value_test=ReturnValueTest('!=', 0),
|
||||
parameters=[
|
||||
':hrac',
|
||||
]
|
||||
)
|
||||
|
||||
# invalid contract type
|
||||
with pytest.raises(ReencryptionCondition.InvalidCondition):
|
||||
_ = ContractCondition(
|
||||
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
method="getPolicy",
|
||||
chain="testerchain",
|
||||
standard_contract_type="ERC90210", # Beverly Hills contract type :)
|
||||
return_value_test=ReturnValueTest('!=', 0),
|
||||
parameters=[
|
||||
':hrac',
|
||||
]
|
||||
)
|
||||
|
||||
# invalid ABI
|
||||
with pytest.raises(ReencryptionCondition.InvalidCondition):
|
||||
_ = ContractCondition(
|
||||
contract_address="0xaDD9D957170dF6F33982001E4c22eCCdd5539118",
|
||||
method="getPolicy",
|
||||
chain="testerchain",
|
||||
function_abi=["rando ABI"],
|
||||
return_value_test=ReturnValueTest('!=', 0),
|
||||
parameters=[
|
||||
':hrac',
|
||||
]
|
||||
)
|
|
@ -0,0 +1,64 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from nucypher.policy.conditions._utils import _deserialize_condition_lingo
|
||||
from nucypher.policy.conditions.evm import ContractCondition
|
||||
from nucypher.policy.conditions.lingo import ConditionLingo
|
||||
|
||||
|
||||
def test_evm_condition_json_serializers(ERC1155_balance_condition_data):
|
||||
original_data = ERC1155_balance_condition_data
|
||||
condition = ContractCondition.from_json(original_data)
|
||||
serialized_data = condition.to_json()
|
||||
|
||||
# TODO functionAbi is present in serialized data
|
||||
deserialized_data = json.loads(serialized_data)
|
||||
deserialized_data.pop("functionAbi")
|
||||
|
||||
assert json.loads(original_data) == deserialized_data
|
||||
|
||||
|
||||
def test_type_resolution_from_json(
|
||||
timelock_condition, rpc_condition, erc20_evm_condition, erc721_evm_condition
|
||||
):
|
||||
conditions = (
|
||||
timelock_condition,
|
||||
rpc_condition,
|
||||
erc20_evm_condition,
|
||||
erc721_evm_condition,
|
||||
)
|
||||
for condition in conditions:
|
||||
condition_json = condition.to_json()
|
||||
resolved_condition = _deserialize_condition_lingo(condition_json)
|
||||
assert isinstance(resolved_condition, type(condition))
|
||||
|
||||
|
||||
def test_conditions_lingo_serialization(lingo):
|
||||
json_serialized_lingo = json.dumps([l.to_dict() for l in lingo.conditions])
|
||||
lingo_json = lingo.to_json()
|
||||
restored_lingo = ConditionLingo.from_json(data=lingo_json)
|
||||
assert lingo_json == json_serialized_lingo
|
||||
restored_lingo_json = restored_lingo.to_json()
|
||||
assert restored_lingo_json == json_serialized_lingo
|
||||
|
||||
lingo_b64 = restored_lingo.to_base64()
|
||||
restored_lingo = ConditionLingo.from_base64(lingo_b64)
|
||||
|
||||
# after all the serialization and transformation the content must remain identical
|
||||
assert restored_lingo.to_json() == lingo_json
|
|
@ -0,0 +1,196 @@
|
|||
"""
|
||||
This file is part of nucypher.
|
||||
|
||||
nucypher is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
nucypher is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.policy.conditions.lingo import ReturnValueTest
|
||||
|
||||
|
||||
def test_return_value_test_invalid_comparators():
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_ = ReturnValueTest(comparator="eq", value=1)
|
||||
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_ = ReturnValueTest(comparator="=", value=1)
|
||||
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_ = ReturnValueTest(comparator="+", value=1)
|
||||
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_ = ReturnValueTest(comparator="*", value=1)
|
||||
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_ = ReturnValueTest(comparator="/", value=1)
|
||||
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_test = ReturnValueTest(comparator="DROP", value=1)
|
||||
|
||||
|
||||
def test_eval_is_evil():
|
||||
# TODO look into more malicious fuzzing cases
|
||||
file = f"file_{os.urandom(10).hex()}"
|
||||
assert not os.path.isfile(file)
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_test = ReturnValueTest(
|
||||
comparator=">",
|
||||
value=f"(lambda: (42, __import__('os').system('touch {file}'))[0])()",
|
||||
)
|
||||
assert not os.path.isfile(file)
|
||||
|
||||
|
||||
def test_return_value_test_with_context_variable_cant_run_eval():
|
||||
# known context variable
|
||||
test = ReturnValueTest(comparator="==", value=":userAddress")
|
||||
with pytest.raises(RuntimeError):
|
||||
test.eval("0xaDD9D957170dF6F33982001E4c22eCCdd5539118")
|
||||
|
||||
# fake context variable
|
||||
test = ReturnValueTest(comparator="==", value=":fakeContextVar")
|
||||
with pytest.raises(RuntimeError):
|
||||
test.eval(0)
|
||||
|
||||
|
||||
def test_return_value_test_integer():
|
||||
# >
|
||||
test = ReturnValueTest(comparator='>', value='0')
|
||||
assert test.eval('1')
|
||||
assert not test.eval('-1')
|
||||
# mixing types can work
|
||||
assert test.eval(1)
|
||||
assert not test.eval(-1)
|
||||
|
||||
test = ReturnValueTest(comparator='>', value=0)
|
||||
assert test.eval(1)
|
||||
assert not test.eval(0)
|
||||
assert not test.eval(-1)
|
||||
# mixed types
|
||||
assert test.eval("1")
|
||||
assert not test.eval("0")
|
||||
|
||||
# >=
|
||||
test = ReturnValueTest(comparator=">=", value=1)
|
||||
assert test.eval(2)
|
||||
assert test.eval(1)
|
||||
assert not test.eval(-2)
|
||||
|
||||
# <
|
||||
test = ReturnValueTest(comparator="<", value=2)
|
||||
assert not test.eval(3)
|
||||
assert not test.eval(2)
|
||||
assert test.eval(-3)
|
||||
|
||||
# <=
|
||||
test = ReturnValueTest(comparator="<=", value=-1)
|
||||
assert not test.eval(3)
|
||||
assert test.eval(-1)
|
||||
assert test.eval(-3)
|
||||
|
||||
# ==
|
||||
test = ReturnValueTest(comparator="==", value=4)
|
||||
assert not test.eval(1)
|
||||
assert test.eval(4)
|
||||
assert not test.eval(-2)
|
||||
|
||||
# !=
|
||||
test = ReturnValueTest(comparator="!=", value=20)
|
||||
assert test.eval(1)
|
||||
assert not test.eval(20)
|
||||
assert test.eval(-2)
|
||||
|
||||
|
||||
def test_return_value_test_string():
|
||||
# string values must be quoted
|
||||
with pytest.raises(ReturnValueTest.InvalidExpression):
|
||||
_test = ReturnValueTest(comparator="==", value="foo")
|
||||
|
||||
test = ReturnValueTest(comparator='==', value='"foo"')
|
||||
assert test.eval('"foo"')
|
||||
assert not test.eval('"bar"')
|
||||
|
||||
test = ReturnValueTest(comparator="!=", value='"foo"')
|
||||
assert not test.eval('"foo"')
|
||||
assert test.eval('"bar"')
|
||||
|
||||
# mixing types works because the value is evaluated as an int, not a string
|
||||
test = ReturnValueTest(
|
||||
comparator="==", value="0xaDD9D957170dF6F33982001E4c22eCCdd5539118"
|
||||
)
|
||||
assert test.eval(992513598061863249514433594012370110655092723992)
|
||||
assert test.eval("0xaDD9D957170dF6F33982001E4c22eCCdd5539118")
|
||||
assert not test.eval("0xdeadbeef")
|
||||
|
||||
test = ReturnValueTest(
|
||||
comparator="!=", value="0xaDD9D957170dF6F33982001E4c22eCCdd5539118"
|
||||
)
|
||||
assert not test.eval("0xaDD9D957170dF6F33982001E4c22eCCdd5539118")
|
||||
assert test.eval("0xdeadbeef")
|
||||
|
||||
|
||||
def test_return_value_test_bool():
|
||||
test = ReturnValueTest(comparator="==", value=True)
|
||||
assert test.eval(True)
|
||||
assert not test.eval(False)
|
||||
assert test.eval("True")
|
||||
assert not test.eval("False")
|
||||
|
||||
test = ReturnValueTest(comparator="!=", value=False)
|
||||
assert test.eval(True)
|
||||
assert not test.eval(False)
|
||||
assert test.eval("True")
|
||||
assert not test.eval("False")
|
||||
|
||||
test = ReturnValueTest(comparator="==", value="True")
|
||||
assert test.eval(True)
|
||||
assert not test.eval(False)
|
||||
assert test.eval("True")
|
||||
assert not test.eval("False")
|
||||
|
||||
test = ReturnValueTest(comparator="!=", value="False")
|
||||
assert test.eval(True)
|
||||
assert not test.eval(False)
|
||||
assert test.eval("True")
|
||||
assert not test.eval("False")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_value",
|
||||
[
|
||||
'"foo"', # string
|
||||
":userAddress", # context variable is an exception case for a string value
|
||||
"0xaDD9D957170dF6F33982001E4c22eCCdd5539118", # string that is evaluated as int
|
||||
125, # int
|
||||
1.223, # float
|
||||
os.urandom(16), # bytes
|
||||
True, # bool
|
||||
"True", # bool as string
|
||||
(1, True, "love"), # tuple
|
||||
["a", "b", "c"], # list
|
||||
{"name": "John", "age": 22}, # dict
|
||||
{True, False}, # set
|
||||
],
|
||||
)
|
||||
def test_return_value_serialization(test_value):
|
||||
schema = ReturnValueTest.ReturnValueTestSchema()
|
||||
comparator = random.choice(ReturnValueTest.COMPARATORS)
|
||||
test = ReturnValueTest(comparator=comparator, value=test_value)
|
||||
reloaded = schema.load(schema.dump(test))
|
||||
assert (test.comparator == reloaded.comparator) and (
|
||||
test.value == reloaded.value
|
||||
), f"test for '{comparator} {test_value}'"
|
|
@ -39,7 +39,7 @@ def receipt():
|
|||
def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
|
||||
# Mocking Web3 and EthereumClient
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=receipt)
|
||||
|
||||
# Test with no chain reorganizations:
|
||||
|
||||
|
@ -52,7 +52,7 @@ def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
|
|||
new_receipt = dict(receipt)
|
||||
new_receipt.update({'blockHash': HexBytes('0xBebeCebada')})
|
||||
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=new_receipt)
|
||||
web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=new_receipt)
|
||||
|
||||
exception = mock_ethereum_client.ChainReorganizationDetected
|
||||
message = exception(receipt=receipt).message
|
||||
|
@ -60,18 +60,19 @@ def test_check_transaction_is_on_chain(mocker, mock_ethereum_client, receipt):
|
|||
_ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
|
||||
|
||||
# Another example: there has been a chain reorganization and our beloved TX is gone for good:
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(side_effect=TransactionNotFound)
|
||||
web3_mock.eth.get_transaction_receipt = mocker.Mock(side_effect=TransactionNotFound)
|
||||
with pytest.raises(exception, match=message):
|
||||
_ = mock_ethereum_client.check_transaction_is_on_chain(receipt=receipt)
|
||||
|
||||
|
||||
@pytest.mark.skip("Needs fix for web3.py v6+")
|
||||
def test_block_until_enough_confirmations(mocker, mock_ethereum_client, receipt):
|
||||
my_tx_hash = receipt['transactionHash']
|
||||
block_number_of_my_tx = receipt['blockNumber']
|
||||
|
||||
# Test that web3's TimeExhausted is propagated:
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(side_effect=TimeExhausted)
|
||||
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(side_effect=TimeExhausted)
|
||||
|
||||
with pytest.raises(TimeExhausted):
|
||||
mock_ethereum_client.block_until_enough_confirmations(transaction_hash=my_tx_hash,
|
||||
|
@ -80,10 +81,10 @@ def test_block_until_enough_confirmations(mocker, mock_ethereum_client, receipt)
|
|||
|
||||
# Test that NotEnoughConfirmations is raised when there are not enough confirmations.
|
||||
# In this case, we're going to mock eth.blockNumber to be stuck
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
web3_mock.eth.getTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(return_value=receipt)
|
||||
web3_mock.eth.get_transaction_receipt = mocker.Mock(return_value=receipt)
|
||||
|
||||
type(web3_mock.eth).blockNumber = PropertyMock(return_value=block_number_of_my_tx) # See docs of PropertyMock
|
||||
type(web3_mock.eth).block_number = PropertyMock(return_value=block_number_of_my_tx) # See docs of PropertyMock
|
||||
|
||||
# Additional adjustments to make the test faster
|
||||
mocker.patch.object(mock_ethereum_client, '_calculate_confirmations_timeout', return_value=0.1)
|
||||
|
@ -112,20 +113,20 @@ def test_wait_for_receipt_no_confirmations(mocker, mock_ethereum_client, receipt
|
|||
|
||||
# Test that web3's TimeExhausted is propagated:
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(side_effect=TimeExhausted)
|
||||
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(side_effect=TimeExhausted)
|
||||
with pytest.raises(TimeExhausted):
|
||||
_ = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
|
||||
web3_mock.eth.waitForTransactionReceipt.assert_called_once_with(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
|
||||
web3_mock.eth.wait_for_transaction_receipt.assert_called_once_with(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
|
||||
|
||||
# Test that when web3's layer returns the receipt, we get that receipt
|
||||
web3_mock.eth.waitForTransactionReceipt = mocker.Mock(return_value=receipt)
|
||||
web3_mock.eth.wait_for_transaction_receipt = mocker.Mock(return_value=receipt)
|
||||
returned_receipt = mock_ethereum_client.wait_for_receipt(transaction_hash=my_tx_hash, timeout=1, confirmations=0)
|
||||
assert receipt == returned_receipt
|
||||
web3_mock.eth.waitForTransactionReceipt.assert_called_once_with(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
|
||||
web3_mock.eth.wait_for_transaction_receipt.assert_called_once_with(transaction_hash=my_tx_hash,
|
||||
timeout=1,
|
||||
poll_latency=MockEthereumClient.TRANSACTION_POLLING_TIME)
|
||||
|
||||
|
||||
def test_wait_for_receipt_with_confirmations(mocker, mock_ethereum_client, receipt):
|
||||
|
|
|
@ -59,7 +59,7 @@ def test_use_pending_nonce_when_building_payload(mock_testerchain, mocker):
|
|||
def mock_get_transaction_count(sender, block_identifier) -> int:
|
||||
return transaction_count[block_identifier]
|
||||
|
||||
mock_testerchain.client.w3.eth.getTransactionCount = mocker.Mock(side_effect=mock_get_transaction_count)
|
||||
mock_testerchain.client.w3.eth.get_transaction_count = mocker.Mock(side_effect=mock_get_transaction_count)
|
||||
|
||||
def simulate_successful_transaction():
|
||||
transaction_count['pending'] += 1
|
||||
|
|
|
@ -14,12 +14,19 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
import json
|
||||
from base64 import b64encode
|
||||
|
||||
import pytest
|
||||
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
from nucypher.control.specifications.fields import PositiveInteger, StringList, String, Base64BytesRepresentation
|
||||
from nucypher.control.specifications.fields import (
|
||||
JSON,
|
||||
Base64BytesRepresentation,
|
||||
PositiveInteger,
|
||||
String,
|
||||
StringList,
|
||||
)
|
||||
|
||||
|
||||
def test_positive_integer_field():
|
||||
|
@ -58,6 +65,69 @@ def test_base64_representation_field():
|
|||
deserialized = field._deserialize(value=serialized, attr=None, data=None)
|
||||
assert deserialized == data
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
# attempt to serialize a non-serializable object
|
||||
field._serialize(value=Exception("non-serializable"), attr=None, obj=None)
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
# attempt to deserialize none base64 data
|
||||
field._deserialize(value=b"raw bytes with non base64 chars ?&^%", attr=None, data=None)
|
||||
|
||||
|
||||
def test_json_field():
|
||||
# test data
|
||||
dict_data = {
|
||||
"domain": {"name": "tdec", "version": 1, "chainId": 1, "salt": "blahblahblah"},
|
||||
"message": {
|
||||
"address": "0x03e75d7dd38cce2e20ffee35ec914c57780a8e29",
|
||||
"blockNumber": 15440685,
|
||||
"blockHash": "0x2220da8b777767df526acffd5375ebb340fc98e53c1040b25ad1a8119829e3bd",
|
||||
},
|
||||
}
|
||||
list_data = [12.5, 1.2, 4.3]
|
||||
str_data = "Everything in the universe has a rhythm, everything dances." # -- Maya Angelou
|
||||
num_data = 1234567890
|
||||
bool_data = True
|
||||
float_data = 2.35
|
||||
|
||||
# test serialization/deserialization of data - no expected type specified
|
||||
test_data = [dict_data, list_data, str_data, num_data, bool_data, float_data]
|
||||
field = JSON()
|
||||
for d in test_data:
|
||||
serialized = field._serialize(value=d, attr=None, obj=None)
|
||||
assert serialized == json.dumps(d)
|
||||
|
||||
deserialized = field._deserialize(value=serialized, attr=None, data=None)
|
||||
assert deserialized == d
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
# attempt to serialize non-json serializable object
|
||||
field._serialize(value=Exception("non-serializable"), attr=None, obj=None)
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
# attempt to deserialize invalid data
|
||||
field._deserialize(
|
||||
value=b"raw bytes", attr=None, data=None
|
||||
)
|
||||
|
||||
# test expected type enforcement
|
||||
test_types = [type(d) for d in test_data]
|
||||
for expected_type in test_types:
|
||||
field = JSON(expected_type=expected_type)
|
||||
for d in test_data:
|
||||
if type(d) == expected_type:
|
||||
# serialization/deserialization should work
|
||||
serialized = field._serialize(value=d, attr=None, obj=None)
|
||||
assert serialized == json.dumps(d)
|
||||
|
||||
deserialized = field._deserialize(value=serialized, attr=None, data=None)
|
||||
assert deserialized == d
|
||||
else:
|
||||
# serialization/deserialization should fail
|
||||
with pytest.raises(InvalidInputData):
|
||||
# attempt to serialize non-json serializable object
|
||||
field._serialize(d, attr=None, obj=None)
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
# attempt to deserialize invalid data
|
||||
field._deserialize(value=json.dumps(d), attr=None, data=None)
|
||||
|
|
|
@ -159,10 +159,10 @@ def test_etherchain():
|
|||
|
||||
with patch.object(feed, '_probe_feed'):
|
||||
feed._raw_data = etherchain_json
|
||||
assert feed.get_gas_price('safeLow') == Web3.toWei(99.0, 'gwei')
|
||||
assert feed.get_gas_price('standard') == Web3.toWei(105.0, 'gwei')
|
||||
assert feed.get_gas_price('fast') == Web3.toWei(108.0, 'gwei')
|
||||
assert feed.get_gas_price('fastest') == Web3.toWei(119.9, 'gwei')
|
||||
assert feed.get_gas_price('safeLow') == Web3.to_wei(99.0, 'gwei')
|
||||
assert feed.get_gas_price('standard') == Web3.to_wei(105.0, 'gwei')
|
||||
assert feed.get_gas_price('fast') == Web3.to_wei(108.0, 'gwei')
|
||||
assert feed.get_gas_price('fastest') == Web3.to_wei(119.9, 'gwei')
|
||||
assert feed.get_gas_price() == feed.get_gas_price('fast') # Default
|
||||
parsed_gas_prices = feed.gas_prices
|
||||
|
||||
|
@ -170,7 +170,7 @@ def test_etherchain():
|
|||
EtherchainGasPriceDatafeed.gas_prices = dict()
|
||||
with patch.dict(EtherchainGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
|
||||
gas_strategy = feed.construct_gas_strategy()
|
||||
assert gas_strategy("web3", "tx") == Web3.toWei(108.0, 'gwei')
|
||||
assert gas_strategy("web3", "tx") == Web3.to_wei(108.0, 'gwei')
|
||||
|
||||
|
||||
def test_upvest():
|
||||
|
@ -181,10 +181,10 @@ def test_upvest():
|
|||
|
||||
with patch.object(feed, '_probe_feed'):
|
||||
feed._raw_data = upvest_json
|
||||
assert feed.get_gas_price('slow') == Web3.toWei(87.19, 'gwei')
|
||||
assert feed.get_gas_price('medium') == Web3.toWei(91.424, 'gwei')
|
||||
assert feed.get_gas_price('fast') == Web3.toWei(97.158, 'gwei')
|
||||
assert feed.get_gas_price('fastest') == Web3.toWei(105.2745, 'gwei')
|
||||
assert feed.get_gas_price('slow') == Web3.to_wei(87.19, 'gwei')
|
||||
assert feed.get_gas_price('medium') == Web3.to_wei(91.424, 'gwei')
|
||||
assert feed.get_gas_price('fast') == Web3.to_wei(97.158, 'gwei')
|
||||
assert feed.get_gas_price('fastest') == Web3.to_wei(105.2745, 'gwei')
|
||||
assert feed.get_gas_price() == feed.get_gas_price('fastest') # Default
|
||||
parsed_gas_prices = feed.gas_prices
|
||||
|
||||
|
@ -192,7 +192,7 @@ def test_upvest():
|
|||
UpvestGasPriceDatafeed.gas_prices = dict()
|
||||
with patch.dict(UpvestGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
|
||||
gas_strategy = feed.construct_gas_strategy()
|
||||
assert gas_strategy("web3", "tx") == Web3.toWei(105.2745, 'gwei')
|
||||
assert gas_strategy("web3", "tx") == Web3.to_wei(105.2745, 'gwei')
|
||||
|
||||
|
||||
def test_zoltu():
|
||||
|
@ -203,10 +203,10 @@ def test_zoltu():
|
|||
|
||||
with patch.object(feed, '_probe_feed'):
|
||||
feed._raw_data = zoltu_json
|
||||
assert feed.get_gas_price('slow') == Web3.toWei(41, 'gwei')
|
||||
assert feed.get_gas_price('medium') == Web3.toWei(58, 'gwei')
|
||||
assert feed.get_gas_price('fast') == Web3.toWei(67, 'gwei')
|
||||
assert feed.get_gas_price('fastest') == Web3.toWei(70, 'gwei')
|
||||
assert feed.get_gas_price('slow') == Web3.to_wei(41, 'gwei')
|
||||
assert feed.get_gas_price('medium') == Web3.to_wei(58, 'gwei')
|
||||
assert feed.get_gas_price('fast') == Web3.to_wei(67, 'gwei')
|
||||
assert feed.get_gas_price('fastest') == Web3.to_wei(70, 'gwei')
|
||||
assert feed.get_gas_price() == feed.get_gas_price('fast') # Default
|
||||
parsed_gas_prices = feed.gas_prices
|
||||
|
||||
|
@ -214,7 +214,7 @@ def test_zoltu():
|
|||
ZoltuGasPriceDatafeed.gas_prices = dict()
|
||||
with patch.dict(ZoltuGasPriceDatafeed.gas_prices, values=parsed_gas_prices):
|
||||
gas_strategy = feed.construct_gas_strategy()
|
||||
assert gas_strategy("web3", "tx") == Web3.toWei(67, 'gwei')
|
||||
assert gas_strategy("web3", "tx") == Web3.to_wei(67, 'gwei')
|
||||
|
||||
|
||||
def test_datafeed_median_gas_price_strategy():
|
||||
|
|
|
@ -14,11 +14,13 @@
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from eth_utils import to_checksum_address
|
||||
from nucypher_core import NodeMetadata, NodeMetadataPayload
|
||||
from nucypher_core import Address, NodeMetadata, NodeMetadataPayload
|
||||
from nucypher_core.umbral import SecretKey, Signer
|
||||
|
||||
from nucypher.acumen.perception import FleetSensor
|
||||
|
@ -78,7 +80,7 @@ class Dummy: # Teacher
|
|||
# A dummy signature with the recovery byte
|
||||
dummy_signature = bytes(signer.sign(b'whatever')) + b'\x00'
|
||||
|
||||
payload = NodeMetadataPayload(staking_provider_address=self.canonical_address,
|
||||
payload = NodeMetadataPayload(staking_provider_address=Address(self.canonical_address),
|
||||
domain=':dummy:',
|
||||
timestamp_epoch=0,
|
||||
operator_signature=dummy_signature,
|
||||
|
|
|
@ -46,9 +46,9 @@ def test_fixed_price_gas_strategy():
|
|||
def test_max_price_gas_strategy(mocker, monkeypatch):
|
||||
|
||||
gas_prices_gwei = [10, 100, 999, 1000, 1001, 1_000_000, 1_000_000_000]
|
||||
gas_prices_wei = [Web3.toWei(gwei_price, 'gwei') for gwei_price in gas_prices_gwei]
|
||||
gas_prices_wei = [Web3.to_wei(gwei_price, 'gwei') for gwei_price in gas_prices_gwei]
|
||||
max_gas_price_gwei = 1000
|
||||
max_gas_price_wei = Web3.toWei(max_gas_price_gwei, 'gwei')
|
||||
max_gas_price_wei = Web3.to_wei(max_gas_price_gwei, 'gwei')
|
||||
mock_gas_strategy = mocker.Mock(side_effect=itertools.cycle(gas_prices_wei))
|
||||
|
||||
wrapped_strategy = max_price_gas_strategy_wrapper(gas_strategy=mock_gas_strategy,
|
||||
|
|
|
@ -21,7 +21,7 @@ from eth_utils import to_canonical_address
|
|||
|
||||
import pytest
|
||||
|
||||
from nucypher_core import RetrievalKit as RetrievalKitClass, MessageKit
|
||||
from nucypher_core import RetrievalKit as RetrievalKitClass, Address, MessageKit
|
||||
from nucypher_core.umbral import SecretKey
|
||||
|
||||
from nucypher.control.specifications.exceptions import InvalidInputData
|
||||
|
@ -106,11 +106,11 @@ def test_retrieval_kit_field(get_random_checksum_address):
|
|||
encrypting_key = SecretKey.random().public_key()
|
||||
capsule = MessageKit(encrypting_key, b'testing retrieval kit with 2 ursulas').capsule
|
||||
ursulas = [get_random_checksum_address(), get_random_checksum_address()]
|
||||
run_tests_on_kit(kit=RetrievalKitClass(capsule, {to_canonical_address(ursula) for ursula in ursulas}))
|
||||
run_tests_on_kit(kit=RetrievalKitClass(capsule, {Address(to_canonical_address(ursula)) for ursula in ursulas}))
|
||||
|
||||
# kit with no ursulas
|
||||
encrypting_key = SecretKey.random().public_key()
|
||||
capsule = MessageKit(encrypting_key, b'testing retrieval kit with no ursulas').capsule
|
||||
capsule = MessageKit(policy_encrypting_key=encrypting_key, plaintext=b'testing retrieval kit with no ursulas').capsule
|
||||
run_tests_on_kit(kit=RetrievalKitClass(capsule, set()))
|
||||
|
||||
with pytest.raises(InvalidInputData):
|
||||
|
|
|
@ -68,8 +68,8 @@ class SyncedMockW3Eth:
|
|||
|
||||
# Support older and newer versions of web3 py in-test
|
||||
version = 5
|
||||
chainId = hex(5)
|
||||
blockNumber = 5
|
||||
chain_id = hex(5)
|
||||
block_number = 5
|
||||
|
||||
def getBlock(self, blockNumber):
|
||||
return {
|
||||
|
@ -122,7 +122,7 @@ class SyncedMockWeb3:
|
|||
return self.provider.clientVersion
|
||||
|
||||
@property
|
||||
def isConnected(self):
|
||||
def is_connected(self):
|
||||
return lambda: True
|
||||
|
||||
|
||||
|
@ -322,8 +322,8 @@ def test_ganache_web3_client():
|
|||
def test_gas_prices(mocker, mock_ethereum_client):
|
||||
web3_mock = mock_ethereum_client.w3
|
||||
|
||||
web3_mock.eth.generateGasPrice = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
|
||||
type(web3_mock.eth).gasPrice = PropertyMock(return_value=DEFAULT_GAS_PRICE) # See docs of PropertyMock
|
||||
web3_mock.eth.generate_gas_price = mocker.Mock(side_effect=[None, GAS_PRICE_FROM_STRATEGY])
|
||||
type(web3_mock.eth).gas_price = PropertyMock(return_value=DEFAULT_GAS_PRICE) # See docs of PropertyMock
|
||||
|
||||
assert mock_ethereum_client.gas_price == DEFAULT_GAS_PRICE
|
||||
assert mock_ethereum_client.gas_price_for_transaction("there's no gas strategy") == DEFAULT_GAS_PRICE
|
||||
|
|
|
@ -69,7 +69,7 @@ def token_airdrop(token_agent, amount: NU, transacting_power: TransactingPower,
|
|||
|
||||
|
||||
def free_gas_price_strategy(w3, transaction_params=None):
|
||||
return 0
|
||||
return None
|
||||
|
||||
|
||||
class TesterBlockchain(BlockchainDeployerInterface):
|
||||
|
@ -173,11 +173,11 @@ class TesterBlockchain(BlockchainDeployerInterface):
|
|||
tx_hashes = list()
|
||||
for address in addresses:
|
||||
tx = {'to': address, 'from': coinbase, 'value': amount, 'gasPrice': self.w3.eth.generate_gas_price()}
|
||||
txhash = self.w3.eth.sendTransaction(tx)
|
||||
txhash = self.w3.eth.send_transaction(tx)
|
||||
|
||||
_receipt = self.wait_for_receipt(txhash)
|
||||
tx_hashes.append(txhash)
|
||||
eth_amount = Web3().fromWei(amount, 'ether')
|
||||
eth_amount = Web3().from_wei(amount, 'ether')
|
||||
self.log.info("Airdropped {} ETH {} -> {}".format(eth_amount, tx['from'], tx['to']))
|
||||
|
||||
return tx_hashes
|
||||
|
@ -203,11 +203,11 @@ class TesterBlockchain(BlockchainDeployerInterface):
|
|||
else:
|
||||
raise ValueError("Specify either hours, or seconds.")
|
||||
|
||||
now = self.w3.eth.getBlock('latest').timestamp
|
||||
now = self.w3.eth.get_block('latest').timestamp
|
||||
end_timestamp = ((now+duration)//base) * base
|
||||
|
||||
self.w3.eth.web3.testing.timeTravel(timestamp=end_timestamp)
|
||||
self.w3.eth.web3.testing.mine(1)
|
||||
self.w3.eth.w3.testing.timeTravel(timestamp=end_timestamp)
|
||||
self.w3.eth.w3.testing.mine(1)
|
||||
|
||||
delta = maya.timedelta(seconds=end_timestamp-now)
|
||||
self.log.info(f"Time traveled {delta} "
|
||||
|
|
|
@ -18,14 +18,17 @@ along with nucypher. If not, see <https://www.gnu.org/licenses/>.
|
|||
import os
|
||||
import random
|
||||
import string
|
||||
from typing import Dict, Tuple
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from nucypher_core import MessageKit, RetrievalKit
|
||||
|
||||
from nucypher.characters.control.specifications.fields import Key, TreasureMap
|
||||
from nucypher.characters.lawful import Enrico
|
||||
from nucypher.control.specifications.fields import JSON
|
||||
from nucypher.crypto.powers import DecryptingPower
|
||||
from nucypher.utilities.porter.control.specifications.fields import RetrievalKit as RetrievalKitField
|
||||
from nucypher.utilities.porter.control.specifications.fields import (
|
||||
RetrievalKit as RetrievalKitField,
|
||||
)
|
||||
|
||||
|
||||
def generate_random_label() -> bytes:
|
||||
|
@ -41,33 +44,82 @@ def generate_random_label() -> bytes:
|
|||
return bytes(random_label, encoding='utf-8')
|
||||
|
||||
|
||||
def retrieval_request_setup(enacted_policy, bob, alice, original_message: bytes = None, encode_for_rest: bool = False) -> Tuple[Dict, MessageKit]:
|
||||
treasure_map = bob._decrypt_treasure_map(enacted_policy.treasure_map,
|
||||
enacted_policy.publisher_verifying_key)
|
||||
def retrieval_request_setup(enacted_policy,
|
||||
bob,
|
||||
alice,
|
||||
specific_messages: Optional[List[bytes]] = None,
|
||||
context: Optional[Dict] = None,
|
||||
encode_for_rest: bool = False,
|
||||
num_random_messages: int = None) -> Tuple[Dict, List[MessageKit]]:
|
||||
"""
|
||||
Creates dict of values for a retrieval request. If no specific messages or number
|
||||
of random messages are provided, a single random message is encrypted.
|
||||
"""
|
||||
if specific_messages and num_random_messages is not None:
|
||||
raise ValueError(
|
||||
"Provide either original_message or num_random_messages parameter, not both."
|
||||
)
|
||||
if not specific_messages and num_random_messages is None:
|
||||
# default to one random message
|
||||
num_random_messages = 1
|
||||
|
||||
treasure_map = bob._decrypt_treasure_map(
|
||||
enacted_policy.treasure_map, enacted_policy.publisher_verifying_key
|
||||
)
|
||||
|
||||
# We pick up our story with Bob already having followed the treasure map above, ie:
|
||||
bob.start_learning_loop()
|
||||
|
||||
# We can pass any number of capsules as args; here we pass just one.
|
||||
enrico = Enrico(policy_encrypting_key=enacted_policy.public_key)
|
||||
if not original_message:
|
||||
original_message = ''.join(random.choice(string.ascii_lowercase) for i in range(20)).encode() # random message
|
||||
message_kit = enrico.encrypt_message(original_message)
|
||||
message_kits = []
|
||||
if specific_messages:
|
||||
for message in specific_messages:
|
||||
message_kits.append(enrico.encrypt_message(message))
|
||||
else:
|
||||
for i in range(num_random_messages):
|
||||
random_message = "".join(
|
||||
random.choice(string.ascii_lowercase) for j in range(20)
|
||||
).encode() # random message
|
||||
message_kits.append(enrico.encrypt_message(random_message))
|
||||
|
||||
encode_bytes = (lambda field, obj: field()._serialize(value=obj, attr=None, obj=None)) if encode_for_rest else (lambda field, obj: obj)
|
||||
|
||||
return (dict(treasure_map=encode_bytes(TreasureMap, treasure_map),
|
||||
retrieval_kits=[encode_bytes(RetrievalKitField, RetrievalKit.from_message_kit(message_kit))],
|
||||
alice_verifying_key=encode_bytes(Key, alice.stamp.as_umbral_pubkey()),
|
||||
bob_encrypting_key=encode_bytes(Key, bob.public_keys(DecryptingPower)),
|
||||
bob_verifying_key=encode_bytes(Key, bob.stamp.as_umbral_pubkey())),
|
||||
message_kit)
|
||||
retrieval_params = dict(
|
||||
treasure_map=encode_bytes(TreasureMap, treasure_map),
|
||||
retrieval_kits=[
|
||||
encode_bytes(RetrievalKitField, RetrievalKit.from_message_kit(message_kit))
|
||||
for message_kit in message_kits
|
||||
],
|
||||
alice_verifying_key=encode_bytes(Key, alice.stamp.as_umbral_pubkey()),
|
||||
bob_encrypting_key=encode_bytes(Key, bob.public_keys(DecryptingPower)),
|
||||
bob_verifying_key=encode_bytes(Key, bob.stamp.as_umbral_pubkey()),
|
||||
)
|
||||
# context is optional
|
||||
if context:
|
||||
retrieval_params["context"] = encode_bytes(JSON, context)
|
||||
|
||||
return retrieval_params, message_kits
|
||||
|
||||
|
||||
def retrieval_params_decode_from_rest(retrieval_params: Dict) -> Dict:
|
||||
decode_bytes = (lambda field, data: field()._deserialize(value=data, attr=None, data=None))
|
||||
return dict(treasure_map=decode_bytes(TreasureMap, retrieval_params['treasure_map']),
|
||||
retrieval_kits=[decode_bytes(RetrievalKitField, kit) for kit in retrieval_params['retrieval_kits']],
|
||||
alice_verifying_key=decode_bytes(Key, retrieval_params['alice_verifying_key']),
|
||||
bob_encrypting_key=decode_bytes(Key, retrieval_params['bob_encrypting_key']),
|
||||
bob_verifying_key=decode_bytes(Key, retrieval_params['bob_verifying_key']))
|
||||
decode_bytes = lambda field, data: field()._deserialize(
|
||||
value=data, attr=None, data=None
|
||||
)
|
||||
decoded_params = dict(
|
||||
treasure_map=decode_bytes(TreasureMap, retrieval_params["treasure_map"]),
|
||||
retrieval_kits=[
|
||||
decode_bytes(RetrievalKitField, kit)
|
||||
for kit in retrieval_params["retrieval_kits"]
|
||||
],
|
||||
alice_verifying_key=decode_bytes(Key, retrieval_params["alice_verifying_key"]),
|
||||
bob_encrypting_key=decode_bytes(Key, retrieval_params["bob_encrypting_key"]),
|
||||
bob_verifying_key=decode_bytes(Key, retrieval_params["bob_verifying_key"]),
|
||||
)
|
||||
# context is optional
|
||||
if "context" in retrieval_params:
|
||||
decoded_params["context"] = decode_bytes(
|
||||
JSON, retrieval_params["context"]
|
||||
)
|
||||
|
||||
return decoded_params
|
||||
|
|
Loading…
Reference in New Issue