Skip to content

Commit 832fb0b

Browse files
committed
indev
1 parent 2e6be37 commit 832fb0b

21 files changed

+738
-115
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,5 @@ cython_debug/
155155
.DS_Store
156156
.idea
157157
testing.*
158-
jessie_alpaca_example.ipynb
158+
testing2.*
159+
examples/old/jessie_alpaca_example.ipynb

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ How to use the package:
6363

6464
### Automated Vaults:
6565

66-
3. Creating an [AutomatedVaultPosition](alpaca_finance/positions.py) instance requires the following:
66+
3. Creating an [AutomatedVaultPosition](alpaca_finance/automated_vault/positions.py) instance requires the following:
6767
- Your position key (string)
6868
- This key should match your position key on Alpaca Finance's webapp
6969
- ![demo](img/demo.png)
@@ -102,7 +102,7 @@ How to use the package:
102102
# Get the amount of shares held and the USD value of all shares held:
103103
position.shares()
104104
105-
# get the full vault summary (See the documentation alpaca_fiance/positions.py for more details):
105+
# get the full vault summary (See the documentation alpaca_fiance/position.py for more details):
106106
position.get_vault_summary()
107107
```
108108

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .position import AutomatedVaultPosition
File renamed without changes.
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from ..util import get_bsc_contract_instance
2+
from ._config import DELTA_NEUTRAL_ORACLE_ADDRESS, AUTOMATED_VAULT_CONTROLLER_ADDRESS
3+
# from .work_bytes import WithdrawWorkByte # <- Deprecated
4+
5+
import requests
6+
import web3.contract
7+
from web3 import Web3
8+
from bep20.util import checksum
9+
from eth_abi import encode_abi
10+
11+
12+
class DeltaNeutralOracle:
13+
def __init__(self, w3_provider: Web3 = None):
14+
self.contract = get_bsc_contract_instance(contract_address=DELTA_NEUTRAL_ORACLE_ADDRESS,
15+
abi_filename="DeltaNeutralOracle.json",
16+
w3_provider=w3_provider)
17+
18+
def lpToDollar(self, lp_amount: int, pancakeswap_lp_token_address: str) -> int:
19+
"""Return the value in USD for the given lpAmount"""
20+
return self.contract.functions.lpToDollar(lp_amount, checksum(pancakeswap_lp_token_address)).call()
21+
22+
def dollarToLp(self, dollar_amount: int, lp_token_address: str) -> int:
23+
"""Return the amount of LP for the given USD"""
24+
return self.contract.functions.dollarToLp(dollar_amount, checksum(lp_token_address)).call()
25+
26+
def getTokenPrice(self, token_address: str) -> int:
27+
"""Return the price of the given token (address) in USD"""
28+
return self.contract.functions.getTokenPrice(checksum(token_address)).call()[0] / 10 ** 18
29+
30+
31+
class DeltaNeutralVault:
32+
def __init__(self, vault_address: str, w3_provider: Web3 = None, protocol: str = "pancakeswap"):
33+
"""
34+
:param vault_address: The address for the Delta Neutral Vault
35+
:param protocol: The protocol ID that the vault resides on (pancakeswap or biswap)
36+
:param w3_provider: Web3 provider (optional), if provided speeds up initialization time
37+
"""
38+
AVAILABLE_PROTOCOLS = ["pancakeswap", "biswap"]
39+
40+
self.contract = get_bsc_contract_instance(contract_address=vault_address,
41+
abi_filename="DeltaNeutralVault.json", w3_provider=w3_provider)
42+
self.protocol = protocol.lower()
43+
assert protocol in AVAILABLE_PROTOCOLS, NotImplementedError(f"Available protocols are {AVAILABLE_PROTOCOLS}")
44+
45+
self.ACTION_WORK = 1
46+
47+
r = requests.get("https://raw.githubusercontent.com/alpaca-finance/bsc-alpaca-contract/main/.mainnet.json").json()
48+
49+
# Get vault addresses:
50+
try:
51+
self.addresses = list(filter(lambda v: v['address'].lower() == vault_address.lower(), r['DeltaNeutralVaults']))[0]
52+
"""
53+
E.x.
54+
"name": "Long 3x BUSD-BTCB PCS2",
55+
"symbol": "L3x-BUSDBTCB-PCS2",
56+
"address": "0xA1679223b7585725aFb425a6F59737a05e085C40",
57+
"deployedBlock": 18042257,
58+
"config": "0x39936A4eC165e2372C8d91c011eb36576dBE5d32",
59+
"assetToken": "0xe9e7cea3dedca5984780bafc599bd69add087d56",
60+
"stableToken": "0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c",
61+
"assetVault": "0x7C9e73d4C71dae564d41F78d56439bB4ba87592f",
62+
"stableVault": "0x08FC9Ba2cAc74742177e0afC3dC8Aed6961c24e7",
63+
"assetDeltaWorker": "0x071ac07A287C643b193bc107D29DF4D53BFFAFf7",
64+
"stableDeltaWorker": "0x3F89B3198cB710248136bbB640e88C1618436d20",
65+
"oracle": "0x08EA5fB66EA41f236E3001d2655e43A1E735787F",
66+
"gateway": "0x0256E784f73391797f80a9902c0fD05a718a812a",
67+
"assetVaultPosId": "54752",
68+
"stableVaultPosId": "5630"
69+
"""
70+
except IndexError:
71+
raise IndexError(f"Could not locate Delta Neutral Vault with address {vault_address}")
72+
73+
# Get strategy addresses:
74+
# self.partialCloseMinimizeStrat = {"pancakeswap": "0x8dcEC5e136B6321a50F8567588c2f25738D286C2",
75+
# "biswap": "0x3739d1E01104b019Ff105B3A8F57BC6ed62F18a4"}
76+
self.partialCloseMinimizeStrat = {"pancakeswap": r['SharedStrategies']["Pancakeswap"]["StrategyPartialCloseMinimizeTrading"],
77+
"biswap": r['SharedStrategies']["Biswap"]["StrategyPartialCloseMinimizeTrading"]}
78+
79+
def invest(self) -> web3.contract.ContractFunction:
80+
pass
81+
82+
def withdraw(self, shares: int) -> web3.contract.ContractFunction:
83+
"""
84+
Withdraw the given amount of shares from the Delta Neutral Vault using the 'Minimize Trading' strategy.
85+
86+
:param shares: The amount of shares to withdraw (in shares/vault token)
87+
:return: Uncalled prepared ContractFunction
88+
"""
89+
# All Slippage Controls
90+
minStableTokenAmount = 0
91+
minAssetTokenAmount = 0
92+
93+
# Constant calldata (should not change)
94+
_calldata = encode_abi(
95+
["uint256", "uint256"],
96+
[25, 1]
97+
)
98+
99+
return self.contract.functions.withdraw(shares, minStableTokenAmount, minAssetTokenAmount, _calldata)
100+
101+
def shares(self, user_address: str) -> int:
102+
"""Return the number of shares owned by the given user"""
103+
return self.contract.functions.balanceOf(checksum(user_address)).call()
104+
105+
def positionInfo(self) -> list:
106+
"""
107+
Return total equity and debt value in USD of stable and asset positions
108+
109+
:return:
110+
- stablePositionEquity
111+
- stablePositionDebtValue
112+
- StableLpAmount
113+
- assetPositionEquity
114+
- assetPositionDebtValue
115+
- assetLpAmount
116+
"""
117+
return list(self.contract.functions.positionInfo().call())
118+
119+
def sharesToUSD(self, share_amount: int) -> int:
120+
"""Returns the value in USD for the given amount of vault shares"""
121+
return self.contract.functions.shareToValue(share_amount).call()
122+
123+
def stableTokenAddress(self) -> str:
124+
"""Returns the address for the delta vault stable token"""
125+
return self.contract.functions.stableToken().call()
126+
127+
def assetTokenAddress(self) -> str:
128+
"""Returns the address for the delta vault stable token"""
129+
return self.contract.functions.assetToken().call()
130+
131+
132+
class DeltaNeutralVaultGateway:
133+
def __init__(self, gateway_address: str, w3_provider: Web3 = None):
134+
self.contract = get_bsc_contract_instance(contract_address=gateway_address,
135+
abi_filename="DeltaNeutralVaultGateway.json", w3_provider=w3_provider)
136+
137+
def withdraw(self, shares: int, stableReturnBps: int) -> web3.contract.ContractFunction:
138+
"""
139+
Withdraw shares from the vault using the gateway
140+
141+
:param shares: The amount of share to withdraw from the vault (in vault tokens) (e.g. 10 shares = 10 ** vault token decimals)
142+
:param stableReturnBps: The percentage of tokens returned that should be stable tokens (in Basis Points)
143+
:return: uncalled prepared ContractFunction
144+
"""
145+
146+
# All Slippage Controls
147+
minWithdrawStableTokenAmount = 0 # Minimum stable token shareOwner expect to receive after withdraw.
148+
minWithdrawAssetTokenAmount = 0 # Minimum asset token shareOwner expect to receive after withdraw.
149+
minSwapStableTokenAmount = 0 # Minimum stable token shareOwner expect to receive after swap.
150+
minSwapAssetTokenAmount = 0 # Minimum asset token shareOwner expect to receive after swap.
151+
152+
# Constant calldata (should not change)
153+
_calldata = encode_abi(
154+
["uint256", "uint256"],
155+
[25, 1]
156+
)
157+
158+
# stableReturnBps = 10000 # Percentage stable token shareOwner expect to receive in bps (10000 == 100%)
159+
return self.contract.functions.withdraw(shares, minWithdrawStableTokenAmount, minWithdrawAssetTokenAmount,
160+
minSwapStableTokenAmount, minSwapAssetTokenAmount, _calldata, stableReturnBps)
161+
162+
163+
class AutomatedVaultController:
164+
def __init__(self, w3_provider: Web3 = None):
165+
self.contract = get_bsc_contract_instance(contract_address=AUTOMATED_VAULT_CONTROLLER_ADDRESS,
166+
abi_filename="AutomatedVaultController.json",
167+
w3_provider=w3_provider)
168+
169+
def getUserVaultShares(self, owner_address: str, vault_address: str) -> int:
170+
return self.contract.functions.getUserVaultShares(checksum(owner_address), checksum(vault_address)).call()
171+
172+
def totalCredit(self, user_address: str) -> int:
173+
"""Get the user's total credit in USD"""
174+
return self.contract.functions.totalCredit(checksum(user_address)).call()
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import requests
2+
3+
4+
def get_eth_price() -> float:
5+
"""Returns the realtime price of ETH in USD"""
6+
return float(requests.get("https://api.binance.com/api/v3/avgPrice?symbol=ETHUSDT").json()["price"])

alpaca_finance/positions.py renamed to alpaca_finance/automated_vault/position.py

Lines changed: 109 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
from typing import Optional
1+
from typing import Optional, Union
2+
from math import floor
23

3-
from .util import get_entry_prices, get_web3_provider, get_vault_addresses
4+
from ..util import get_entry_prices, get_web3_provider, get_vault_addresses
45
from ._config import DEFAULT_BSC_RPC_URL
5-
from .vault_contracts import DeltaNeutralVault, DeltaNeutralOracle, AutomatedVaultController, DeltaNeutralVaultGateway
6+
from .receipt import TransactionReceipt, build_receipt
7+
from .contracts import DeltaNeutralVault, DeltaNeutralOracle, AutomatedVaultController, DeltaNeutralVaultGateway
68

9+
from eth_abi import decode_abi
710
import requests
11+
import web3.contract
812
from web3 import Web3
913
from attrdict import AttrDict
1014
from bep20 import BEP20Token
@@ -41,15 +45,40 @@ def __init__(self, position_key: str, owner_wallet_address: str, owner_wallet_ke
4145

4246
""" ------------------ Transactional Methods (Requires private wallet key) ------------------ """
4347

44-
def invest(self):
48+
def do_invest(self) -> TransactionReceipt:
4549
raise NotImplementedError
4650

47-
def withdraw(self, shares: int):
48-
"""Withdraws the specified amount of shares from the automated vault position."""
49-
return self.vault.withdraw(shares)
51+
def do_withdraw(self, shares: int, pct_stable: float = None, strategy: str = "Minimize Trading") -> TransactionReceipt:
52+
"""
53+
Withdraws the specified amount of shares from the automated vault position.
5054
51-
def close_position(self):
52-
raise NotImplementedError
55+
:param shares: The amount of share to withdraw from the vault (in share tokens) (self.shares()[0] = close position)
56+
:param pct_stable: The percentage of stable token returned to the owner (.50 = 50% stable and 50% asset returned)
57+
:param strategy: The strategy to use to withdraw, as shown on the webapp (Minimize Trading, Convert All)
58+
"""
59+
assert self.shares()[0] >= shares, f"Shares owned insufficient to withdraw {shares} " \
60+
f"({self.from_wei(shares, self.bep20_vault_token.decimals())}) shares"
61+
62+
if strategy.lower() == "minimize trading":
63+
return self._execute(self.vault.withdraw(shares))
64+
elif strategy.lower() == "convert all":
65+
assert pct_stable is not None, "Please provide a stable token percentage to determine token swap"
66+
assert 0.0 < pct_stable <= 1.0, "Invalid value for pct_stable parameter, must follow 0.0 < pct_stable <= 1.0"
67+
68+
stable_return_bps = floor(pct_stable * 10000)
69+
70+
return self._execute(self.gateway.withdraw(shares, stableReturnBps=stable_return_bps))
71+
else:
72+
raise ValueError("Invalid strategy - Options are 'Minimize Trading' or 'Convert All'")
73+
74+
def do_close(self, pct_stable: float = None, strategy: str = "Minimize Trading") -> TransactionReceipt:
75+
"""
76+
Withdraws all outstanding shares from the pool and closes position.
77+
78+
:param pct_stable: See self.withdraw()
79+
:param strategy: See self.withdraw()
80+
"""
81+
return self.do_withdraw(shares=self.shares()[0], pct_stable=pct_stable, strategy=strategy)
5382

5483
""" ---------------- Informational Methods (Private wallet key not required) ---------------- """
5584

@@ -171,28 +200,91 @@ def cost_basis(self) -> float: # tuple[float, float]
171200

172201
""" -------------------------------- Utility Methods -------------------------------- """
173202

174-
def sign_and_send_tx(self):
175-
pass
203+
def _execute(self, function_call: web3.contract.ContractFunction) -> TransactionReceipt:
204+
"""
205+
:param function_call: The uncalled and prepared contract method to sign and send
206+
"""
207+
if self.owner_key is None:
208+
raise ValueError("Private key is required to sign transactions")
209+
210+
txn = function_call.buildTransaction({
211+
"from": self.owner_address,
212+
'chainId': 56, # 56: BSC mainnet
213+
# 'gas': 76335152,
214+
'gasPrice': 5000000000,
215+
'nonce': self.w3_provider.eth.get_transaction_count(self.owner_address),
216+
})
217+
print(txn)
218+
219+
signed_txn = self.w3_provider.eth.account.sign_transaction(
220+
txn, private_key=self.owner_key
221+
)
222+
tx_hash = self.w3_provider.eth.send_raw_transaction(signed_txn.rawTransaction)
223+
224+
receipt = dict(self.w3_provider.eth.wait_for_transaction_receipt(tx_hash))
225+
226+
try:
227+
return build_receipt(receipt)
228+
except Exception as exc:
229+
print(f"COULD NOT BUILD TRANSACTION RECEIPT OBJECT - {exc}")
230+
# Catch case to prevent receipt from being lost if TransactionReceipt object somehow can't be built
231+
return receipt
232+
233+
@staticmethod
234+
def from_wei(amt: int, decimals: int) -> int:
235+
return amt * (10 ** decimals)
176236

177-
def decode_transaction_data(self, transaction_address: Optional):
237+
def _decode_withdraw_transaction(self, transaction_address: Optional):
178238
"""
179239
Returns the transaction data used to invoke the smart contract function for the underlying contract
180240
First fetches the transaction data for the HomoraBank.execute() function, then gets the transaction data
181241
for the underlying smart contract
182-
:param w3_provider: The Web3.HTTPProvider object for interacting with the network
242+
183243
:param transaction_address: The transaction address (binary or str)
184244
:return: (
185245
decoded bank function (ContractFunction, dict),
186246
decoded spell function (ContractFunction, dict)
187247
)
188248
"""
189249
transaction = self.w3_provider.eth.get_transaction(transaction_address)
250+
print(transaction)
251+
252+
try:
253+
decoded_bank_transaction = self.vault.contract.decode_function_input(transaction.input)
254+
except:
255+
decoded_bank_transaction = self.gateway.contract.decode_function_input(transaction.input)
256+
257+
decoded_calldata = decode_abi(
258+
['uint256', 'uint256'],
259+
decoded_bank_transaction[1]['_data']
260+
)
261+
262+
return decoded_bank_transaction, decoded_calldata
263+
264+
def _decode_deposit_transaction(self, transaction_address: Optional):
265+
"""
266+
Returns the transaction data used to invoke the smart contract function for the underlying contract
267+
First fetches the transaction data for the HomoraBank.execute() function, then gets the transaction data
268+
for the underlying smart contract
190269
191-
decoded_bank_transaction = self.vault.contract.decode_function_input(transaction.input)
270+
:param transaction_address: The transaction address (binary or str)
271+
:return: (
272+
decoded bank function (ContractFunction, dict),
273+
decoded spell function (ContractFunction, dict)
274+
)
275+
"""
276+
transaction = self.w3_provider.eth.get_transaction(transaction_address)
277+
print(transaction)
192278

193-
# return decoded_bank_transaction
279+
try:
280+
decoded_bank_transaction = self.vault.contract.decode_function_input(transaction.input)
281+
except:
282+
decoded_bank_transaction = self.gateway.contract.decode_function_input(transaction.input)
194283

195-
encoded_contract_data = decoded_bank_transaction[1]['_data']
284+
decoded_calldata = decode_abi(
285+
["uint256"],
286+
decoded_bank_transaction[1]['_data']
287+
)
196288

197-
return decoded_bank_transaction, self.gateway.contract.decode_function_input(encoded_contract_data)
289+
return decoded_bank_transaction, decoded_calldata
198290

0 commit comments

Comments
 (0)