Skip to content

Commit 9d740e9

Browse files
committed
Progress toward passing all tests
o Break out package extras [invoice] from [wallet], and add [all] o Remove the extras [dev]; move into requirements-tests.txt o Default to Alchemy "Testing" API token w/ warning
1 parent 7966d57 commit 9d740e9

File tree

10 files changed

+97
-80
lines changed

10 files changed

+97
-80
lines changed

GNUmakefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ dist/slip39-$(VERSION)-py3-none-any.whl: build-check FORCE
167167

168168
# Install from wheel, including all optional extra dependencies (except dev)
169169
install: dist/slip39-$(VERSION)-py3-none-any.whl FORCE
170-
$(PY3) -m pip install --force-reinstall $<[gui,wallet,serial]
170+
$(PY3) -m pip install --force-reinstall $<[all]
171171

172172
install-dev:
173173
$(PY3) -m pip install --upgrade -r requirements-dev.txt

requirements-dev.txt

Lines changed: 0 additions & 9 deletions
This file was deleted.

requirements-tests.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
aiosmtpd >=1.4, <2
2+
build
3+
cx_Freeze >=6.12
14
flake8
5+
pyinstaller >=5.5
26
pytest >=7.2.0,<8
37
pytest-cov >=4.0.0,<5
4-
aiosmtpd >=1.4, <2
8+
setuptools
9+
wheel

requirements-wallet.txt

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1 @@
11
eth-account >=0.7.0,<0.8
2-
py-solc-x >=1.1.1,<1.2
3-
pycryptodome >=3.16, <4
4-
requests >=2.20, <3
5-
tabulate >=0.9, <1
6-
crypto-licensing >=3.0.3
7-
dkimpy[ed25519] >=1.0.5,<2
8-
9-
# These versions are very brittle; must be upgraded in lock-step (see web3.py/setup.py)
10-
web3[tester] ==6.0.0b8
11-
eth-tester[py-evm] ==v0.7.0-beta.1

setup.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import glob
44
import fnmatch
55

6-
76
#
87
# All platforms
98
#
@@ -12,14 +11,21 @@
1211
install_requires = open( os.path.join( HERE, "requirements.txt" )).readlines()
1312
tests_require = open( os.path.join( HERE, "requirements-tests.txt" )).readlines()
1413
extras_require = {
15-
option: open( os.path.join( HERE, f"requirements-{option}.txt" )).readlines()
14+
option: list(
15+
# Remove whitespace, elide blank lines and comments
16+
''.join( r.split() )
17+
for r in open( os.path.join( HERE, f"requirements-{option}.txt" )).readlines()
18+
if r.strip() and not r.strip().startswith( '#' )
19+
)
1620
for option in [
17-
'gui', # slip39[gui]: Support PySimpleGUI/tkinter Graphical UI App
18-
'dev', # slip39[dev]: All modules to support development
19-
'serial', # slip39[serial]: Support serial I/O of generated wallet data
20-
'wallet', # slip39[wallet]: Paper Wallet and BIP-38/Ethereum wallet encryption
21+
'gui', # slip39[gui]: Support PySimpleGUI/tkinter Graphical UI App
22+
'serial', # slip39[serial]: Support serial I/O of generated wallet data
23+
'wallet', # slip39[wallet]: Paper Wallet and BIP-38/Ethereum wallet encryption
24+
'invoice', # slip39[invoice]: Generation of invoices, and associated Smart Contracts
2125
]
2226
}
27+
# Make python-slip39[all] install all extra (non-tests) requirements, excluding duplicates
28+
extras_require['all'] = list( set( sum( extras_require.values(), [] )))
2329

2430
Executable = None
2531
if sys.platform == 'win32':

slip39/invoice/communications_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def test_communications_dkim():
117117
msg,
118118
#from_addr = SMTP_FROM, # Envelope MAIL FROM: specifies actual sender
119119
#to_addrs = [ SMTP_TO ], # Will be the same as message To: (should default)
120-
relay = ['mail2.kundert.ca'], # 'localhost', # use eg. ssh -fNL 0.0.0.0:25:linda.mx.cloudflare.net:25 [email protected]
120+
#relay = ['mail2.kundert.ca'], # 'localhost', # use eg. ssh -fNL 0.0.0.0:25:linda.mx.cloudflare.net:25 [email protected]
121121
#port = 25, # 465 --> SSL, 587 --> TLS (default),
122122
#usessl = False, starttls = False, verifycert = False, # to mail.kundert.ca; no TLS
123123
#usessl = False, starttls = True, verifycert = False, # default

slip39/invoice/ethereum.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ def contract_address(
6666
assert isinstance( b_address, bytes ) and len( b_address ) == 20, \
6767
f"Expected 20-byte adddress, got {b_address!r}"
6868

69-
if nonce is not None and salt is None and creation is None and creation_hash:
70-
# A CREATE (traditional, or transaction-based) contract creation
69+
if nonce is not None and salt is None and creation is None and creation_hash is None:
70+
# A CREATE (traditional, or transaction-based) contract creation; only address and nonce
7171
assert isinstance( nonce, int ), \
7272
f"The nonce for CREATE must be an integer, not {nonce!r}"
7373
b_result = Web3.keccak( rlp_encode([ b_address, nonce ]) )
@@ -99,11 +99,56 @@ class Chain( Enum ):
9999
Goerli = 2
100100

101101

102+
#
103+
# Etherscan API, for accessing Gas Oracle and wallet data
104+
#
102105
etherscan_urls = dict(
103106
Ethereum = 'https://api.etherscan.io/api',
104107
Goerli = 'https://api-goerli.etherscan.io/api',
105108
)
106109

110+
#
111+
# Alchemy API, for accessing the Ethereum blockchain w/o running a local instance
112+
#
113+
# They provide a free "testing" API token, with a very low 50 "Compute Units/Second" limit, for
114+
# testing their API: https://docs.alchemy.com/reference/throughput. This should be sufficient for
115+
# accessing "free" (view) APIs in contracts. However, for any advanced usage (ie. to deploy
116+
# contracts), you'll probably need an official ALCHEMY_API_TOKEN
117+
#
118+
alchemy_urls = dict(
119+
Ethereum = 'eth-mainnet.g.alchemy.com/v2',
120+
Goerli = 'eth-goerli.g.alchemy.com/v2',
121+
)
122+
123+
alchemy_api_testing = dict(
124+
Goerli = 'AxnmGEYn7VDkC4KqfNSFbSW9pHFR7PDO',
125+
Ethereum = 'J038e3gaccJC6Ue0BrvmpjzxsdfGly9n',
126+
)
127+
128+
129+
def alchemy_url( chain, protocol='wss' ):
130+
"""Return our Alchemy API URL, including our API token, from the ALCHEMY_API_TOKEN environment
131+
variable.
132+
133+
If none specified, we'll default to their "Testing" API token, which should be adequate for
134+
low-rate "free" (view) contract APIs. However, for deploying contracts, either an official
135+
(free) Alchemy API token will be required, or a local Ethereum node.
136+
137+
"""
138+
assert protocol in ( 'wss', 'https' ), \
139+
"Must specify either Websocket over SSL ('wss') or HTTP over SSL ('https')"
140+
api_token = os.getenv( 'ALCHEMY_API_TOKEN' )
141+
if not alchemy_url.api_token or api_token != alchemy_url.api_token:
142+
# Alchemy API token not yet discovered, or has changed
143+
if api_token:
144+
alchemy_url.api_token = api_token
145+
log.info( f"Using supplied Alchemy {chain} API token: {alchemy_url.api_token:.5}..." )
146+
elif not alchemy_url.api_token:
147+
alchemy_url.api_token = alchemy_api_testing[chain.name]
148+
log.warning( f"Using \"Testing\" Alchemy {chain} API token: {alchemy_url.api_token}; obtain an official API key: https://docs.alchemy.com/reference/api-overview" )
149+
return f"{protocol}://{alchemy_urls[chain.name]}/{alchemy_url.api_token}"
150+
alchemy_url.api_token = None # noqa E305
151+
107152

108153
@memoize( maxage=ETHERSCAN_MEMO_MAXAGE, maxsize=ETHERSCAN_MEMO_MAXSIZE, log_at=logging.INFO )
109154
def etherscan( chain, params, headers=None, apikey=None, timeout=None, verify=True ):
@@ -628,7 +673,7 @@ class Contract:
628673
def __init__(
629674
self,
630675
w3_provider,
631-
agent, # The Ethereum account of the agent accessing the Contract
676+
agent = None, # The Ethereum account of the agent accessing the Contract
632677
agent_prvkey: Optional[bytes] = None, # Can only query public data, view methods without
633678
source: Optional[Path] = None,
634679
version: Optional[str] = None,
@@ -642,15 +687,16 @@ def __init__(
642687
gas_oracle: Optional[GasOracle] = None,
643688
gas_oracle_timeout: Optional[Tuple[int,float]] = None, # None avoids waiting, doesn't check
644689
):
645-
# If a GasOracle is supplied, well wait up to gas_oracle_timeout seconds for it to report
690+
# If a GasOracle is supplied, we'll wait up to gas_oracle_timeout seconds for it to report
646691
# online. Give it a tickle here to get it started, while we do other time-consuming stuff.
647692
self._gas_oracle = GasOracle() if gas_oracle is None else gas_oracle
648693
gas_oracle_beg = timer()
649694
bool( self._gas_oracle )
650695

651696
self._w3 = Web3( w3_provider )
652697
self._agent = agent
653-
self._w3.eth.default_account = str( self._agent )
698+
if self._agent is not None:
699+
self._w3.eth.default_account = str( self._agent )
654700

655701
if agent_prvkey:
656702
account_signing = eth_account.Account.from_key( '0x' + agent_prvkey )

slip39/invoice/multipayout.py

Lines changed: 20 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
from __future__ import annotations
1919

2020
import logging
21-
import os
2221
import textwrap
2322

2423
from typing import Union
@@ -28,7 +27,6 @@
2827
from tabulate import tabulate
2928

3029
from ..util import remainder_after, fraction_allocated, commas
31-
from ..api import account
3230
from .ethereum import Chain, Contract, contract_address # noqa F401
3331

3432
__author__ = "Perry Kundert"
@@ -38,29 +36,6 @@
3836

3937
log = logging.getLogger( 'multipayout' )
4038

41-
goerli_account = None
42-
goerli_xprvkey = os.getenv( 'GOERLI_XPRVKEY' )
43-
if not goerli_xprvkey:
44-
goerli_seed = os.getenv( 'GOERLI_SEED' )
45-
if goerli_seed:
46-
try:
47-
# why m/44'/1'? Dunno. That's the derivation path Trezor Suite uses for Goerli wallets...
48-
goerli_xprvkey = account( goerli_seed, crypto="ETH", path="m/44'/1'/0'" ).xprvkey
49-
except Exception:
50-
pass
51-
# Using the "xprv..." key, and derive the 1st sub-account, on the combined path m/44'/1'/0'/0/0
52-
goerli_account = account( goerli_xprvkey, crypto='ETH', path="m/0/0" )
53-
54-
55-
alchemy_urls = dict(
56-
Ethereum = 'eth-mainnet.g.alchemy.com/v2',
57-
Goerli = 'eth-goerli.g.alchemy.com/v2',
58-
)
59-
60-
61-
def alchemy_url( chain, protocol='wss' ):
62-
return f"{protocol}://{alchemy_urls[chain.name]}/{os.getenv( 'ALCHEMY_API_TOKEN' )}"
63-
6439

6540
def payout_reserve( addr_frac, bits=16 ):
6641
"""Produce recipients array elements w/ fixed fractional/proportional amounts to predefined
@@ -117,21 +92,32 @@ class MultiPayoutERC20( Contract ):
11792
address, or _deploy a new contract by specifying payees and optionally a list of erc20s to
11893
support.
11994
120-
>>> assert os.getenv( 'ETHERSCAN_API_TOKEN' )
121-
>>> assert os.getenv( 'ALCHEMY_API_TOKEN' )
122-
>>> assert goerli_account
95+
>>> from .ethereum import Chain, alchemy_url
12396
>>> mp = MultiPayoutERC20( Web3.WebsocketProvider( alchemy_url( Chain.Goerli )),
124-
... agent = goerli_account.address,
125-
... agent_prvkey = goerli_account.prvkey,
12697
... address = "0x8b3D24A120BB486c2B7583601E6c0cf37c9A2C04"
12798
... )
99+
MultiPayoutERC20 Payees:
100+
| Payee | Share | Frac. % | Reserve | Reserve/2^16 | Frac.Rec. % | Error % |
101+
|--------------------------------------------+-----------------------+-----------+-----------+----------------+---------------+-----------|
102+
| 0x7Fc431B8FC8250A992567E3D7Da20EE68C155109 | 4323/65536 | 6.59638 | 61213 | 61213 | 6.59638 | 0 |
103+
| 0xEeC2b464c2f50706E3364f5893c659edC9E4153A | 1507247699/4294967296 | 35.0933 | 40913 | 40913 | 35.0933 | 0 |
104+
| 0xE5714055437154E812d451aF86239087E0829fA8 | 2504407469/4294967296 | 58.3103 | 0 | 0 | 58.3103 | 0 |
105+
ERC-20s:
106+
| Token | Symbol | Digits |
107+
|--------------------------------------------+----------+----------|
108+
| 0xe802376580c10fE23F027e1E19Ed9D54d4C9311e | USDT | 6 |
109+
| 0xde637d4C445cA2aae8F782FFAc8d2971b93A4998 | USDC | 6 |
110+
| 0xaFF4481D10270F50f203E0763e2597776068CBc5 | WEENUS | 18 |
111+
| 0x1f9061B953bBa0E36BF50F21876132DcF276fC6e | ZEENUS | 0 |
112+
113+
>>> import json
128114
>>> print( json.dumps( mp._payees, indent=4, default=lambda f: f"{float( f * 100 ):9.5f}%" ))
129115
{
130116
"0x7Fc431B8FC8250A992567E3D7Da20EE68C155109": " 6.59637%",
131117
"0xEeC2b464c2f50706E3364f5893c659edC9E4153A": " 35.09335%",
132118
"0xE5714055437154E812d451aF86239087E0829fA8": " 58.31028%"
133119
}
134-
>>> print( json.dumps( mp._erc20s, indent=4 ))
120+
>>> print( json.dumps( mp._erc20s_data, indent=4 ))
135121
{
136122
"0xe802376580c10fE23F027e1E19Ed9D54d4C9311e": [
137123
"USDT",
@@ -189,8 +175,7 @@ def __str__( self ):
189175
{self.__class__.__name__} Payees:
190176
{textwrap.indent( self._payees_table, ' ' * 4 )}
191177
ERC-20s:
192-
{textwrap.indent( self._erc20s_table, ' ' * 4 )}
193-
"""
178+
{textwrap.indent( self._erc20s_table, ' ' * 4 )}"""
194179

195180
@property
196181
def _erc20s_table( self ):
@@ -215,10 +200,7 @@ def _payees_data( self ):
215200
addrs,fracs,rsrvs = zip( *payout_reserve( self._payees, bits=self._bits ))
216201
share = list( self._payees[a] for a in addrs )
217202
rsrvs_int = list( map( int, rsrvs ))
218-
fracs_recov = list( fraction_allocated(
219-
rsrvs_int,
220-
scale = 2**self._bits,
221-
))
203+
fracs_recov = list( fraction_allocated( rsrvs_int, scale=2**self._bits ))
222204
# Now, compute the "error" between the originally specified payout fractions, and the payout
223205
# fractions recovered after reversing the 1/2^bits reserve calculations. The error should
224206
# always be < 1/2^bits.
@@ -275,7 +257,6 @@ def _payees_reserve( self, payees_reserve ):
275257
log.warning( f"Recovered payees fractions don't match: \n{self._payees_table}" )
276258
else:
277259
self._payees = payees
278-
log.info( f"Recovered payees fractions from reserves: \n{self._payees_table}" )
279260

280261
@property
281262
def _payees_table( self ):
@@ -314,7 +295,6 @@ def _update( self ):
314295
315296
"""
316297
self._forwarder_hash = self.forwarder_hash()
317-
log.info( f"{self._name} Forwarder CREATE2 hash: 0x{self._forwarder_hash.hex()}" )
318298

319299
def payees_reserve():
320300
for i in count():
@@ -349,4 +329,4 @@ def erc20s_data():
349329
if self._erc20s:
350330
assert self._erc20s == erc20s
351331
else:
352-
self._ers20s = erc20s
332+
self._erc20s = erc20s

slip39/invoice/multipayout_test.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from eth_account import Account
1919

2020
from . import contract_address
21-
from .ethereum import Etherscan, Chain
21+
from .ethereum import Etherscan, Chain, alchemy_url
2222
from .multipayout import MultiPayoutERC20, payout_reserve
2323
from ..api import account, accounts
2424

@@ -108,10 +108,9 @@ def test_solcx_smoke():
108108
print( f"Goerli Ethereum Testnet dst ETH addresses: {json.dumps( goerli_destination, indent=4 )}" )
109109

110110
web3_testers += [(
111-
# Web3.HTTPProvider( f"https://eth-goerli.g.alchemy.com/v2/{os.getenv( 'ALCHEMY_API_TOKEN' )}" ),
112111
"Goerli",
113-
Web3.WebsocketProvider( # Provider and chain_id (if any)
114-
f"wss://eth-goerli.g.alchemy.com/v2/{os.getenv( 'ALCHEMY_API_TOKEN' )}"
112+
Web3.WebsocketProvider(
113+
alchemy_url( Chain.Goerli ) # f"wss://eth-goerli.g.alchemy.com/v2/{os.getenv( 'ALCHEMY_API_TOKEN' )}"
115114
), None,
116115
goerli_src.address, goerli_src.prvkey, goerli_destination,
117116
),]
@@ -274,12 +273,12 @@ def test_create2(address, salt, creation, expected_address):
274273
(
275274
'0x6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0',
276275
0,
277-
'0xcd234a471b72ba2f1ccf0a70fcaba648a5eecd8d',
276+
'0xcd234A471b72ba2F1Ccf0A70FCABA648a5eeCD8d',
278277
),
279278
(
280279
'0x6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0',
281280
1,
282-
'0x343c43a37d37dff08ae8c4a11544c718abb4fcf8',
281+
'0x343c43A37D37dfF08AE8C4A11544c718AbB4fCF8',
283282
),
284283
])
285284
def test_create(address, nonce, expected_address):

slip39/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version_info__ = ( 10, 1, 1 )
1+
__version_info__ = ( 10, 2, 0 )
22
__version__ = '.'.join( map( str, __version_info__ ))

0 commit comments

Comments
 (0)