const { config, data, onRequestClose, onActionSuccess, chainId } = props; if (!data) { return; } const MIN_ETH_GAS_FEE = 0.001; const ROUND_DOWN = 0; function isValid(a) { if (!a) return false; if (isNaN(Number(a))) return false; if (a === "") return false; return true; } const { symbol, balance, marketReferencePriceInUsd, supplyAPY, usageAsCollateralEnabled, decimals, token, name: tokenName, } = data; const WithdrawContainer = styled.div` display: flex; flex-direction: column; gap: 20px; `; const TokenTexture = styled.div` font-size: 20px; font-weight: bold; color: white; `; const TokenWrapper = styled.div` display: flex; img { margin-right: 4px; } `; const GrayTexture = styled.div` font-size: 12px; font-weight: 500; color: #7c7c86; `; const PurpleTexture = styled.div` font-size: 14px; font-weight: 500; color: #8a8db9; `; const GreenTexture = styled.div` font-size: 14px; font-weight: bold; color: #2cffa7; `; const RedTexture = styled.div` font-size: 14px; font-weight: bold; color: red; `; const WhiteTexture = styled.div` font-size: 14px; font-weight: bold; color: white; `; const TransactionOverviewContainer = styled.div` display: flex; flex-direction: column; gap: 20px; `; const Input = styled.input` background: transparent; border: none; outline: none; font-size: 20px; font-weight: bold; color: white; flex: 1; width: 160px; &[type="number"]::-webkit-outer-spin-button, &[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } &[type="number"] { -moz-appearance: textfield; } `; const Max = styled.span` color: #8247e5; cursor: pointer; `; State.init({ amount: "", amountInUSD: "0.00", loading: false, }); function getNonce(tokenAddress, userAddress) { const token = new ethers.Contract( tokenAddress, config.erc20Abi.body, Ethers.provider().getSigner() ); return token.nonces(userAddress).then((nonce) => nonce.toNumber()); } /** * * @param {string} user user address * @param {string} reserve AAVE reserve address (token to supply) * @param {string} tokenName token name * @param {string} amount token amount in full decimals * @param {number} deadline unix timestamp in SECONDS * @returns raw signature string will could be used in supplyWithPermit */ function signERC20Approval(user, reserve, tokenName, amount, deadline) { return getNonce(reserve, user).then((nonce) => { const typeData = { types: { EIP712Domain: [ { name: "name", type: "string" }, { name: "version", type: "string" }, { name: "chainId", type: "uint256" }, { name: "verifyingContract", type: "address" }, ], Permit: [ { name: "owner", type: "address" }, { name: "spender", type: "address" }, { name: "value", type: "uint256" }, { name: "nonce", type: "uint256" }, { name: "deadline", type: "uint256" }, ], }, primaryType: "Permit", domain: { name: tokenName, version: "1", chainId, verifyingContract: reserve, }, message: { owner: user, spender: config.aavePoolV3Address, value: amount, nonce, deadline, }, }; const dataToSign = JSON.stringify(typeData); return Ethers.provider().send("eth_signTypedData_v4", [user, dataToSign]); }); } /** * * @param {string} user user address * @param {string} reserve AAVE reserve address (token to supply) * @param {string} amount token amount in full decimals * @param {number} deadline unix timestamp in SECONDS * @param {string} rawSig signature from signERC20Approval * @returns txn object */ function supplyWithPermit(user, reserve, amount, deadline, rawSig) { const sig = ethers.utils.splitSignature(rawSig); const pool = new ethers.Contract( config.aavePoolV3Address, config.aavePoolV3ABI.body, Ethers.provider().getSigner() ); return pool[ "supplyWithPermit(address,uint256,address,uint16,uint256,uint8,bytes32,bytes32)" ](reserve, amount, user, 0, deadline, sig.v, sig.r, sig.s); } function depositETH(amount) { State.update({ loading: true, }); return Ethers.provider() .getSigner() .getAddress() .then((address) => { const wrappedTokenGateway = new ethers.Contract( config.wrappedTokenGatewayV3Address, config.wrappedTokenGatewayV3ABI.body, Ethers.provider().getSigner() ); return wrappedTokenGateway.depositETH( config.aavePoolV3Address, address, 0, { value: amount, } ); }) .then((tx) => { tx.wait().then((res) => { const { status } = res; if (status === 1) { onActionSuccess({ msg: `You supplied ${Big(amount) .div(Big(10).pow(decimals)) .toFixed(8)} ${symbol}`, callback: () => { onRequestClose(); State.update({ loading: false, }); }, }); console.log("tx succeeded", res); } else { console.log("tx failed", res); State.update({ loading: false, }); } }); }) .catch(() => State.update({ loading: false })); } function depositErc20(amount) { State.update({ loading: true, }); const deadline = Math.floor(Date.now() / 1000 + 3600); // after an hour Ethers.provider() .getSigner() .getAddress() .then((userAddress) => { signERC20Approval(userAddress, token, tokenName, amount, deadline) .then((rawSig) => { return supplyWithPermit(userAddress, token, amount, deadline, rawSig); }) .then((tx) => { tx.wait().then((res) => { const { status } = res; if (status === 1) { onActionSuccess({ msg: `You supplied ${Big(amount) .div(Big(10).pow(decimals)) .toFixed(8)} ${symbol}`, callback: () => { onRequestClose(); State.update({ loading: false, }); }, }); console.log("tx succeeded", res); } else { State.update({ loading: false, }); console.log("tx failed", res); } }); }) .catch(() => State.update({ loading: false })); }) .catch(() => State.update({ loading: false })); } const maxValue = symbol === "ETH" || symbol === "WETH" ? Big(balance).minus(MIN_ETH_GAS_FEE).toFixed() : balance; const changeValue = (value) => { if (Number(value) > Number(maxValue)) { value = maxValue; } if (Number(value) < 0) { value = "0"; } if (isValid(value)) { State.update({ amountInUSD: Big(value) .mul(marketReferencePriceInUsd) .toFixed(2, ROUND_DOWN), }); } else { State.update({ amountInUSD: "0.00" }); } State.update({ amount: value }); }; return ( <> <Widget src={`${config.ownerId}/widget/AAVE.Modal.BaseModal`} props={{ title: `Supply ${symbol}`, onRequestClose: onRequestClose, children: ( <WithdrawContainer> <Widget src={`${config.ownerId}/widget/AAVE.Modal.RoundedCard`} props={{ title: "Amount", config, children: ( <> <Widget src={`${config.ownerId}/widget/AAVE.Modal.FlexBetween`} props={{ left: ( <TokenTexture> <Input type="number" value={state.amount} onChange={(e) => { changeValue(e.target.value); }} placeholder="0" /> </TokenTexture> ), right: ( <TokenWrapper> <img width={26} height={26} src={`https://app.aave.com/icons/tokens/${symbol.toLowerCase()}.svg`} /> <TokenTexture>{symbol}</TokenTexture> </TokenWrapper> ), }} /> <Widget src={`${config.ownerId}/widget/AAVE.Modal.FlexBetween`} props={{ left: <GrayTexture>${state.amountInUSD}</GrayTexture>, right: ( <GrayTexture> Wallet Balance: {balance} <Max onClick={() => { changeValue(maxValue); }} > MAX </Max> </GrayTexture> ), }} /> </> ), }} /> <Widget src={`${config.ownerId}/widget/AAVE.Modal.RoundedCard`} props={{ title: "Transaction Overview", config, children: ( <TransactionOverviewContainer> <Widget src={`${config.ownerId}/widget/AAVE.Modal.FlexBetween`} props={{ left: <PurpleTexture>Supply APY</PurpleTexture>, right: ( <WhiteTexture> {(Number(supplyAPY) * 100).toFixed(2)}% </WhiteTexture> ), }} /> <Widget src={`${config.ownerId}/widget/AAVE.Modal.FlexBetween`} props={{ left: <PurpleTexture>Collateralization</PurpleTexture>, right: usageAsCollateralEnabled ? ( <GreenTexture>Enabled</GreenTexture> ) : ( <RedTexture>Disabled</RedTexture> ), }} /> </TransactionOverviewContainer> ), }} /> <Widget src={`${config.ownerId}/widget/AAVE.PrimaryButton`} props={{ config, children: `Supply ${symbol}`, loading: state.loading, onClick: () => { const amount = Big(state.amount) .mul(Big(10).pow(decimals)) .toFixed(0); if (symbol === "ETH" || symbol === "WETH") { // supply weth depositETH(amount); } else { // supply common depositErc20(amount); } }, }} /> </WithdrawContainer> ), config, }} /> </> );