diff --git a/README.md b/README.md index 33ac8e23..72ea3133 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,10 @@ you are modifying weaviate schema. cd docker docker-compose up ``` + +## Steps to add new widget command +- Update `widgets.txt` with the widget command details +- Bump up the widget index version in `INDEX_NAME` https://github.com/yieldprotocol/chatweb3-backend/blob/dev/index/widgets.py#L9  +- Similarly, bump up the index version in `index_name`  https://github.com/yieldprotocol/chatweb3-backend/blob/dev/config.py#L6 +- Run this Python command to update our Weaviate Vector DB with the new widget `python3 -c "from index import widgets; widgets.backfill()"` +- Add the widget's handler function in `replace_match()` https://github.com/yieldprotocol/chatweb3-backend/blob/dev/tools/index_widget.py#L189 diff --git a/knowledge_base/widgets.txt b/knowledge_base/widgets.txt index bd84e714..8bd4743b 100644 --- a/knowledge_base/widgets.txt +++ b/knowledge_base/widgets.txt @@ -216,6 +216,16 @@ Required Parameters: -{token}: token to withdraw -{amount}: quantity to withdraw --- +Widget magic command: <|savings-dai-deposit({amount})|> +Description of widget: This widget is used when the user wants to deposit DAI into SavingsDAI to get SavingsDAI, that can be later redeemed for a profit. +Required Parameters: +-{amount}: amount of DAI to deposit +--- +Widget magic command: <|savings-dai-redeem({amount})|> +Description of widget: This widget is used when the user wants to redeem their SavingsDAI to get DAI, realizing a profit. +Required Parameters: +-{amount}: amount of SavingsDAI to redeem +--- Widget magic command: <|display-yield-farm({project},{network}, {token}, {amount})|> Description of widget: This widget is only to be used for the Compound project to allow the user to yield farm by putting tokens or depositing tokens of a certain amount into the Compound project Required Parameters: diff --git a/requirements.txt b/requirements.txt index 2ad4ca60..057ebf99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ SQLAlchemy_Utils==0.39.0 psycopg2-binary==2.9.5 qcore==1.8.0 PyYAML==6.0 -web3==6.2.0 +web3==6.3.0 gpt_index==0.4.23 transformers==4.26.1 pyWalletConnect==1.3.3 diff --git a/tools/index_widget.py b/tools/index_widget.py index e0f99ced..9ba858f9 100644 --- a/tools/index_widget.py +++ b/tools/index_widget.py @@ -220,11 +220,11 @@ def replace_match(m: re.Match) -> Union[str | Generator]: return str(fetch_yields(*params)) elif command == aave.AaveSupplyContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='supply')) - elif command == aave.AaveBorrowUIWorkflow.WORKFLOW_TYPE: + elif command == aave.AaveBorrowContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='borrow')) - elif command == 'aave-repay': + elif command == aave.AaveRepayContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='repay')) - elif command == 'aave-withdraw': + elif command == aave.AaveWithdrawContractWorkflow.WORKFLOW_TYPE: return str(exec_aave_operation(*params, operation='withdraw')) elif command == 'ens-from-address': return str(ens_from_address(*params)) diff --git a/ui_workflows/aave/__init__.py b/ui_workflows/aave/__init__.py index 3e8b17ee..e25ea201 100644 --- a/ui_workflows/aave/__init__.py +++ b/ui_workflows/aave/__init__.py @@ -1,2 +1 @@ -from .ui_integration import AaveSupplyUIWorkflow, AaveBorrowUIWorkflow -from .contract_abi_integration import AaveSupplyContractWorkflow \ No newline at end of file +from .contract_abi_integration import * \ No newline at end of file diff --git a/ui_workflows/aave/abis/aave_atoken.abi.json b/ui_workflows/aave/abis/aave_atoken.abi.json new file mode 100644 index 00000000..79bd31f9 --- /dev/null +++ b/ui_workflows/aave/abis/aave_atoken.abi.json @@ -0,0 +1,568 @@ +[ + { + "inputs": [ + { "internalType": "contract IPool", "name": "pool", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "BalanceTransfer", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "treasury", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "incentivesController", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "aTokenDecimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "string", + "name": "aTokenName", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "aTokenSymbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "ATOKEN_REVISION", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EIP712_REVISION", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "PERMIT_TYPEHASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "POOL", + "outputs": [ + { "internalType": "contract IPool", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "RESERVE_TREASURY_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNDERLYING_ASSET_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { + "internalType": "address", + "name": "receiverOfUnderlying", + "type": "address" + }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "burn", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { + "internalType": "uint256", + "name": "subtractedValue", + "type": "uint256" + } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getIncentivesController", + "outputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getPreviousIndex", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getScaledUserBalanceAndSupply", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "handleRepayment", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "addedValue", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "initializingPool", + "type": "address" + }, + { "internalType": "address", "name": "treasury", "type": "address" }, + { + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "internalType": "contract IAaveIncentivesController", + "name": "incentivesController", + "type": "address" + }, + { "internalType": "uint8", "name": "aTokenDecimals", "type": "uint8" }, + { "internalType": "string", "name": "aTokenName", "type": "string" }, + { "internalType": "string", "name": "aTokenSymbol", "type": "string" }, + { "internalType": "bytes", "name": "params", "type": "bytes" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "caller", "type": "address" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "mint", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "mintToTreasury", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" }, + { "internalType": "address", "name": "spender", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "token", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "rescueTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "scaledBalanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "scaledTotalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "controller", + "type": "address" + } + ], + "name": "setIncentivesController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "recipient", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "address", "name": "to", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" } + ], + "name": "transferOnLiquidation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "target", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "transferUnderlyingTo", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/ui_workflows/aave/abis/aave_variable_debt_token.abi.json b/ui_workflows/aave/abis/aave_variable_debt_token.abi.json new file mode 100644 index 00000000..2907474e --- /dev/null +++ b/ui_workflows/aave/abis/aave_variable_debt_token.abi.json @@ -0,0 +1,515 @@ +[ + { + "inputs": [ + { "internalType": "contract IPool", "name": "pool", "type": "address" } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "fromUser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "toUser", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "asset", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "BorrowAllowanceDelegated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Burn", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "pool", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "incentivesController", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint8", + "name": "debtTokenDecimals", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "string", + "name": "debtTokenName", + "type": "string" + }, + { + "indexed": false, + "internalType": "string", + "name": "debtTokenSymbol", + "type": "string" + }, + { + "indexed": false, + "internalType": "bytes", + "name": "params", + "type": "bytes" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "caller", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "onBehalfOf", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "balanceIncrease", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "index", + "type": "uint256" + } + ], + "name": "Mint", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DEBT_TOKEN_REVISION", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DELEGATION_WITH_SIG_TYPEHASH", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EIP712_REVISION", + "outputs": [{ "internalType": "bytes", "name": "", "type": "bytes" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "POOL", + "outputs": [ + { "internalType": "contract IPool", "name": "", "type": "address" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UNDERLYING_ASSET_ADDRESS", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" } + ], + "name": "allowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "approve", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "delegatee", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" } + ], + "name": "approveDelegation", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "balanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "fromUser", "type": "address" }, + { "internalType": "address", "name": "toUser", "type": "address" } + ], + "name": "borrowAllowance", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "from", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "burn", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [{ "internalType": "uint8", "name": "", "type": "uint8" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "decreaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "delegator", "type": "address" }, + { "internalType": "address", "name": "delegatee", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "deadline", "type": "uint256" }, + { "internalType": "uint8", "name": "v", "type": "uint8" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "delegationWithSig", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "getIncentivesController", + "outputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getPreviousIndex", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "getScaledUserBalanceAndSupply", + "outputs": [ + { "internalType": "uint256", "name": "", "type": "uint256" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "increaseAllowance", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPool", + "name": "initializingPool", + "type": "address" + }, + { + "internalType": "address", + "name": "underlyingAsset", + "type": "address" + }, + { + "internalType": "contract IAaveIncentivesController", + "name": "incentivesController", + "type": "address" + }, + { "internalType": "uint8", "name": "debtTokenDecimals", "type": "uint8" }, + { "internalType": "string", "name": "debtTokenName", "type": "string" }, + { "internalType": "string", "name": "debtTokenSymbol", "type": "string" }, + { "internalType": "bytes", "name": "params", "type": "bytes" } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" }, + { "internalType": "address", "name": "onBehalfOf", "type": "address" }, + { "internalType": "uint256", "name": "amount", "type": "uint256" }, + { "internalType": "uint256", "name": "index", "type": "uint256" } + ], + "name": "mint", + "outputs": [ + { "internalType": "bool", "name": "", "type": "bool" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "owner", "type": "address" } + ], + "name": "nonces", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "user", "type": "address" } + ], + "name": "scaledBalanceOf", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "scaledTotalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IAaveIncentivesController", + "name": "controller", + "type": "address" + } + ], + "name": "setIncentivesController", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [{ "internalType": "string", "name": "", "type": "string" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "address", "name": "", "type": "address" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "transferFrom", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "nonpayable", + "type": "function" + } +] diff --git a/ui_workflows/aave/common.py b/ui_workflows/aave/common.py index 8e315f36..c3ece696 100644 --- a/ui_workflows/aave/common.py +++ b/ui_workflows/aave/common.py @@ -1,3 +1,4 @@ +import os import re import json from typing import Optional, Union, Literal @@ -7,11 +8,18 @@ from web3 import Web3 from utils import load_contract_abi -from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS +from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError, ContractStepProcessingResult, BaseContractWorkflow +def load_aave_contract_error_codes(): + with open(os.path.join(os.path.dirname(__file__), "./contract_abi_integration/aave_contract_error_codes.json")) as f: + return json.load(f) + +AAVE_CONTRACT_ERROR_CODES = load_aave_contract_error_codes() FIVE_SECONDS = 5000 AAVE_POOL_V3_PROXY_ADDRESS = Web3.to_checksum_address("0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2") AAVE_WRAPPED_TOKEN_GATEWAY = Web3.to_checksum_address("0xd322a49006fc828f9b5b37ab215f99b4e5cab19c") +AAVE_VARIABLE_DEBT_TOKEN_ADDRESS = Web3.to_checksum_address("0xea51d7853eefb32b6ee06b1c12e6dcca88be0ffe") +AAVE_ATOKEN_ADDRESS = Web3.to_checksum_address("0x4d5f47fa6a74757f35c14fd3a6ef8e3c9bc514e8") AAVE_SUPPORTED_TOKENS = [ "ETH", @@ -35,6 +43,18 @@ def get_aave_wrapped_token_gateway_contract(): web3_provider = context.get_web3_provider() return web3_provider.eth.contract(address=AAVE_WRAPPED_TOKEN_GATEWAY, abi=load_contract_abi(__file__, "./abis/aave_wrapped_token_gateway.abi.json")) +def get_aave_variable_debt_token_contract(): + web3_provider = context.get_web3_provider() + return web3_provider.eth.contract(address=AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, abi=load_contract_abi(__file__, "./abis/aave_variable_debt_token.abi.json")) + +def get_aave_atoken_contract(): + web3_provider = context.get_web3_provider() + return web3_provider.eth.contract(address=AAVE_ATOKEN_ADDRESS, abi=load_contract_abi(__file__, "./abis/aave_atoken.abi.json")) + +def common_aave_validation(token): + if (token not in AAVE_SUPPORTED_TOKENS): + raise WorkflowValidationError(f"Token {token} not supported by Aave") + class AaveMixin: def _goto_page_and_open_walletconnect(self, page): """Go to page and open WalletConnect modal""" @@ -90,3 +110,16 @@ def aave_revoke_eth_approval(): # https://docs.aave.com/developers/tokens/debttoken#approvedelegation aave_set_eth_approval(0) +def aave_parse_contract_error(code: str): + # Ref: https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/libraries/helpers/Errors.sol + print(f"Aave error code: {code}") + if code not in AAVE_CONTRACT_ERROR_CODES: + return f"Unexpected Aave error. Check with support" + return AAVE_CONTRACT_ERROR_CODES[code] + +def aave_check_for_error_and_compute_result(contract_workflow: BaseContractWorkflow, tx): + error_message = contract_workflow._simulate_tx_for_error_check(tx) + if error_message: + return ContractStepProcessingResult(status="error", error_msg=aave_parse_contract_error(error_message)) + else: + return ContractStepProcessingResult(status="success", tx=tx) diff --git a/ui_workflows/aave/contract_abi_integration/__init__.py b/ui_workflows/aave/contract_abi_integration/__init__.py index 40202826..4e5a264f 100644 --- a/ui_workflows/aave/contract_abi_integration/__init__.py +++ b/ui_workflows/aave/contract_abi_integration/__init__.py @@ -1 +1,4 @@ -from .aave_supply_contract_workflow import AaveSupplyContractWorkflow \ No newline at end of file +from .aave_supply_contract_workflow import AaveSupplyContractWorkflow +from .aave_borrow_contract_workflow import AaveBorrowContractWorkflow +from .aave_repay_contract_workflow import AaveRepayContractWorkflow +from .aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py new file mode 100644 index 00000000..cadd566e --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_borrow_contract_workflow.py @@ -0,0 +1,89 @@ +from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict + +import web3 + +from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult +from database.models import ( + db_session, MultiStepWorkflow, WorkflowStep, WorkflowStepStatus, WorkflowStepUserActionType, ChatMessage, ChatSession, SystemConfig +) +from ..common import AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, get_aave_wrapped_token_gateway_contract, get_aave_variable_debt_token_contract, get_aave_pool_v3_address_contract, common_aave_validation, aave_parse_contract_error, aave_check_for_error_and_compute_result + +class AaveBorrowContractWorkflow(BaseMultiStepContractWorkflow): + """ + NOTE: Refer to the docstring in ../ui_integration/aave_borrow_ui_workflow.py (AaveBorrowUIWorkflow) to get more info on the various scenarios to handle for Aave borrow + """ + + WORKFLOW_TYPE = 'aave-borrow' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + self.token = workflow_params["token"] + self.amount = workflow_params["amount"] + + if self.token == "ETH": + initiate_ETH_approval_step = RunnableStep("initiate_ETH_approval", WorkflowStepUserActionType.tx, f"Approve borrow of {self.amount} ETH on Aave", self.initiate_ETH_approval) + confirm_ETH_borrow_step = RunnableStep("confirm_ETH_borrow", WorkflowStepUserActionType.tx, f"Confirm borrow of {self.amount} ETH on Aave", self.confirm_ETH_borrow) + steps = [initiate_ETH_approval_step, confirm_ETH_borrow_step] + + final_step_type = "confirm_ETH_borrow" + else: + confirm_ERC20_borrow_step = RunnableStep("confirm_ERC20_borrow", WorkflowStepUserActionType.tx, f"Confirm borrow of {self.amount} {self.token} on Aave", self.confirm_ERC20_borrow) + steps = [confirm_ERC20_borrow_step] + final_step_type = "confirm_ERC20_borrow" + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + common_aave_validation(self.token) + + def initiate_ETH_approval(self): + """Initiate approval for ETH token""" + + from_address = self.wallet_address + to_address = AAVE_WRAPPED_TOKEN_GATEWAY + + borrow_allowance = get_aave_variable_debt_token_contract().functions.borrowAllowance(from_address, to_address).call() + + if parse_token_amount(self.wallet_chain_id, self.token, self.amount) <= borrow_allowance: + return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ETH_borrow") + + delegatee = AAVE_WRAPPED_TOKEN_GATEWAY + amount = int(web3.constants.MAX_INT, 16) + encoded_data = get_aave_variable_debt_token_contract().encodeABI(fn_name='approveDelegation', args=[delegatee, amount]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_VARIABLE_DEBT_TOKEN_ADDRESS, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) + + def confirm_ETH_borrow(self): + pool_address = AAVE_POOL_V3_PROXY_ADDRESS + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 # Variable + referral_code = 0 + encoded_data = get_aave_wrapped_token_gateway_contract().encodeABI(fn_name='borrowETH', args=[pool_address, amount, interest_rate_mode, referral_code]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_WRAPPED_TOKEN_GATEWAY, + 'data': encoded_data, + } + + return aave_check_for_error_and_compute_result(self, tx) + + def confirm_ERC20_borrow(self): + asset_address = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 # Variable + referral_code = 0 + on_behalf_of = self.wallet_address + + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='borrow', args=[asset_address, amount, interest_rate_mode, referral_code, on_behalf_of]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_POOL_V3_PROXY_ADDRESS, + 'data': encoded_data, + } + + return aave_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json b/ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json new file mode 100644 index 00000000..c0a2fae3 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_contract_error_codes.json @@ -0,0 +1,92 @@ +{ + "1": "The caller of the function is not a pool admin", + "2": "The caller of the function is not an emergency admin", + "3": "The caller of the function is not a pool or emergency admin", + "4": "The caller of the function is not a risk or pool admin", + "5": "The caller of the function is not an asset listing or pool admin", + "6": "The caller of the function is not a bridge", + "7": "Pool addresses provider is not registered", + "8": "Invalid id for the pool addresses provider", + "9": "Address is not a contract", + "10": "The caller of the function is not the pool configurator", + "11": "The caller of the function is not an AToken", + "12": "The address of the pool addresses provider is invalid", + "13": "Invalid return value of the flashloan executor function", + "14": "Reserve has already been added to reserve list", + "15": "Maximum amount of reserves in the pool reached", + "16": "Zero eMode category is reserved for volatile heterogeneous assets", + "17": "Invalid eMode category assignment to asset", + "18": "The liquidity of the reserve needs to be 0", + "19": "Invalid flashloan premium", + "20": "Invalid risk parameters for the reserve", + "21": "Invalid risk parameters for the eMode category", + "22": "Invalid bridge protocol fee", + "23": "The caller of this function must be a pool", + "24": "Invalid amount to mint", + "25": "Invalid amount to burn", + "26": "Amount must be greater than 0", + "27": "Action requires an active reserve", + "28": "Action cannot be performed because the reserve is frozen", + "29": "Action cannot be performed because the reserve is paused", + "30": "Borrowing is not enabled", + "31": "Stable borrowing is not enabled", + "32": "User cannot withdraw more than the available balance", + "33": "Invalid interest rate mode selected", + "34": "The collateral balance is 0", + "35": "Health factor is lesser than the liquidation threshold", + "36": "There is not enough collateral to cover a new borrow", + "37": "Collateral is (mostly) the same currency that is being borrowed", + "38": "The requested amount is greater than the max loan size in stable rate mode", + "39": "For repayment of a specific type of debt, the user needs to have debt that type", + "40": "To repay on behalf of a user an explicit amount to repay is needed", + "41": "User does not have outstanding stable rate debt on this reserve", + "42": "User does not have outstanding variable rate debt on this reserve", + "43": "The underlying balance needs to be greater than 0", + "44": "Interest rate rebalance conditions were not met", + "45": "Health factor is not below the threshold", + "46": "The collateral chosen cannot be liquidated", + "47": "User did not borrow the specified currency", + "49": "Inconsistent flashloan parameters", + "50": "Borrow cap is exceeded", + "51": "Supply cap is exceeded", + "52": "Unbacked mint cap is exceeded", + "53": "Debt ceiling is exceeded", + "54": "Claimable rights over underlying not zero (aToken supply or accruedToTreasury)", + "55": "Stable debt supply is not zero", + "56": "Variable debt supply is not zero", + "57": "Ltv validation failed", + "58": "Inconsistent eMode category", + "59": "Price oracle sentinel validation failed", + "60": "Asset is not borrowable in isolation mode", + "61": "Reserve has already been initialized", + "62": "User is in isolation mode", + "63": "Invalid ltv parameter for the reserve", + "64": "Invalid liquidity threshold parameter for the reserve", + "65": "Invalid liquidity bonus parameter for the reserve", + "66": "Invalid decimals parameter of the underlying asset of the reserve", + "67": "Invalid reserve factor parameter for the reserve", + "68": "Invalid borrow cap for the reserve", + "69": "Invalid supply cap for the reserve", + "70": "Invalid liquidation protocol fee for the reserve", + "71": "Invalid eMode category for the reserve", + "72": "Invalid unbacked mint cap for the reserve", + "73": "Invalid debt ceiling for the reserve", + "74": "Invalid reserve index", + "75": "ACL admin cannot be set to the zero address", + "76": "Array parameters that should be equal length are not", + "77": "Zero address not valid", + "78": "Invalid expiration", + "79": "Invalid signature", + "80": "Operation not supported", + "81": "Debt ceiling is not zero", + "82": "Asset is not listed", + "83": "Invalid optimal usage ratio", + "84": "Invalid optimal stable to total debt ratio", + "85": "The underlying asset cannot be rescued", + "86": "Reserve has already been added to reserve list", + "87": "The token implementation pool address and the pool address provided by the initializing pool do not match", + "88": "Stable borrowing is enabled", + "89": "User is trying to borrow multiple assets including a siloed one", + "90": "The total debt of the reserve needs to be 0", + "91": "FlashLoaning for this asset is disabled" +} diff --git a/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py new file mode 100644 index 00000000..f551ec46 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_repay_contract_workflow.py @@ -0,0 +1,66 @@ +from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict + +import web3 + +from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult +from database.models import ( + MultiStepWorkflow, WorkflowStepUserActionType +) +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_wrapped_token_gateway_contract, get_aave_pool_v3_address_contract, common_aave_validation, aave_check_for_error_and_compute_result + +class AaveRepayContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'aave-repay' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + self.token = workflow_params["token"] + self.amount = workflow_params["amount"] + + if self.token == "ETH": + confirm_ETH_repay_step = RunnableStep("confirm_ETH_repay", WorkflowStepUserActionType.tx, f"Confirm repay of {self.amount} ETH on Aave", self.confirm_ETH_repay) + steps = [confirm_ETH_repay_step] + + final_step_type = confirm_ETH_repay_step.type + else: + initiate_ERC20_approval_step = RunnableStep("initiate_ERC20_approve", WorkflowStepUserActionType.tx, f"Approve repay of {self.amount} {self.token} on Aave", self.initiate_ERC20_approval) + confirm_ERC20_repay_step = RunnableStep("confirm_ERC20_repay", WorkflowStepUserActionType.tx, f"Confirm repay of {self.amount} {self.token} on Aave", self.confirm_ERC20_repay) + steps = [initiate_ERC20_approval_step, confirm_ERC20_repay_step] + final_step_type = confirm_ERC20_repay_step.type + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + common_aave_validation(self.token) + + def confirm_ETH_repay(self): + pool_address = AAVE_POOL_V3_PROXY_ADDRESS + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 + on_behalf_of = self.wallet_address + encoded_data = get_aave_wrapped_token_gateway_contract().encodeABI(fn_name='repayETH', args=[pool_address, amount, interest_rate_mode, on_behalf_of]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_WRAPPED_TOKEN_GATEWAY, + 'data': encoded_data, + 'value': hexify_token_amount(self.wallet_chain_id, self.token, self.amount) + } + + return aave_check_for_error_and_compute_result(self, tx) + + def initiate_ERC20_approval(self): + return self._initiate_ERC20_approval(AAVE_POOL_V3_PROXY_ADDRESS, self.token, self.amount, 'confirm_ERC20_repay') + + def confirm_ERC20_repay(self): + asset_address = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + interest_rate_mode = 2 + on_behalf_of = self.wallet_address + + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='repay', args=[asset_address, amount, interest_rate_mode, on_behalf_of]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_POOL_V3_PROXY_ADDRESS, + 'data': encoded_data, + } + + return aave_check_for_error_and_compute_result(self, tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py index 4671445a..adcb6e0a 100644 --- a/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py +++ b/ui_workflows/aave/contract_abi_integration/aave_supply_contract_workflow.py @@ -11,7 +11,7 @@ db_session, MultiStepWorkflow, WorkflowStepUserActionType ) from ...base import BaseMultiStepContractWorkflow, WorkflowStepClientPayload, RunnableStep, WorkflowValidationError, ContractStepProcessingResult -from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_pool_v3_address_contract, get_aave_wrapped_token_gateway_contract +from ..common import AAVE_SUPPORTED_TOKENS, AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_pool_v3_address_contract, get_aave_wrapped_token_gateway_contract, aave_check_for_error_and_compute_result class AaveSupplyContractWorkflow(BaseMultiStepContractWorkflow): """ @@ -60,24 +60,11 @@ def confirm_ETH_supply_step(self) -> ContractStepProcessingResult: 'value': hexify_token_amount(self.wallet_chain_id, self.token, self.amount), } - return ContractStepProcessingResult(status="success", tx=tx) + return aave_check_for_error_and_compute_result(self, tx) def initiate_ERC20_approval_step(self): """Initiate approval of ERC20 token to be spent by Aave""" - - if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address, AAVE_POOL_V3_PROXY_ADDRESS, self.amount)): - return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ERC20_supply") - - spender = AAVE_POOL_V3_PROXY_ADDRESS - value = parse_token_amount(self.wallet_chain_id, self.token, self.amount) - encoded_data = generate_erc20_approve_encoded_data(self.web3_provider, self.wallet_chain_id, self.token, spender, value) - tx = { - 'from': self.wallet_address, - 'to': get_token_address(self.wallet_chain_id, self.token), - 'data': encoded_data, - } - return ContractStepProcessingResult(status="success", tx=tx) - + return self._initiate_ERC20_approval(AAVE_POOL_V3_PROXY_ADDRESS, self.token, self.amount, 'confirm_ERC20_supply') def confirm_ERC20_supply_step(self, extra_params=None) -> ContractStepProcessingResult: """Confirm supply of ERC20 token""" @@ -93,4 +80,4 @@ def confirm_ERC20_supply_step(self, extra_params=None) -> ContractStepProcessing 'data': encoded_data, } - return ContractStepProcessingResult(status="success", tx=tx) + return aave_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py new file mode 100644 index 00000000..e30ee5fb --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/aave_withdraw_contract_workflow.py @@ -0,0 +1,79 @@ +from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict + +import web3 + +from utils import get_token_balance, parse_token_amount, hexify_token_amount, estimate_gas, get_token_address, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance +from ...base import RunnableStep, WorkflowStepClientPayload, BaseMultiStepContractWorkflow, WorkflowValidationError, ContractStepProcessingResult +from database.models import ( + MultiStepWorkflow, WorkflowStepUserActionType +) +from ..common import AAVE_POOL_V3_PROXY_ADDRESS, AAVE_WRAPPED_TOKEN_GATEWAY, get_aave_wrapped_token_gateway_contract, get_aave_pool_v3_address_contract, common_aave_validation, get_aave_atoken_contract, aave_check_for_error_and_compute_result + +class AaveWithdrawContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'aave-withdraw' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + self.token = workflow_params["token"] + self.amount = workflow_params["amount"] + + if self.token == "ETH": + initiate_ETH_approval_step = RunnableStep("initiate_ETH_approval", WorkflowStepUserActionType.tx, f"Approve withdraw of {self.amount} ETH on Aave", self.initiate_ETH_approval) + confirm_ETH_withdraw_step = RunnableStep("confirm_ETH_withdraw", WorkflowStepUserActionType.tx, f"Confirm withdraw of {self.amount} ETH on Aave", self.confirm_ETH_withdraw) + steps = [initiate_ETH_approval_step, confirm_ETH_withdraw_step] + + final_step_type = confirm_ETH_withdraw_step.type + else: + confirm_ERC20_withdraw_step = RunnableStep("confirm_ERC20_withdraw", WorkflowStepUserActionType.tx, f"Confirm withdraw of {self.amount} {self.token} on Aave", self.confirm_ERC20_withdraw) + steps = [confirm_ERC20_withdraw_step] + final_step_type = confirm_ERC20_withdraw_step.type + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + common_aave_validation(self.token) + + def initiate_ETH_approval(self): + owner = self.wallet_address + spender = AAVE_WRAPPED_TOKEN_GATEWAY + + allowance = get_aave_atoken_contract().functions.allowance(owner, spender).call() + + if parse_token_amount(self.wallet_chain_id, self.token, self.amount) <= allowance: + return ContractStepProcessingResult(status="replace", replace_with_step_type="confirm_ETH_withdraw") + + amount = int(web3.constants.MAX_INT, 16) + + encoded_data = get_aave_atoken_contract().encodeABI(fn_name='approve', args=[spender, amount]) + tx = { + 'from': self.wallet_address, + 'to': get_aave_atoken_contract().address, + 'data': encoded_data, + } + + return ContractStepProcessingResult(status="success", tx=tx) + + def confirm_ETH_withdraw(self): + pool_address = AAVE_POOL_V3_PROXY_ADDRESS + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + to_address = self.wallet_address + encoded_data = get_aave_wrapped_token_gateway_contract().encodeABI(fn_name='withdrawETH', args=[pool_address, amount, to_address]) + tx = { + 'from': self.wallet_address, + 'to': get_aave_wrapped_token_gateway_contract().address, + 'data': encoded_data, + } + + return aave_check_for_error_and_compute_result(self, tx) + + def confirm_ERC20_withdraw(self): + asset_address = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + to_address = self.wallet_address + encoded_data = get_aave_pool_v3_address_contract().encodeABI(fn_name='withdraw', args=[asset_address, amount, to_address]) + tx = { + 'from': self.wallet_address, + 'to': AAVE_POOL_V3_PROXY_ADDRESS, + 'data': encoded_data, + } + + return aave_check_for_error_and_compute_result(self, tx) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py new file mode 100644 index 00000000..b1e7b2f8 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_erc20.py @@ -0,0 +1,49 @@ +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_erc20" +def test_contract_aave_borrow_erc20(setup_fork): + token = "DAI" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Confirm borrow of 0.1 DAI on Aave" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert dai_balance_end == dai_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py new file mode 100644 index 00000000..ea496cdd --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_approval.py @@ -0,0 +1,60 @@ + +""" +Test for borrowing ETH on Aave +""" +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_eth_no_approval" +def test_contract_aave_borrow_eth_no_approval(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + # Pre-approve ETH borrow to set the test environment for borrow + aave_set_eth_approval(1*10**18) + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + assert multistep_result.description == "Confirm borrow of 0.1 ETH on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert eth_balance_end == eth_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py new file mode 100644 index 00000000..42a848a7 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_no_collateral.py @@ -0,0 +1,26 @@ + +""" +Test for borrowing ETH on Aave + +""" +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_eth_no_collateral" +def test_contract_aave_borrow_eth_no_collateral(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-approve ETH borrow to set the test environment for borrow + aave_set_eth_approval(1*10**18) + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # TODO: This will fail as Tenderly is not able to simulate the tx on the latest block for a fork, investigate why + assert multistep_result.status == 'error' + assert multistep_result.error_msg == 'The collateral balance is 0' + + + \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py new file mode 100644 index 00000000..6f1aab17 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_borrow_eth_with_approval.py @@ -0,0 +1,63 @@ + +""" +Test for borrowing ETH on Aave +""" +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_borrow_eth_with_approval" +def test_contract_aave_borrow_eth_with_approval(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + assert multistep_result.description == "Approve borrow of 0.1 ETH on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + assert multistep_result.description == "Confirm borrow of 0.1 ETH on Aave" + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert eth_balance_end == eth_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py new file mode 100644 index 00000000..00aa2066 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_erc20.py @@ -0,0 +1,67 @@ + +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_repay_contract_workflow import AaveRepayContractWorkflow +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_repay_erc20" +def test_contract_aave_repay_erc20(setup_fork): + token = "USDT" + amount = 10 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + # First borrow in order to test repay + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + usdt_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Approve repay of 10 USDT on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + assert multistep_result.description == "Confirm repay of 10 USDT on Aave" + + assert multistep_result.is_final_step == True + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + usdt_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert usdt_balance_end == usdt_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py new file mode 100644 index 00000000..e3a86cfd --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_repay_eth.py @@ -0,0 +1,60 @@ + +""" +Test for Repaying ETH on Aave +""" +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_repay_contract_workflow import AaveRepayContractWorkflow +from ..aave_borrow_contract_workflow import AaveBorrowContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_repay_eth" +def test_contract_aave_repay_eth(setup_fork): + token = "ETH" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + # Pre-approve ETH borrow to set the test environment for borrow + aave_set_eth_approval(1*10**18) + + # First borrow in order to test repay + multistep_result = AaveBorrowContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Confirm repay of 0.1 ETH on Aave" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = AaveRepayContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert eth_balance_end == eth_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py index 961be490..acac7e90 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_no_approval.py @@ -1,9 +1,8 @@ """ Test for supplying an ERC20 token on Aave without approval step as it is already pre-approved """ -from logging import basicConfig, INFO -from dataclasses import dataclass, asdict - +import context +from utils import get_token_balance, parse_token_amount from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID from ...common import aave_set_usdc_allowance from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow @@ -14,6 +13,8 @@ def test_contract_aave_supply_erc20_no_approval(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + usdc_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Set allowance for pre-approval aave_set_usdc_allowance(int(amount * 10 ** 6)) @@ -43,4 +44,6 @@ def test_contract_aave_supply_erc20_no_approval(setup_fork): # Final state of workflow should be terminated assert multistep_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + usdc_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert usdc_balance_end == usdc_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py index c4e9e1fe..75989e59 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_erc20_with_approval.py @@ -1,10 +1,8 @@ - -import re -import time -import json """ Test for supplying an ERC20 token on Aave with approval step """ +import context +from utils import get_token_balance, parse_token_amount import os import requests from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable @@ -16,12 +14,14 @@ from ...common import aave_revoke_usdc_approval -# Invoke this with python3 -m pytest -s -k "test_aave_supply_erc20_with_approval" +# Invoke this with python3 -m pytest -s -k "test_contract_aave_supply_erc20_with_approval" def test_contract_aave_supply_erc20_with_approval(setup_fork): token = "USDC" amount = 0.1 workflow_params = {"token": token, "amount": amount} + usdc_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + # Make sure to revoke any USDC pre-approval to ensure Aave UI is in the correct state to show approval flow aave_revoke_usdc_approval() @@ -69,4 +69,5 @@ def test_contract_aave_supply_erc20_with_approval(setup_fork): # Final state of workflow should be terminated assert multi_step_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + usdc_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert usdc_balance_end == usdc_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py index b512bae0..1d63a920 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_eth.py @@ -2,9 +2,9 @@ """ Test for supplying a ETH on Aave """ -import re - -from ....base import setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID from ...contract_abi_integration import AaveSupplyContractWorkflow # Invoke this with python3 -m pytest -s -k "test_contract_aave_supply_eth" @@ -13,6 +13,8 @@ def test_contract_aave_supply_eth(setup_fork): amount = 0.1 workflow_params = {"token": token, "amount": amount} + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + multi_step_result = AaveSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() assert multi_step_result.description == "Confirm supply of 0.1 ETH on Aave" @@ -41,4 +43,5 @@ def test_contract_aave_supply_eth(setup_fork): # Final state of workflow should be terminated assert multi_step_result.status == "terminated" - # TODO - For thorough validation, ensure to assert the actual amount used in tx matches expectation by fetching decoded tx data from Tenderly \ No newline at end of file + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert eth_balance_end == eth_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py index f1377ec1..a3ef593b 100644 --- a/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_supply_extreme_amount.py @@ -2,8 +2,6 @@ """ Test for supplying an ETH amount greater than account balance on Aave """ -import re - import context from ....base import setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py new file mode 100644 index 00000000..460066b0 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_erc20.py @@ -0,0 +1,52 @@ + +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_set_usdc_allowance +from ..aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow +from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_withdraw_erc20" +def test_contract_aave_withdraw_erc20(setup_fork): + token = "USDC" + amount = 100 + workflow_params = {"token": token, "amount": amount} + + # Pre-deposit USDC in order to test withdraw + aave_set_usdc_allowance(parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount)) + multistep_result = AaveSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + usdc_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Confirm withdraw of 100 USDC on Aave" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + usdc_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert usdc_balance_end == usdc_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py new file mode 100644 index 00000000..7eac5fc9 --- /dev/null +++ b/ui_workflows/aave/contract_abi_integration/tests/test_aave_withdraw_eth.py @@ -0,0 +1,62 @@ + +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import aave_supply_eth_for_borrow_test, aave_set_eth_approval +from ..aave_withdraw_contract_workflow import AaveWithdrawContractWorkflow +from ..aave_supply_contract_workflow import AaveSupplyContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_aave_withdraw_eth" +def test_contract_aave_withdraw_eth(setup_fork): + token = "ETH" + amount = 0.5 + workflow_params = {"token": token, "amount": amount} + + # Pre-supply ETH to Aave to setup the test environment for borrow + aave_supply_eth_for_borrow_test() + + eth_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == "Approve withdraw of 0.5 ETH on Aave" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + assert multistep_result.description == "Confirm withdraw of 0.5 ETH on Aave" + assert multistep_result.is_final_step == True + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multistep_result = AaveWithdrawContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + eth_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert eth_balance_end == eth_balance_start + parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) diff --git a/ui_workflows/base/__init__.py b/ui_workflows/base/__init__.py index 955df871..6f32d5b2 100644 --- a/ui_workflows/base/__init__.py +++ b/ui_workflows/base/__init__.py @@ -2,7 +2,7 @@ from .common import ( WorkflowStepClientPayload, RunnableStep, StepProcessingResult, MultiStepResult, Result, WorkflowValidationError, ContractStepProcessingResult, - tenderly_simulate_tx, compute_abi_abspath, setup_mock_db_objects, process_result_and_simulate_tx, + tenderly_simulate_tx_on_fork, compute_abi_abspath, setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, revoke_erc20_approval, set_erc20_allowance, advance_fork_time_secs, advance_fork_blocks, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID, USDC_ADDRESS diff --git a/ui_workflows/base/base_contract_workflow.py b/ui_workflows/base/base_contract_workflow.py index 158cede3..8a1a15ca 100644 --- a/ui_workflows/base/base_contract_workflow.py +++ b/ui_workflows/base/base_contract_workflow.py @@ -1,11 +1,14 @@ from abc import ABC, abstractmethod from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict from dataclasses import dataclass -import json +import requests -import context +from web3 import Web3, exceptions -from web3 import Web3 +import context +import env +from utils import TENDERLY_API_KEY +from .common import get_latest_simulation_id_on_fork class BaseContractWorkflow(ABC): """Grandparent base class for contract workflows. Do not directly use this class, use either BaseSingleStepContractWorkflow or BaseMultiStepContractWorkflow class""" @@ -18,11 +21,6 @@ def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: s self.workflow_params = workflow_params self.web3_provider = context.get_web3_provider() - def run(self) -> Any: - """Main function to call to run the workflow.""" - ret = self._run() - return ret - @abstractmethod def _run(self) -> Any: """Implement the contract interaction logic here.""" @@ -32,3 +30,40 @@ def _run(self) -> Any: def _general_workflow_validation(self): """Override this method to perform any common validation checks for all steps in the workflow before running them""" + def run(self) -> Any: + """Main function to call to run the workflow.""" + ret = self._run() + return ret + + def _simulate_tx_for_error_check(self, tx: Dict) -> Optional[str]: + if env.is_prod(): + tenderly_simulate_api_url = f"https://api.tenderly.co/api/v1/account/Yield/project/chatweb3/simulate" + else: + tenderly_simulate_api_url = f"https://api.tenderly.co/api/v1/account/Yield/project/chatweb3/fork/{context.get_web3_fork_id()}/simulate" + + payload = { + "save": False, + "save_if_fails": False, + "simulation_type": "quick", + "network_id": self.wallet_chain_id, + "from": tx['from'], + "to": tx['to'], + "input": tx['data'], + "value": tx.get('value', 0), + } + + if not env.is_prod(): + payload["root"] = get_latest_simulation_id_on_fork(context.get_web3_tenderly_fork_url()) + + res = requests.post(tenderly_simulate_api_url, json=payload, headers={'X-Access-Key': TENDERLY_API_KEY}) + + if res.status_code == 200: + simulation_data = res.json() + transaction = simulation_data['transaction'] + error_message = transaction.get('error_message') + return error_message + else: + return None + + + diff --git a/ui_workflows/base/base_multi_step_contract_workflow.py b/ui_workflows/base/base_multi_step_contract_workflow.py index 3ae6e451..e3d84ce8 100644 --- a/ui_workflows/base/base_multi_step_contract_workflow.py +++ b/ui_workflows/base/base_multi_step_contract_workflow.py @@ -2,7 +2,7 @@ import uuid from typing import Any, Callable, Dict, List, Optional, Union, Literal, TypedDict -from utils import estimate_gas +from utils import parse_token_amount, generate_erc20_approve_encoded_data, has_sufficient_erc20_allowance, get_token_address from database.models import db_session, WorkflowStep, WorkflowStepStatus, MultiStepWorkflow from .common import WorkflowStepClientPayload, RunnableStep, ContractStepProcessingResult, MultiStepResult, WorkflowValidationError, compute_abi_abspath @@ -20,4 +20,17 @@ def run(self) -> MultiStepResult: return BaseMultiStepMixin.run(self) def _run(self) -> MultiStepResult: - return BaseMultiStepMixin._run(self) \ No newline at end of file + return BaseMultiStepMixin._run(self) + + def _initiate_ERC20_approval(self, spender, token, amount, replace_with_step_type): + if (has_sufficient_erc20_allowance(self.web3_provider, self.wallet_chain_id, token, self.wallet_address, spender, amount)): + return ContractStepProcessingResult(status="replace", replace_with_step_type=replace_with_step_type) + + value = parse_token_amount(self.wallet_chain_id, token, amount) + encoded_data = generate_erc20_approve_encoded_data(self.web3_provider, self.wallet_chain_id, token, spender, value) + tx = { + 'from': self.wallet_address, + 'to': get_token_address(self.wallet_chain_id, token), + 'data': encoded_data, + } + return ContractStepProcessingResult(status="success", tx=tx) \ No newline at end of file diff --git a/ui_workflows/base/base_multi_step_mixin.py b/ui_workflows/base/base_multi_step_mixin.py index 0248fffd..dd40c7cc 100644 --- a/ui_workflows/base/base_multi_step_mixin.py +++ b/ui_workflows/base/base_multi_step_mixin.py @@ -122,14 +122,16 @@ def _run(self, *args, **kwargs) -> MultiStepResult: else: # For contract ABI approach tx = processing_result.tx - try: - tx['gas'] = estimate_gas(tx) - except Exception: - # If gas usage estimation fails, use fallback arbitary gas limit to attempt tx - tx['gas'] = FALLBACK_GAS_LIMIT - if tx and "value" not in tx: - tx['value'] = "0x0" + if tx: + try: + tx['gas'] = estimate_gas(tx) + except Exception: + # If gas usage estimation fails, use fallback arbitary gas limit to attempt tx + tx['gas'] = FALLBACK_GAS_LIMIT + + if "value" not in tx: + tx['value'] = "0x0" computed_user_description = processing_result.override_user_description or self.curr_step_description @@ -218,7 +220,10 @@ def _handle_step_replace(self, *args, **kwargs) -> Union[ContractStepProcessingR if self.workflow_approach == WorkflowApproach.UI: return runnable_step.function(page, browser_context, replacement_extra_params) else: - return runnable_step.function(replacement_extra_params) + if replacement_extra_params: + return runnable_step.function(replacement_extra_params) + else: + return runnable_step.function() def _find_runnable_step_index_by_step_type(self, step_type) -> int: return [i for i,s in enumerate(self.runnable_steps) if s.type == step_type][0] diff --git a/ui_workflows/base/common.py b/ui_workflows/base/common.py index 64c2bb84..f28a7885 100644 --- a/ui_workflows/base/common.py +++ b/ui_workflows/base/common.py @@ -8,6 +8,7 @@ import requests import context +from utils import TENDERLY_API_KEY from database.models import db_session, ChatMessage, ChatSession, SystemConfig from database.models import (MultiStepWorkflow) @@ -72,7 +73,7 @@ class WorkflowValidationError(Exception): class WorkflowFailed(Exception): pass -def tenderly_simulate_tx(wallet_address: str, tx: Dict) -> str: +def tenderly_simulate_tx_on_fork(wallet_address: str, tx: Dict) -> str: payload = { "jsonrpc": "2.0", "method": "eth_sendTransaction", @@ -98,15 +99,39 @@ def tenderly_simulate_tx(wallet_address: str, tx: Dict) -> str: fork_web3 = Web3(Web3.HTTPProvider(fork_rpc_url)) receipt = fork_web3.eth.wait_for_transaction_receipt(tx_hash) - print("receipt:", receipt) + tenderly_simulation_id = get_latest_simulation_id_on_fork(fork_rpc_url) - print("Tenderly TxHash:", tx_hash) + tenderly_dashboard_link = f"https://dashboard.tenderly.co/Yield/chatweb3/fork/{fork_id}/simulation/{tenderly_simulation_id}" + print("Tenderly simulation dashboard link:", tenderly_dashboard_link) + + # Tx Error handling if receipt['status'] == 0: - raise Exception(f"Transaction failed, tx_hash: {tx_hash}, check fork for more details - https://dashboard.tenderly.co/Yield/chatweb3/fork/{fork_id}") + tenderly_simulation_api = f"https://api.tenderly.co/api/v1/account/Yield/project/chatweb3/fork/{fork_id}/simulation/{tenderly_simulation_id}" + res = requests.get(tenderly_simulation_api, headers={'X-Access-Key': TENDERLY_API_KEY}) + error_message = 'n/a' + if res.status_code == 200: + simulation_data = res.json() + error_message = simulation_data['transaction']['error_message'] + raise Exception(f"Transaction failed, error_message: {error_message}, check fork for more details - {tenderly_dashboard_link}") return tx_hash +def get_latest_simulation_on_fork(fork_rpc_url): + get_latest_tx_payload = { + "jsonrpc": "2.0", + "method": "evm_getLatest", + "params": [] + } + + res = requests.post(fork_rpc_url, json=get_latest_tx_payload) + res.raise_for_status() + + return res.json() + +def get_latest_simulation_id_on_fork(fork_rpc_url): + return get_latest_simulation_on_fork(fork_rpc_url)['result'] + def advance_fork_blocks(num_blocks) -> None: fork_rpc_url = context.get_web3_tenderly_fork_url() @@ -169,7 +194,7 @@ def compute_abi_abspath(wf_file_path, abi_relative_path): def process_result_and_simulate_tx(wallet_address, result: Union[Result, MultiStepResult]) -> Optional[str]: if result.status == "success": - tx_hash = tenderly_simulate_tx(wallet_address, result.tx) + tx_hash = tenderly_simulate_tx_on_fork(wallet_address, result.tx) print("Workflow successful") return tx_hash elif result.status == "terminated": diff --git a/ui_workflows/conftest.py b/ui_workflows/conftest.py index 72960edb..3fbd08f7 100644 --- a/ui_workflows/conftest.py +++ b/ui_workflows/conftest.py @@ -5,16 +5,17 @@ import pytest import context -from utils import create_fork, remove_fork +from utils import create_fork, remove_fork, TEST_TENDERLY_FORK_ID @pytest.fixture(scope="module") def setup_fork(): # Before test - fork_id = create_fork() + fork_id = TEST_TENDERLY_FORK_ID or create_fork() with context.with_request_context(None, None, wallet_chain_id=None, fork_id=fork_id): # Return to test function yield {"fork_id": fork_id} # After test - remove_fork(fork_id) \ No newline at end of file + if not TEST_TENDERLY_FORK_ID: + remove_fork(fork_id) \ No newline at end of file diff --git a/ui_workflows/multistep_handler.py b/ui_workflows/multistep_handler.py index d6f9ca27..87e2c416 100644 --- a/ui_workflows/multistep_handler.py +++ b/ui_workflows/multistep_handler.py @@ -52,8 +52,12 @@ def process_multistep_workflow(payload: MessagePayload, send_message: Callable): result = register_ens_domain(workflow_params['domain'], user_chat_message_id, workflow_db_obj, step) elif workflow_type == aave.AaveSupplyContractWorkflow.WORKFLOW_TYPE: result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "supply", user_chat_message_id, workflow_db_obj, step) - elif workflow_type == aave.AaveBorrowUIWorkflow.WORKFLOW_TYPE: + elif workflow_type == aave.AaveBorrowContractWorkflow.WORKFLOW_TYPE: result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "borrow", user_chat_message_id, workflow_db_obj, step) + elif workflow_type == aave.AaveRepayContractWorkflow.WORKFLOW_TYPE: + result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "repay", user_chat_message_id, workflow_db_obj, step) + elif workflow_type == aave.AaveWithdrawContractWorkflow.WORKFLOW_TYPE: + result = exec_aave_operation(workflow_params['token'], workflow_params['amount'], "withdraw", user_chat_message_id, workflow_db_obj, step) else: raise Exception(f'Workflow type {workflow_type} not supported.') @@ -93,7 +97,11 @@ def exec_aave_operation(token: str, amount: str, operation: Literal["supply", "b if operation == 'supply': wf = aave.AaveSupplyContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) elif operation == 'borrow': - wf = aave.AaveBorrowUIWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) + wf = aave.AaveBorrowContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) + elif operation == 'repay': + wf = aave.AaveRepayContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) + elif operation == 'withdraw': + wf = aave.AaveWithdrawContractWorkflow(wallet_chain_id, wallet_address, user_chat_message_id, workflow_params, workflow, wf_step_client_payload) else: raise Exception(f'Operation {operation} not supported.') diff --git a/ui_workflows/savings_dai/__init__.py b/ui_workflows/savings_dai/__init__.py new file mode 100644 index 00000000..e25ea201 --- /dev/null +++ b/ui_workflows/savings_dai/__init__.py @@ -0,0 +1 @@ +from .contract_abi_integration import * \ No newline at end of file diff --git a/ui_workflows/savings_dai/abis/savings_dai.abi.json b/ui_workflows/savings_dai/abis/savings_dai.abi.json new file mode 100644 index 00000000..e4da8a03 --- /dev/null +++ b/ui_workflows/savings_dai/abis/savings_dai.abi.json @@ -0,0 +1 @@ +[{"inputs":[{"internalType":"address","name":"_daiJoin","type":"address"},{"internalType":"address","name":"_pot","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"address","name":"spender","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Deposit","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"from","type":"address"},{"indexed":true,"internalType":"address","name":"to","type":"address"},{"indexed":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"receiver","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"uint256","name":"assets","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"shares","type":"uint256"}],"name":"Withdraw","type":"event"},{"inputs":[],"name":"DOMAIN_SEPARATOR","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PERMIT_TYPEHASH","outputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"}],"name":"allowance","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"approve","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"asset","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"balanceOf","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"convertToAssets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"convertToShares","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"dai","outputs":[{"internalType":"contract DaiLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"daiJoin","outputs":[{"internalType":"contract DaiJoinLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"subtractedValue","type":"uint256"}],"name":"decreaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"deploymentChainId","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"}],"name":"deposit","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"addedValue","type":"uint256"}],"name":"increaseAllowance","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"maxDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"maxMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"pure","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"maxRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"}],"name":"maxWithdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"}],"name":"mint","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"name","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"nonces","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"bytes","name":"signature","type":"bytes"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"address","name":"spender","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"uint256","name":"deadline","type":"uint256"},{"internalType":"uint8","name":"v","type":"uint8"},{"internalType":"bytes32","name":"r","type":"bytes32"},{"internalType":"bytes32","name":"s","type":"bytes32"}],"name":"permit","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"pot","outputs":[{"internalType":"contract PotLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"previewDeposit","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"previewMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"name":"previewRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"name":"previewWithdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"shares","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"redeem","outputs":[{"internalType":"uint256","name":"assets","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalAssets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalSupply","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transfer","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"from","type":"address"},{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"}],"name":"transferFrom","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"vat","outputs":[{"internalType":"contract VatLike","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"version","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"assets","type":"uint256"},{"internalType":"address","name":"receiver","type":"address"},{"internalType":"address","name":"owner","type":"address"}],"name":"withdraw","outputs":[{"internalType":"uint256","name":"shares","type":"uint256"}],"stateMutability":"nonpayable","type":"function"}] \ No newline at end of file diff --git a/ui_workflows/savings_dai/common.py b/ui_workflows/savings_dai/common.py new file mode 100644 index 00000000..7a683617 --- /dev/null +++ b/ui_workflows/savings_dai/common.py @@ -0,0 +1,24 @@ +import os +import re +import json +from typing import Optional, Union, Literal + +import context +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError +from web3 import Web3 + +from utils import load_contract_abi +from ..base import StepProcessingResult, revoke_erc20_approval, set_erc20_allowance, TEST_WALLET_ADDRESS, USDC_ADDRESS, WorkflowValidationError, ContractStepProcessingResult, BaseContractWorkflow + +SAVINGS_DAI_ADDRESS = Web3.to_checksum_address("0x83F20F44975D03b1b09e64809B757c47f942BEeA") + +def get_savings_dai_address_contract(): + web3_provider = context.get_web3_provider() + return web3_provider.eth.contract(address=SAVINGS_DAI_ADDRESS, abi=load_contract_abi(__file__, "./abis/savings_dai.abi.json")) + +def savings_dai_check_for_error_and_compute_result(contract_workflow: BaseContractWorkflow, tx): + error_message = contract_workflow._simulate_tx_for_error_check(tx) + if error_message: + return ContractStepProcessingResult(status="error", error_msg=error_message) + else: + return ContractStepProcessingResult(status="success", tx=tx) diff --git a/ui_workflows/savings_dai/contract_abi_integration/__init__.py b/ui_workflows/savings_dai/contract_abi_integration/__init__.py new file mode 100644 index 00000000..78eb9365 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/__init__.py @@ -0,0 +1,2 @@ +from .savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow +from .savings_dai_redeem_contract_workflow import SavingsDaiRedeemContractWorkflow \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py new file mode 100644 index 00000000..afff6a72 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_deposit_contract_workflow.py @@ -0,0 +1,55 @@ +import re +from logging import basicConfig, INFO +from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable +from dataclasses import dataclass, asdict + +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError + +import env +from utils import get_token_balance, estimate_gas, parse_token_amount, hexify_token_amount, has_sufficient_erc20_allowance, generate_erc20_approve_encoded_data, get_token_address +from database.models import ( + db_session, MultiStepWorkflow, WorkflowStepUserActionType +) +from ...base import BaseMultiStepContractWorkflow, WorkflowStepClientPayload, RunnableStep, WorkflowValidationError, ContractStepProcessingResult +from ..common import SAVINGS_DAI_ADDRESS, get_savings_dai_address_contract, savings_dai_check_for_error_and_compute_result + +class SavingsDaiDepositContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'savings-dai-deposit' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + serlf.token = "DAI" + self.amount = workflow_params["amount"] + + # The only token that can be deposited is DAI, you have to handle approval before final confirmation + initiate_erc20_approval_step = RunnableStep("initiate_ERC20_approval", WorkflowStepUserActionType.tx, f"Approve deposit of {self.amount} {self.token} on SavingsDAI", self.initiate_erc20_approval_step) + confirm_erc4626_deposit_step = RunnableStep("confirm_ERC4626_deposit", WorkflowStepUserActionType.tx, f"Confirm deposit of {self.amount} {self.token} on SavingsDAI", self.confirm_erc4626_deposit_step) + steps = [initiate_erc20_approval_step, confirm_erc4626_deposit_step] + + final_step_type = "confirm_erc4626_deposit" + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + if (get_token_balance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address) < parse_token_amount(self.wallet_chain_id, self.token, self.amount)): + raise WorkflowValidationError(f"Insufficient {self.token} balance in wallet") + + + def initiate_ERC20_approval_step(self): + """Initiate approval of ERC20 token to be taken by SavingsDAI""" + return self._initiate_ERC20_approval(SAVINGS_DAI_ADDRESS, self.token, self.amount, 'confirm_ERC20_approval') + + def confirm_ERC20_supply_step(self, extra_params=None) -> ContractStepProcessingResult: + """Confirm deposit of DAI""" + + asset = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + on_behalf_of = self.wallet_address + referral_code = 0 + encoded_data = get_savings_dai_address_contract().encodeABI(fn_name='deposit', args=[amount, self.wallet_address]) + tx = { + 'from': self.wallet_address, + 'to': SAVINGS_DAI_ADDRESS, + 'data': encoded_data, + } + + return savings_dai_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py new file mode 100644 index 00000000..a681ac77 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/savings_dai_redeem_contract_workflow.py @@ -0,0 +1,55 @@ +import re +from logging import basicConfig, INFO +from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable +from dataclasses import dataclass, asdict + +from playwright.sync_api import TimeoutError as PlaywrightTimeoutError + +import env +from utils import get_token_balance, estimate_gas, parse_token_amount, hexify_token_amount, has_sufficient_erc20_allowance, generate_erc20_approve_encoded_data, get_token_address +from database.models import ( + db_session, MultiStepWorkflow, WorkflowStepUserActionType +) +from ...base import BaseMultiStepContractWorkflow, WorkflowStepClientPayload, RunnableStep, WorkflowValidationError, ContractStepProcessingResult +from ..common import SAVINGS_DAI_ADDRESS, get_savings_dai_address_contract, savings_dai_check_for_error_and_compute_result + +class SavingsDaiRedeemContractWorkflow(BaseMultiStepContractWorkflow): + WORKFLOW_TYPE = 'savings-dai-redeem' + + def __init__(self, wallet_chain_id: int, wallet_address: str, chat_message_id: str, workflow_params: Dict, multistep_workflow: Optional[MultiStepWorkflow] = None, curr_step_client_payload: Optional[WorkflowStepClientPayload] = None) -> None: + serlf.token = "SavingsDAI" + self.amount = workflow_params["amount"] + + # The only token that can be redeemed is SavingsDAI, you have to handle approval before final confirmation + initiate_erc20_approval_step = RunnableStep("initiate_ERC20_approval", WorkflowStepUserActionType.tx, f"Approve redemption of {self.amount} {self.token} on SavingsDAI", self.initiate_erc20_approval_step) + confirm_erc4626_redeem_step = RunnableStep("confirm_ERC4626_redeem", WorkflowStepUserActionType.tx, f"Confirm redemption of {self.amount} {self.token} on SavingsDAI", self.confirm_erc20_deposit_step) + steps = [initiate_erc20_approval_step, confirm_erc4626_redeem_step] + + final_step_type = "confirm_erc4626_redeem" + + super().__init__(wallet_chain_id, wallet_address, chat_message_id, self.WORKFLOW_TYPE, multistep_workflow, workflow_params, curr_step_client_payload, steps, final_step_type) + + def _general_workflow_validation(self): + if (get_token_balance(self.web3_provider, self.wallet_chain_id, self.token, self.wallet_address) < parse_token_amount(self.wallet_chain_id, self.token, self.amount)): + raise WorkflowValidationError(f"Insufficient {self.token} balance in wallet") + + + def initiate_ERC20_approval_step(self): + """Initiate approval of ERC20 token to be taken by SavingsDAI""" + return self._initiate_ERC20_approval(SAVINGS_DAI_ADDRESS, self.token, self.amount, 'confirm_ERC20_approval') + + def confirm_ERC4626_redeem_step(self, extra_params=None) -> ContractStepProcessingResult: + """Confirm redemption of SavingsDAI""" + + asset = get_token_address(self.wallet_chain_id, self.token) + amount = parse_token_amount(self.wallet_chain_id, self.token, self.amount) + on_behalf_of = self.wallet_address + referral_code = 0 + encoded_data = get_savings_dai_address_contract().encodeABI(fn_name='redeem', args=[amount, self.wallet_address]) + tx = { + 'from': self.wallet_address, + 'to': SAVINGS_DAI_ADDRESS, + 'data': encoded_data, + } + + return savings_dai_check_for_error_and_compute_result(self, tx) diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/__init__.py b/ui_workflows/savings_dai/contract_abi_integration/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py new file mode 100644 index 00000000..3ed1fcf9 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_extreme_amount.py @@ -0,0 +1,22 @@ + +""" +Test for depositing a DAI amount greater than account balance on SavingsDai +""" +import context +from ....base import setup_mock_db_objects, process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID +from ..savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_deposit_extreme_amount" +def test_contract_savings_dai_deposit_extreme_amount(setup_fork): + web3_provider = context.get_web3_provider() + current_dai_balance = web3_provider.dai.get_balance(TEST_WALLET_ADDRESS) + test_extreme_dai_amount = current_dai_balance + 10*10**18 # Add 10 dai to the current available balance + + token = "DAI" + amount = test_extreme_dai_amount + workflow_params = {"token": token, "amount": amount} + + multistep_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.error_msg == "Insufficient dai balance in wallet" \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py new file mode 100644 index 00000000..6e110a50 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_no_approval.py @@ -0,0 +1,49 @@ +""" +Test for depositing DAI on SavingsDAI without approval step as it is already pre-approved +""" +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID +from ...common import savings_dai_set_dai_allowance +from ..savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_deposit_no_approval" +def test_contract_savings_dai_deposit_no_approval(setup_fork): + token = "DAI" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Set allowance for pre-approval + savings_dai_set_dai_allowance(int(amount * 10 ** 18)) + + # Confirm deposit of DAI with no approval step + multistep_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + assert multistep_result.description == "Confirm deposit of 0.1 DAI on SavingsDAI" + + assert multistep_result.is_final_step + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + multistep_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + assert dai_balance_end == dai_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py new file mode 100644 index 00000000..74867123 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_deposit_with_approval.py @@ -0,0 +1,73 @@ +""" +Test for depositing Dai on SavingsDai with approval step +""" +import context +from utils import get_token_balance, parse_token_amount +import os +import requests +from typing import Any, Dict, List, Optional, Union, Literal, TypedDict, Callable +from dataclasses import dataclass, asdict + +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, MOCK_CHAT_MESSAGE_ID, TEST_WALLET_ADDRESS, TEST_WALLET_CHAIN_ID + +from ..savings_dai_deposit_contract_workflow import SavingsDaiDepositContractWorkflow + +from ...common import savings_dai_revoke_dai_approval + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_deposit_with_approval" +def test_contract_savings_dai_deposit_with_approval(setup_fork): + token = "DAI" + amount = 0.1 + workflow_params = {"token": token, "amount": amount} + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Make sure to revoke any DAI pre-approval to ensure SavingsDai UI is in the correct state to show approval flow + savings_dai_revoke_dai_approval() + + # Step 1 - Approve deposit of DAI + multi_step_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multi_step_result.description == "Approve deposit of 0.1 DAI on SavingsDai" + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multi_step_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multi_step_result.step_id, + "type": multi_step_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multi_step_result.workflow_id + + multi_step_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Step 2 - Process Step 1 response from FE and continue to Step 2 which is to confirm deposit of DAI + multi_step_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multi_step_workflow, curr_step_client_payload).run() + + assert multi_step_result.description == "Confirm deposit of 0.1 DAI on SavingsDai" + + assert multi_step_result.is_final_step + + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multi_step_result) + + curr_step_client_payload = { + "id": multi_step_result.step_id, + "type": multi_step_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + multi_step_result = SavingsDaiDepositContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multi_step_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multi_step_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + assert dai_balance_end == dai_balance_start - parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount) \ No newline at end of file diff --git a/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py new file mode 100644 index 00000000..bb6a0b45 --- /dev/null +++ b/ui_workflows/savings_dai/contract_abi_integration/tests/test_savings_dai_redeem.py @@ -0,0 +1,57 @@ + +import context +from utils import get_token_balance, parse_token_amount +from ....base import process_result_and_simulate_tx, fetch_multi_step_workflow_from_db, TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID +from ...common import savings_dai_set_dai_allowance +from ..savings_dai_withdraw_contract_workflow import SavingsDaiRedeemContractWorkflow +from ..savings_dai_supply_contract_workflow import SavingsDaiSupplyContractWorkflow + +# Invoke this with python3 -m pytest -s -k "test_contract_savings_dai_withdraw_erc20" +def test_contract_savings_dai_withdraw_erc20(setup_fork): + token = "DAI" + amount = 100 + workflow_params = {"token": token, "amount": amount} + + # Pre-deposit DAI in order to test withdraw + savings_dai_set_dai_allowance(parse_token_amount(TEST_WALLET_CHAIN_ID, token, amount)) + multistep_result = SavingsDaiSupplyContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + dai_balance_start = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + + # Redeem all SavingsDAI obtained + token = "SavingsDAI" + amount = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, token, TEST_WALLET_ADDRESS) + workflow_params = {"token": token, "amount": amount} + + multistep_result = SavingsDaiRedeemContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params).run() + + # Assert what the user will see on the UI + assert multistep_result.description == f"Confirm redemption of {amount} SavingsDAI on SavingsDai" + + assert multistep_result.is_final_step == True + + # Simulating user signing/confirming a tx on the UI with their wallet + tx_hash = process_result_and_simulate_tx(TEST_WALLET_ADDRESS, multistep_result) + + # Mocking FE response payload to backend + curr_step_client_payload = { + "id": multistep_result.step_id, + "type": multistep_result.step_type, + "status": 'success', + "statusMessage": "TX successfully sent", + "userActionData": tx_hash + } + + workflow_id = multistep_result.workflow_id + + multistep_workflow = fetch_multi_step_workflow_from_db(workflow_id) + + # Process FE response payload + multistep_result = SavingsDaiRedeemContractWorkflow(TEST_WALLET_CHAIN_ID, TEST_WALLET_ADDRESS, MOCK_CHAT_MESSAGE_ID, workflow_params, multistep_workflow, curr_step_client_payload).run() + + # Final state of workflow should be terminated + assert multistep_result.status == "terminated" + + dai_balance_end = get_token_balance(context.get_web3_provider(), TEST_WALLET_CHAIN_ID, "DAI", TEST_WALLET_ADDRESS) + assert dai_balance_end > dai_balance_start diff --git a/utils/constants.py b/utils/constants.py index 8e2be514..1c65f2e9 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -8,4 +8,6 @@ TENDERLY_FORK_BASE_URL = "https://rpc.tenderly.co/fork" DEFAULT_MAINNET_FORK_ID = "08f78838-4799-47a8-88fb-1f169fa99f57" -TENDERLY_FORK_URL = f"{TENDERLY_FORK_BASE_URL}/{DEFAULT_MAINNET_FORK_ID}" \ No newline at end of file +TENDERLY_FORK_URL = f"{TENDERLY_FORK_BASE_URL}/{DEFAULT_MAINNET_FORK_ID}" + +TEST_TENDERLY_FORK_ID = os.getenv('TEST_TENDERLY_FORK_ID', "") \ No newline at end of file diff --git a/utils/crypto_token.py b/utils/crypto_token.py index e058309e..495f93aa 100644 --- a/utils/crypto_token.py +++ b/utils/crypto_token.py @@ -46,6 +46,10 @@ "address": "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", "decimals": 8 }, + "SavingsDAI": { + "address": "0x83F20F44975D03b1b09e64809B757c47f942BEeA", + "decimals": 18 + }, } def parse_token_amount(chain_id: int, token: str, amount: str) -> int: diff --git a/utils/tenderly.py b/utils/tenderly.py index d6708a87..389affb7 100644 --- a/utils/tenderly.py +++ b/utils/tenderly.py @@ -8,6 +8,8 @@ def create_fork(): if not TENDERLY_API_KEY: raise Exception("TENDERLY_API_KEY required to run simulations in isolated forks") + print("Creating fork...") + payload = { "network_id": "1", "block_number": 17297193 # https://etherscan.io/block/17297193 @@ -18,4 +20,5 @@ def create_fork(): def remove_fork(fork_id: str): res = requests.delete(f"{TENDERLY_PROJECT_URL}/{fork_id}", headers={"X-Access-Key": TENDERLY_API_KEY}) - res.raise_for_status() \ No newline at end of file + res.raise_for_status() + print(f"Fork deleted: {fork_id}") \ No newline at end of file