diff --git a/front/src/common/types.ts b/front/src/common/types.ts
index 9fc335e2..4dafe926 100644
--- a/front/src/common/types.ts
+++ b/front/src/common/types.ts
@@ -1,11 +1,14 @@
export enum TransactionType {
SEND_MOVE_INPUT,
+ MINT_LP_NFT,
+ DEPOSIT_LP_NFT,
CREATE_GAME_INPUT,
JOIN_GAME_INPUT,
RESIGN_GAME_INPUT,
DEPLOY_BOT_INPUT,
DEPOSIT_ERC20,
APPROVE_ERC20,
+ MINT_ERC20,
BOT_STEP,
MANAGER_BOT_INPUT,
RELEASE_FUNDS,
@@ -77,6 +80,17 @@ export interface BotStepTransactionInfo extends BaseTransactionInfo {
hash: string;
}
+export interface MintLpNftTransactionInfo extends BaseTransactionInfo {
+ type: TransactionType.MINT_LP_NFT;
+ stableAddress: string;
+ stableAmount: string;
+}
+
+export interface DepositLpNftTransactionInfo extends BaseTransactionInfo {
+ type: TransactionType.DEPOSIT_LP_NFT;
+ tokenId: string;
+}
+
export interface CreateGameTransactionInfo extends BaseTransactionInfo {
type: TransactionType.CREATE_GAME_INPUT;
name: string;
@@ -127,6 +141,12 @@ export interface ApproveErc20TransactionInfo extends BaseTransactionInfo {
amount: string;
}
+export interface MintErc20TransactionInfo extends BaseTransactionInfo {
+ type: TransactionType.MINT_ERC20;
+ tokenAddress: string;
+ tokenAmount: string;
+}
+
export interface BetTransactionInfo extends BaseTransactionInfo {
type: TransactionType.BET_INPUT;
gameId: string;
@@ -158,11 +178,14 @@ export interface JoinTournamentTransactionInfo extends BaseTransactionInfo {
export type TransactionInfo =
| SendMoveTransactionInfo
+ | MintLpNftTransactionInfo
+ | DepositLpNftTransactionInfo
| CreateGameTransactionInfo
| JoinGameTransactionInfo
| DeployBotTransactionInfo
| DepositErc20TransactionInfo
| ApproveErc20TransactionInfo
+ | MintErc20TransactionInfo
| ResignGameTransactionInfo
| BotStepTransactionInfo
| ManagerBotTransactionInfo
diff --git a/front/src/components/Body.jsx b/front/src/components/Body.jsx
index f37b1438..8ce1c8d7 100644
--- a/front/src/components/Body.jsx
+++ b/front/src/components/Body.jsx
@@ -14,6 +14,10 @@ import GameList from "./list/GameList";
import { createGame } from "../state/game/gameSlice";
import { useDispatch } from "react-redux";
import FileUploader from "./BotUploader";
+import ModalDepositLpNft from "./ModalDepositLpNft";
+import ModalMintStables from "./ModalMintStables";
+import ModalMintLpNft from "./ModalMintLpNft";
+import ModalViewLpNft from "./ModalViewLpNft";
import ModalCreateGame from "./ModalCreateGame";
import ModalDepositToken from "./ModalDepositToken";
import { useAppSelector } from "../state/hooks";
diff --git a/front/src/components/ModalDepositLpNft.jsx b/front/src/components/ModalDepositLpNft.jsx
new file mode 100644
index 00000000..43f6fb7a
--- /dev/null
+++ b/front/src/components/ModalDepositLpNft.jsx
@@ -0,0 +1,47 @@
+import { Text, Modal, Button } from "@nextui-org/react";
+import { TransactionType } from "../common/types";
+import { useLpNft } from "../hooks/token";
+import { useActionCreator } from "../state/game/hooks";
+
+export default ({ visible, closeHandler }) => {
+ const addAction = useActionCreator();
+
+ const lpNft = useLpNft();
+
+ const lpNftTokenAddress = lpNft ? lpNft.tokenAddress : "";
+ const lpNftTokenId = lpNft ? lpNft.tokenId : "";
+ const lpNftName = lpNft ? lpNft.name : "";
+ const lpNftImageUri = lpNft ? lpNft.imageUri : "";
+
+ const handleDepositLpNft = async () => {
+ const [depositActionId, wait] = await addAction({
+ type: TransactionType.DEPOSIT_LP_NFT,
+ tokenAddress: lpNftTokenAddress,
+ tokenId: lpNftTokenId,
+ });
+ };
+
+ return (
+
+
+
+ Deposit your LP NFT
+
+
+
+ {lpNftName}
+
+
+
+
+
+
+ );
+};
diff --git a/front/src/components/ModalMintLpNft.jsx b/front/src/components/ModalMintLpNft.jsx
new file mode 100644
index 00000000..f2f5840b
--- /dev/null
+++ b/front/src/components/ModalMintLpNft.jsx
@@ -0,0 +1,93 @@
+import * as React from "react";
+import { Text, Modal, Input, Row, Button } from "@nextui-org/react";
+//import { useDispatch } from "react-redux";
+import { FaCoins } from "react-icons/fa";
+import { useActionCreator } from "../state/game/hooks";
+import { TransactionType } from "../common/types";
+import Select from "react-select";
+import { useStableList } from "../hooks/token";
+import { CONTRACTS } from "../ether/contracts";
+
+export default ({ visible, closeHandler }) => {
+ //const dispatch = useDispatch()
+ const addAction = useActionCreator();
+ const [depositValue, setDepositValue] = React.useState(0);
+ const [stableAddress, setStableAddress] = React.useState(0);
+ const stableList = useStableList();
+
+ const stables = stableList.map((stable) => {
+ return {
+ value: stable.address,
+ label: stable.name,
+ };
+ });
+
+ const onDepositValueChange = (event) => setDepositValue(event.target.value);
+ const onStableAddressChange = (newValue) => setStableAddress(newValue.value);
+ const handleMintLpNft = async () => {
+ // TODO: Get network name
+ const networkName = "localhost";
+
+ // Fetch abi list
+ const contracts = CONTRACTS[networkName];
+ const abis =
+ contracts && contracts.InputFacet && contracts.ERC20PortalFacet
+ ? contracts
+ : CONTRACTS.localhost;
+
+ // Get the staker contract address
+ const uniV3StakerAddress = abis.UniV3Staker.address;
+
+ const [approvalActionId, forApproval] = await addAction({
+ type: TransactionType.APPROVE_ERC20,
+ tokenAddress: stableAddress,
+ spender: uniV3StakerAddress,
+ amount: depositValue,
+ });
+
+ await forApproval;
+
+ const [action, wait] = await addAction({
+ type: TransactionType.MINT_LP_NFT,
+ stableAddress: stableAddress,
+ stableAmount: depositValue,
+ });
+
+ await wait;
+ };
+
+ return (
+
+
+
+ Mint your chess set
+
+
+
+
+ }
+ onChange={onDepositValueChange}
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/front/src/components/ModalMintStables.jsx b/front/src/components/ModalMintStables.jsx
new file mode 100644
index 00000000..b6215d2f
--- /dev/null
+++ b/front/src/components/ModalMintStables.jsx
@@ -0,0 +1,66 @@
+import * as React from "react";
+import { Text, Modal, Input, Row, Button } from "@nextui-org/react";
+import { FaCoins } from "react-icons/fa";
+import { useActionCreator } from "../state/game/hooks";
+import { TransactionType } from "../common/types";
+import Select from "react-select";
+import { useStableList } from "../hooks/token";
+
+export default ({ visible, closeHandler }) => {
+ const addAction = useActionCreator();
+ const [depositValue, setMintValue] = React.useState(0);
+ const [stableAddress, setStableAddress] = React.useState(0);
+ const stableList = useStableList();
+
+ const stables = stableList.map((stable) => {
+ return {
+ value: stable.address,
+ label: stable.name,
+ };
+ });
+
+ const onMintValueChange = (event) => setMintValue(event.target.value);
+ const onStableAddressChange = (newValue) => setStableAddress(newValue.value);
+ const handleMintStables = async () => {
+ await addAction({
+ type: TransactionType.MINT_ERC20,
+ tokenAddress: stableAddress,
+ tokenAmount: depositValue,
+ });
+ };
+
+ return (
+
+
+
+ Mint stable coins
+
+
+
+
+ }
+ onChange={onMintValueChange}
+ />
+
+
+
+
+
+
+
+ );
+};
diff --git a/front/src/components/ModalViewLpNft.jsx b/front/src/components/ModalViewLpNft.jsx
new file mode 100644
index 00000000..683f0b11
--- /dev/null
+++ b/front/src/components/ModalViewLpNft.jsx
@@ -0,0 +1,28 @@
+import { Text, Modal } from "@nextui-org/react";
+import { useLpNft } from "../hooks/token";
+
+export default ({ visible, closeHandler }) => {
+ const lpNft = useLpNft();
+
+ const lpNftName = lpNft ? lpNft.name : "";
+ const lpNftImageUri = lpNft ? lpNft.imageUri : "";
+
+ return (
+
+
+
+ View your LP NFT
+
+
+
+ {lpNftName}
+
+
+
+ );
+};
diff --git a/front/src/hooks/token.ts b/front/src/hooks/token.ts
index 7bb4567c..9afad399 100644
--- a/front/src/hooks/token.ts
+++ b/front/src/hooks/token.ts
@@ -2,10 +2,17 @@ import { useWeb3React } from "@web3-react/core";
import { useEffect, useMemo, useState } from "react";
import { useSelector } from "react-redux";
+import { CHAINS } from "../ether/chains";
+import { CONTRACTS } from "../ether/contracts";
import { useAppSelector } from "../state/hooks";
import { COINGECKO_LIST, fetchTokenList } from "../utils/lists";
+import STABLE_LIST from "../utils/lists/ultrachess.stablelists.json";
import ULTRACHESS_LIST from "../utils/lists/ultrachess.tokenlists.json";
-import { useContractCallResult, useErc20Contract } from "./contract";
+import {
+ useContract,
+ useContractCallResult,
+ useErc20Contract,
+} from "./contract";
export interface Token {
address: string;
@@ -16,6 +23,15 @@ export interface Token {
logoUri?: string;
}
+export interface LpNft {
+ address: string;
+ chainId: number;
+ tokenId: number;
+ name: string;
+ description: string;
+ imageUri: string;
+}
+
export function useTokenList(): Token[] {
const { chainId } = useWeb3React();
const [tokenList, setTokenList] = useState([]);
@@ -42,6 +58,82 @@ export function useTokenList(): Token[] {
return tokenList;
}
+export function useStableList(): Token[] {
+ const { chainId } = useWeb3React();
+ const [stableList, setStableList] = useState([]);
+
+ useEffect(() => {
+ const callFetch = async () => {
+ // const result = await fetchStableList(
+ // ULTRACHESS_LIST,
+ // (ensName: string) => {
+ // return {} as Promise
+ // }
+ // )
+ console.log("chainId: ", chainId);
+ const stables = STABLE_LIST.filter((stable) => stable.chainId == chainId);
+ setStableList(stables);
+ };
+
+ callFetch().catch(console.error);
+ }, [chainId]);
+
+ return stableList;
+}
+
+export function useLpNft(): LpNft {
+ const { account, chainId } = useWeb3React();
+ const [lpNft, setLpNft] = useState();
+
+ // Get network name
+ const CHAIN = CHAINS[chainId];
+ const networkName =
+ CHAIN && CHAIN.networkName ? CHAIN.networkName : "localhost";
+
+ // Fetch abi list
+ const contracts = CONTRACTS[networkName];
+ const abis =
+ contracts && contracts.InputFacet && contracts.ERC20PortalFacet
+ ? contracts
+ : CONTRACTS.localhost;
+
+ const lpSftContract = useContract(abis.LpSft.address, abis.LpSft.abi);
+
+ useEffect(() => {
+ const callFetch = async () => {
+ if (!lpSftContract) return;
+
+ const tokenIds = await lpSftContract.getTokenIds(account);
+ if (tokenIds.length > 0) {
+ const tokenId = tokenIds[0];
+ const tokenUri = await lpSftContract.uri(tokenId);
+
+ // Decode the token URI
+ const tokenUriResponse = await fetch(tokenUri);
+ const tokenMetadata = await tokenUriResponse.json();
+
+ const tokenName = tokenMetadata.name;
+ const tokenDescription = tokenMetadata.description;
+ const tokenImageUri = tokenMetadata.image;
+
+ const lpNft = {
+ address: lpSftContract.address,
+ chainId: chainId,
+ tokenId: tokenId,
+ name: tokenName,
+ description: tokenDescription,
+ imageUri: tokenImageUri,
+ };
+ setLpNft(lpNft);
+ }
+ };
+
+ callFetch().catch(console.error);
+ }, [chainId, lpSftContract]);
+
+ return lpNft;
+}
+
export function useTokenFromList(address: string): Token {
const tokenList = useTokenList();
diff --git a/front/src/state/game/hooks.ts b/front/src/state/game/hooks.ts
index 90472899..3e33fada 100644
--- a/front/src/state/game/hooks.ts
+++ b/front/src/state/game/hooks.ts
@@ -613,12 +613,19 @@ export function useActionCreator(): (
const dispatch = useDispatch();
const addAction = useAddAction();
const addTransaction = useTransactionAdder();
- // TODO: Handle DAPP_ADDRESSES[networkName] or CONTRACTS[networkName] not defined
+ // TODO: Handle dappAddress or abis not defined
const contract = useContract(dappAddress, abis.InputFacet.abi);
const erc20PortalContract = useContract(
dappAddress,
abis.ERC20PortalFacet.abi
);
+ const uniV3StakerContract = useContract(
+ abis.UniV3Staker.address,
+ abis.UniV3Staker.abi
+ );
+ const daiContract = useContract(abis.DAI.address, abis.DAI.abi);
+ const usdcContract = useContract(abis.USDC.address, abis.USDC.abi);
+ const usdtContract = useContract(abis.USDT.address, abis.USDT.abi);
return useCallback(
async (info: TransactionInfo) => {
@@ -632,7 +639,9 @@ export function useActionCreator(): (
id: id,
type:
info.type == TransactionType.APPROVE_ERC20 ||
- info.type == TransactionType.DEPOSIT_ERC20
+ info.type == TransactionType.DEPOSIT_ERC20 ||
+ info.type == TransactionType.MINT_ERC20 ||
+ info.type == TransactionType.MINT_LP_NFT
? ActionType.TRANSACTION
: ActionType.INPUT,
transactionInfo: info,
@@ -875,6 +884,73 @@ export function useActionCreator(): (
erc20Amount
);
break;
+ case TransactionType.MINT_ERC20: {
+ const { tokenAddress, tokenAmount } = info;
+
+ // Get the stable contract
+ let stableContract;
+ if (tokenAddress.toLowerCase() == daiContract.address.toLowerCase())
+ stableContract = daiContract;
+ else if (
+ tokenAddress.toLowerCase() == usdcContract.address.toLowerCase()
+ )
+ stableContract = usdcContract;
+ else if (
+ tokenAddress.toLowerCase() == usdtContract.address.toLowerCase()
+ )
+ stableContract = usdtContract;
+
+ // TODO: More efficient decimal handling
+ const decimals = await stableContract.decimals();
+
+ // Calculate ERC20 amount
+ var erc20Amount = ethers.BigNumber.from(
+ ethers.utils.parseUnits(tokenAmount, decimals)
+ );
+
+ // Mint tokens
+ result = await stableContract.mint(
+ await stableContract.signer.getAddress(),
+ erc20Amount
+ );
+
+ break;
+ }
+ case TransactionType.MINT_LP_NFT: {
+ const { stableAddress, stableAmount } = info;
+
+ // Get the stable index
+ let stableIndex;
+ if (
+ stableAddress.toLowerCase() == daiContract.address.toLowerCase()
+ )
+ stableIndex = 0;
+ else if (
+ stableAddress.toLowerCase() == usdcContract.address.toLowerCase()
+ )
+ stableIndex = 1;
+ else if (
+ stableAddress.toLowerCase() == usdtContract.address.toLowerCase()
+ )
+ stableIndex = 2;
+
+ // Mint the LP NFT
+ result = await uniV3StakerContract.stakeNFTOneStable(
+ stableIndex,
+ stableAmount
+ );
+
+ break;
+ }
+ case TransactionType.DEPOSIT_LP_NFT: {
+ const { tokenId } = info;
+
+ alert(`Depositing token ${tokenId}`);
+
+ //result = await uniV3StakerContract.stakeNFTOneToken(stableIndex, stableAmount);
+
+ break;
+ }
default:
break;
}
@@ -900,7 +976,9 @@ export function useActionCreator(): (
id: id,
type:
info.type == TransactionType.APPROVE_ERC20 ||
- info.type == TransactionType.DEPOSIT_ERC20
+ info.type == TransactionType.DEPOSIT_ERC20 ||
+ info.type == TransactionType.MINT_ERC20 ||
+ info.type == TransactionType.MINT_LP_NFT
? ActionType.TRANSACTION
: ActionType.INPUT,
transactionInfo: info,
diff --git a/front/src/utils/lists/ultrachess.stablelists.json b/front/src/utils/lists/ultrachess.stablelists.json
new file mode 100644
index 00000000..457ee069
--- /dev/null
+++ b/front/src/utils/lists/ultrachess.stablelists.json
@@ -0,0 +1,26 @@
+[
+ {
+ "name": "USDC",
+ "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
+ "symbol": "USDC",
+ "decimals": 6,
+ "chainId": 1,
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ {
+ "name": "USDC",
+ "address": "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174",
+ "symbol": "USDC",
+ "decimals": 6,
+ "chainId": 137,
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ },
+ {
+ "name": "USDC",
+ "address": "0xea753456f554F59f70CD0E078FBd2FED058Cedcc",
+ "symbol": "USDC",
+ "decimals": 6,
+ "chainId": 31337,
+ "logoURI": "https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png"
+ }
+]
diff --git a/server/bot.py b/server/bot.py
index 17a6138d..57977f20 100644
--- a/server/bot.py
+++ b/server/bot.py
@@ -4,6 +4,8 @@
import subprocess
import chess.engine
+import requests
+from eth_abi import encode_abi
import deps
import notification
@@ -11,6 +13,9 @@
logging.basicConfig(level="INFO")
logger = logging.getLogger(__name__)
+# TODO
+MINT_BOT_FUNCTION_SELECTOR = b"\xb4\x83\xaf\xd3\xf4\xca\xed\xc6\xee\xbfD$o\xe5N8\xc9^1y\xa5\xec\x9e\xa8\x17@\xec\xa5\xb4\x82\xd1."
+
class Bot:
def __init__(self, id, owner, binary, timestamp):
@@ -89,6 +94,29 @@ def create(self, owner, binary, timestamp):
self.bots[id] = bot
return True
+ def mintNft(self, owner, timestamp, options, rollup_server):
+ id = options["botId"]
+ recipient = options["recipient"]
+ bot = self.bots[id]
+
+ # TODO: Set bot owner to recipient
+
+ # Encode a function call
+ function_payload = MINT_BOT_FUNCTION_SELECTOR + encode_abi(
+ ["string", "address"], [id, recipient]
+ )
+
+ """
+ # Post voucher executing the transfer
+ voucher = {"address": botNftAddress, "payload": "0x" + function_payload.hex()}
+ logger.info(f"Issuing voucher {voucher}")
+
+ response = requests.post(rollup_server + "/voucher", json=voucher)
+ logger.info(f"Received voucher status {response.status_code} body {response.content}")
+ """
+
+ return True
+
def getOwner(self, id):
return self.bots[id].owner
diff --git a/server/main.py b/server/main.py
index 945daad5..8954491a 100644
--- a/server/main.py
+++ b/server/main.py
@@ -299,6 +299,12 @@ def handle_advance(data):
except Exception:
traceback.print_exc()
success = False
+ elif operator == "mintBotNft":
+ try:
+ botManager.mintNft(sender, timeStamp, value, rollup_server)
+ except Exception:
+ traceback.print_exc()
+ success = False
elif operator == "createTourney":
try:
tournamentManager.create(sender, value)
diff --git a/server/nft.py b/server/nft.py
new file mode 100644
index 00000000..ec2db4ee
--- /dev/null
+++ b/server/nft.py
@@ -0,0 +1,78 @@
+import logging
+
+import requests
+from eth_abi import encode_abi
+
+from deps import *
+
+logging.basicConfig(level="INFO")
+logger = logging.getLogger(__name__)
+
+TRANSFER_FUNCTION_SELECTOR = b"\xb4\x83\xaf\xd3\xf4\xca\xed\xc6\xee\xbfD$o\xe5N8\xc9^1y\xa5\xec\x9e\xa8\x17@\xec\xa5\xb4\x82\xd1."
+
+
+class AccountBalanceManager:
+ def __init__(self):
+ self.accounts = {"0x": {}}
+
+ def getBalance(self, account, token):
+ lowered_account = account.lower()
+ if not lowered_account in self.accounts:
+ self.accounts[lowered_account] = {}
+ if not token in self.accounts[lowered_account]:
+ self.accounts[lowered_account][token] = 0
+ return self.accounts[lowered_account][token]
+
+ def setBalance(self, account, balance, token):
+ lowered_account = account.lower()
+ if not lowered_account in self.accounts:
+ self.accounts[lowered_account] = {}
+ self.accounts[lowered_account][token] = balance
+
+ def deposit(self, account, value, token):
+ prev = self.getBalance(account, token)
+ new = prev + value
+ logger.info(
+ "prev:"
+ + str(prev)
+ + " new:"
+ + str(new)
+ + " value:"
+ + str(value)
+ + " token:"
+ + str(token)
+ )
+ self.setBalance(account, new, token)
+
+ def withdraw(self, account, value, token):
+ prev = self.getBalance(account, token)
+ if value > prev:
+ return False
+ new = prev - value
+ self.setBalance(account, new, token)
+ return True
+
+ def release(self, account, token, rollup_server):
+ # Fetch balance
+ amount = self.getBalance(account, token)
+ # Encode a transfer function call that returns the amount back to the depositor
+ # Encode a transfer function call that returns the amount back to the depositor
+ transfer_payload = TRANSFER_FUNCTION_SELECTOR + encode_abi(
+ ["address", "uint256"], [account, amount]
+ )
+ # Post voucher executing the transfer on the ERC-20 contract: "I don't want your money"!
+ voucher = {"address": token, "payload": "0x" + transfer_payload.hex()}
+ logger.info(f"Issuing voucher {voucher}")
+ response = requests.post(rollup_server + "/voucher", json=voucher)
+ logger.info(
+ f"Received voucher status {response.status_code} body {response.content}"
+ )
+ # Withdraw balance
+ self.withdraw(account, amount, token)
+ return True
+
+ def getStringState(self):
+ return str(self.accounts)
+
+ def getState(self):
+ return self.accounts