Skip to content

Commit d07b688

Browse files
committed
v1.1.3
1 parent 4257612 commit d07b688

File tree

9 files changed

+139
-34
lines changed

9 files changed

+139
-34
lines changed

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
recursive-include alpaca_finance/abi *
1+
recursive-include alpaca_finance/automated_vault/abi *
22
include requirements.txt

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,16 @@ How to use the package
5555
from alpaca_finance.automated_vault import AutomatedVaultPosition
5656
```
5757

58+
<!--
5859
2. ***(Optional)*** Create your Web3 provider object to interact with the network (By default, the BSC RPC URL is used):
5960
```python
6061
from alpaca_finance.util import get_web3_provider
6162
6263
provider = get_web3_provider("your_rpc_url")
6364
```
65+
-->
6466

65-
3. Creating an [AutomatedVaultPosition](alpaca_finance/automated_vault/positions.py) instance requires the following:
67+
2. Creating an [AutomatedVaultPosition](alpaca_finance/automated_vault/positions.py) instance requires the following:
6668
- Your position key (string)
6769
- This key should match your position key on Alpaca Finance's webapp
6870
- ![demo](img/demo.png)
@@ -76,6 +78,21 @@ How to use the package
7678
```python
7779
position = AutomatedVaultPosition(position_key="n3x-BNBBUSD-PCS1", owner_wallet_address="0x...", owner_private_key="123abc456efg789hij...")
7880
```
81+
82+
3. How to **approve tokens**:
83+
- Tokens that have never been approved on the Alpaca web interface will need to be approved programmatically
84+
- The current options for token approval are as follows:
85+
1. Using the `AutomatedVaultPosition.auto_token_approval` attribute:
86+
```python
87+
# Set to False by default
88+
# Tokens are only approved if the allowance is insufficent for the transaction
89+
position.auto_token_approval = True
90+
```
91+
2. Using the `AutomatedVaultPosition.do_approve_token` method:
92+
93+
See the [approve_token.ipynb](examples/automated_vault/approve_token.ipynb) example file
94+
95+
7996
4. Use your position instance to interact with Alpaca Finance:
8097
- For reference, see the BEP20Token class [documentation](https://github.com/hschickdevs/Python-BEP20-Token/blob/main/bep20/token.py)
8198
- Please view the **usage examples** under [examples/automated_vault](examples/automated_vault)

alpaca_finance/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.1.1"
1+
__version__ = "1.1.3"

alpaca_finance/automated_vault/position.py

Lines changed: 88 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,36 +46,47 @@ def __init__(self, position_key: str, owner_wallet_address: str, owner_wallet_ke
4646
self.stable_token = BEP20Token(self.vault.stableTokenAddress(), self.w3_provider)
4747
self.asset_token = BEP20Token(self.vault.assetTokenAddress(), self.w3_provider)
4848

49+
# If True, enables the allowance verification and automatic approval of tokens before transactions
50+
self.auto_token_approval = False
51+
52+
# Specify custom web3 transaction gasPrice parameter for the BSC transactions
53+
self.gasPrice = 5000000000
54+
4955
""" ------------------ Transactional Methods (Requires private wallet key) ------------------ """
5056

51-
def do_invest(self, stable_token_amt: int = 0, asset_token_amt: int = 0, _approve=False) -> TransactionReceipt:
57+
def do_invest(self, stable_token_amt: int = 0, asset_token_amt: int = 0) -> TransactionReceipt:
5258
"""
5359
Invest the specified amount of each token into the Automated Vault.
5460
Use self.asset_token and self.stable_token to identify the underlying assets.
5561
5662
:param stable_token_amt: The amount of stable token to deposit
5763
:param asset_token_amt: The amount of asset token to deposit
58-
:param _approve: If True, approves the deposit token for spending by the vault token
59-
:return:
64+
65+
:return: TransactionReceipt object
6066
"""
6167
assert stable_token_amt > 0 or asset_token_amt > 0, \
6268
"Please provide an investment value for either the stable or asset tokens"
6369

70+
txn_nonce = self._get_nonce()
71+
6472
# Ensure that allowances match desired investment amount
6573
if stable_token_amt > 0:
6674
token_bal = self.stable_token.balanceOf(self.owner_address)
6775
assert token_bal >= stable_token_amt, \
6876
f"Insufficient funds to invest {stable_token_amt} {self.stable_token.symbol()} ({token_bal} Owned)"
69-
if _approve:
70-
self.do_approve_token(self.stable_token)
77+
if self.auto_token_approval:
78+
if self.do_approve_token(self.stable_token) is not None:
79+
txn_nonce += 1
7180
if asset_token_amt > 0:
7281
token_bal = self.asset_token.balanceOf(self.owner_address)
7382
assert token_bal >= asset_token_amt, \
7483
f"Insufficient funds to invest {asset_token_amt} {self.asset_token.symbol()} ({token_bal} Owned)"
75-
if _approve:
76-
self.do_approve_token(self.asset_token)
84+
if self.auto_token_approval:
85+
if self.do_approve_token(self.asset_token) is not None:
86+
txn_nonce += 1
7787

78-
return self._execute(self.vault.invest(stable_token_amt, asset_token_amt, shareReceiver=self.owner_address))
88+
return self._execute(self.vault.invest(stable_token_amt, asset_token_amt, shareReceiver=self.owner_address),
89+
_nonce=txn_nonce)
7990

8091
def do_withdraw(self, shares: int, pct_stable: float = None, strategy: str = "Minimize Trading") -> TransactionReceipt:
8192
"""
@@ -89,56 +100,90 @@ def do_withdraw(self, shares: int, pct_stable: float = None, strategy: str = "Mi
89100
assert self.shares()[0] >= shares, f"Shares owned insufficient to withdraw {shares} " \
90101
f"({self.from_wei(shares, self.bep20_vault_token.decimals())}) shares"
91102

103+
txn_nonce = self._get_nonce()
104+
105+
# Process withdraw functions according to the strategy passed:
92106
if strategy.lower() == "minimize trading":
93-
# assert self.bep20_vault_token.allowance(self.owner_address, self.vault.address) >= shares, \
94-
# f"Insufficient approval amount - Spender ({self.vault.address}) requires an allowance of {shares} " \
95-
# f"{self.bep20_vault_token.symbol()} ({self.bep20_vault_token.address})"
107+
if self.auto_token_approval:
108+
if self.do_approve_token(self.bep20_vault_token, _spender=self.vault.address, _min_amount=shares) is not None:
109+
txn_nonce += 1
110+
else:
111+
assert self.bep20_vault_token.allowance(self.owner_address, self.vault.address) >= shares, \
112+
f"Insufficient approval amount - Spender ({self.vault.address}) requires an allowance of {shares} " \
113+
f"{self.bep20_vault_token.symbol()} ({self.bep20_vault_token.address})"
114+
115+
return self._execute(self.vault.withdraw(shares), _nonce=txn_nonce)
96116

97-
return self._execute(self.vault.withdraw(shares))
98117
elif strategy.lower() == "convert all":
99118
assert pct_stable is not None, "Please provide a stable token percentage to determine token swap"
100-
assert self.bep20_vault_token.allowance(self.owner_address, self.gateway.address) >= shares, \
101-
f"Insufficient approval amount - Spender ({self.gateway.address}) requires an allowance of {shares} " \
102-
f"{self.bep20_vault_token.symbol()} ({self.bep20_vault_token.address})"
103119

104-
assert 0.0 < pct_stable <= 1.0, "Invalid value for pct_stable parameter, must follow 0.0 < pct_stable <= 1.0"
120+
if self.auto_token_approval:
121+
if self.do_approve_token(self.bep20_vault_token, _spender=self.gateway.address, _min_amount=shares) is not None:
122+
txn_nonce += 1
123+
else:
124+
assert self.bep20_vault_token.allowance(self.owner_address, self.gateway.address) >= shares, \
125+
f"Insufficient approval amount - Spender ({self.gateway.address}) requires an allowance of {shares} " \
126+
f"{self.bep20_vault_token.symbol()} ({self.bep20_vault_token.address})"
127+
128+
assert 0.0 <= pct_stable <= 1.0, "Invalid value for pct_stable parameter, must follow 0.0 <= pct_stable <= 1.0"
105129

106130
stable_return_bps = floor(pct_stable * 10000)
107131

108-
return self._execute(self.gateway.withdraw(shares, stableReturnBps=stable_return_bps))
132+
return self._execute(self.gateway.withdraw(shares, stableReturnBps=stable_return_bps), _nonce=txn_nonce)
133+
109134
else:
110135
raise ValueError("Invalid strategy - Options are 'Minimize Trading' or 'Convert All'")
111136

112137
def do_close(self, pct_stable: float = None, strategy: str = "Minimize Trading") -> TransactionReceipt:
113138
"""
114139
Withdraws all outstanding shares from the pool and closes position.
115140
116-
:param pct_stable: See self.withdraw()
117-
:param strategy: See self.withdraw()
141+
:param pct_stable: See self.do_withdraw()
142+
:param strategy: See self.do_withdraw()
118143
"""
119-
return self.do_withdraw(shares=self.shares()[0], pct_stable=pct_stable, strategy=strategy)
144+
shares = self.shares()[0]
120145

121-
def do_approve_token(self, token: Union[BEP20Token, str], amount: int = None, _spender: str = None) -> TransactionReceipt:
146+
assert shares > 0, f"Cannot close position with a balance of {shares} shares."
147+
148+
return self.do_withdraw(shares=shares, pct_stable=pct_stable, strategy=strategy)
149+
150+
def do_approve_token(self, token: Union[BEP20Token, str], amount: int = None, _min_amount: int = None,
151+
_spender: str = None) -> Union[TransactionReceipt, None]:
122152
"""
123153
Approves the given token for usage by the Automated Vault.
124154
125155
:param token: Optional - either the BEP20Token object, or the token address (str)
126156
:param amount: The amount of token to approve, default = maximum
127157
:param _spender: (Should not be changed by the caller) The address to give token spending access to
158+
:param _min_amount: Used for internal functions when checking to see if a token has the minimum approval requirement
159+
160+
:return:
161+
If the current token allowance already meets or exceeds the given amount:
162+
- None
163+
Else if the current token allowance does not meet the given amount:
164+
- TransactionReceipt object
128165
"""
129166
if type(token) != BEP20Token:
130167
token = BEP20Token(token)
131168

169+
print(f"Approving {token.symbol()}...")
170+
132171
if amount is None:
133172
# Use maximum approval amount if no amount is specified
134173
amount = 2 ** 256 - 1
135174

136175
if _spender is None:
137176
_spender = self.address
138177

139-
func_call = token.prepare_approve(checksum(_spender), amount)
178+
if token.allowance(self.owner_address, _spender) >= (amount if _min_amount is None else _min_amount):
179+
print("Approval canceled - allowance already exceeds amount")
180+
return None
140181

141-
return self._execute(func_call)
182+
txn = self._execute(token.prepare_approve(checksum(_spender), amount))
183+
184+
print(f"Approved {'MAX' if amount == (2 ** 256 - 1) else amount} {token.symbol()} for contract {_spender}.")
185+
186+
return txn
142187

143188

144189
""" ---------------- Informational Methods (Private wallet key not required) ---------------- """
@@ -187,6 +232,14 @@ def get_vault_summary(self, position_key: str = None) -> AttrDict:
187232
for vault in r.json()["data"]["strategyPools"]:
188233
if vault["key"].lower() == position_key:
189234
return AttrDict(vault)
235+
else:
236+
try:
237+
if vault["iuToken"]["symbol"].lower() == position_key:
238+
print(f"Warning: Had to use iuToken to match vault data instead of key (Key = {vault['key']})")
239+
return AttrDict(vault)
240+
except KeyError:
241+
# In the event that the vault data did not have an iuToken key for some reason
242+
pass
190243
else:
191244
raise ValueError(f"Could not locate a vault with the key {self.key}")
192245

@@ -261,7 +314,7 @@ def cost_basis(self) -> float: # tuple[float, float]
261314

262315
""" -------------------------------- Utility Methods -------------------------------- """
263316

264-
def _execute(self, function_call: web3.contract.ContractFunction) -> TransactionReceipt:
317+
def _execute(self, function_call: web3.contract.ContractFunction, _nonce: int = None) -> Union[TransactionReceipt, AttrDict]:
265318
"""
266319
:param function_call: The uncalled and prepared contract method to sign and send
267320
"""
@@ -272,8 +325,8 @@ def _execute(self, function_call: web3.contract.ContractFunction) -> Transaction
272325
"from": self.owner_address,
273326
'chainId': 56, # 56: BSC mainnet
274327
# 'gas': 76335152,
275-
'gasPrice': 5000000000,
276-
'nonce': self.w3_provider.eth.get_transaction_count(self.owner_address),
328+
'gasPrice': self.gasPrice, # 5000000000
329+
'nonce': self._get_nonce() if _nonce is None else _nonce,
277330
})
278331
# print(txn)
279332

@@ -289,7 +342,10 @@ def _execute(self, function_call: web3.contract.ContractFunction) -> Transaction
289342
except Exception as exc:
290343
print(f"COULD NOT BUILD TRANSACTION RECEIPT OBJECT - {exc}")
291344
# Catch case to prevent receipt from being lost if TransactionReceipt object somehow can't be built
292-
return receipt
345+
return AttrDict(receipt)
346+
347+
def _get_nonce(self) -> int:
348+
return self.w3_provider.eth.get_transaction_count(self.owner_address)
293349

294350
@staticmethod
295351
def to_wei(amt: float, decimals: int) -> int:
@@ -316,8 +372,10 @@ def _decode_withdraw_transaction(self, transaction_address: Optional):
316372

317373
try:
318374
decoded_bank_transaction = self.vault.contract.decode_function_input(transaction.input)
375+
print("withdraw on vault")
319376
except:
320377
decoded_bank_transaction = self.gateway.contract.decode_function_input(transaction.input)
378+
print("withdraw on gateway")
321379

322380
decoded_calldata = decode_abi(
323381
['uint256', 'uint256'],
@@ -339,12 +397,14 @@ def _decode_deposit_transaction(self, transaction_address: Optional):
339397
)
340398
"""
341399
transaction = self.w3_provider.eth.get_transaction(transaction_address)
342-
# print(transaction)
400+
print(transaction)
343401

344402
try:
345403
decoded_bank_transaction = self.vault.contract.decode_function_input(transaction.input)
404+
print("deposit on vault")
346405
except:
347406
decoded_bank_transaction = self.gateway.contract.decode_function_input(transaction.input)
407+
print("deposit on gateway")
348408

349409
decoded_calldata = decode_abi(
350410
["uint256"],

examples/automated_vault/approve_token.ipynb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@
6666
"position.name, position.key, position.asset_token.symbol(), position.bep20_vault_token.symbol()"
6767
]
6868
},
69+
{
70+
"cell_type": "markdown",
71+
"metadata": {},
72+
"source": [
73+
"> **NOTE**: As of v1.1.3, you can now set the `position.auto_token_approval` attribute to `True` to automatically approve tokens.\n",
74+
"\n",
75+
"- This will only approve tokens that lack the required allowance for the transaction to be completed."
76+
]
77+
},
6978
{
7079
"cell_type": "markdown",
7180
"metadata": {},

examples/automated_vault/close_position.ipynb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@
7272
"source": [
7373
"### Close the Position (Automatically withdraws all shares):\n",
7474
"\n",
75-
"- Using the \"convert all\" strategy to withdraw tokens"
75+
"- Using the \"convert all\" strategy to withdraw tokens\n",
76+
"- Setting `auto_token_approval` to `True` so that the contract can automatically withdraw tokens even if the vault token has not been approved."
7677
]
7778
},
7879
{
@@ -92,6 +93,8 @@
9293
}
9394
],
9495
"source": [
96+
"position.auto_token_approval = True\n",
97+
"\n",
9598
"txn_receipt = position.do_close(pct_stable=1.0, strategy=\"convert all\")\n",
9699
"txn_receipt"
97100
]

examples/automated_vault/invest.ipynb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
"# Create a automated vault position instance:\n",
6464
"position = AutomatedVaultPosition(position_key=POSITION_KEY, owner_wallet_address=PUBKEY, owner_wallet_key=PRIVKEY)\n",
6565
"\n",
66+
"# Set automated token approval so that allowances on deposit tokens are increased if needed:\n",
67+
"position.auto_token_approval = True\n",
68+
"\n",
69+
"# Show position info:\n",
6670
"position.name, position.key, position.shares()[1]"
6771
]
6872
},

examples/automated_vault/withdraw.ipynb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
"# Create a automated vault position instance:\n",
6464
"position = AutomatedVaultPosition(position_key=POSITION_KEY, owner_wallet_address=PUBKEY, owner_wallet_key=PRIVKEY)\n",
6565
"\n",
66+
"# Enable auto token approval so that allowances are extended automatically if needed:\n",
67+
"position.auto_token_approval = True\n",
68+
"\n",
69+
"# Show position Info:\n",
6670
"position.name, position.key, position.shares()[1]"
6771
]
6872
},

setup.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,28 @@
1-
from setuptools import setup
1+
from setuptools import setup, find_packages
22
import sys
33

44
from alpaca_finance import __version__
55

66
if sys.version_info < (3, 9):
77
sys.exit('Python 3.9+ required to install this package. Install it here: https://www.python.org/downloads/')
88

9+
10+
def readme():
11+
with open("README.md") as infile:
12+
return infile.read().strip()
13+
14+
915
setup(
1016
name='alpaca_finance',
1117
version=__version__,
1218
author='Harrison Schick',
1319
author_email='[email protected]',
1420
description='An unofficial Python3.9+ package that models positions on the Alpaca Finance platform to simplify interaction with their smart contracts in your Python projects.',
21+
long_description=readme(),
22+
long_description_content_type="text/markdown",
1523
url='https://github.com/PathX-Projects/Alpaca-Finance-Python',
1624
# license='MIT',
17-
packages=['alpaca_finance'],
25+
packages=find_packages(),
1826
include_package_data=True,
1927
install_requires=[line.strip() for line in open('requirements.txt').readlines()],
2028
)

0 commit comments

Comments
 (0)