diff --git a/README.md b/README.md
index 110b5a6d..2502ca33 100644
--- a/README.md
+++ b/README.md
@@ -81,6 +81,11 @@ type Config = {
},
contracts?: {
nftFaucet?: string
+ marketplace?: {
+ fixedPrice: {
+ tez: string;
+ }
+ }
}
}
```
diff --git a/client/package.json b/client/package.json
index b47a5411..a62e92c3 100644
--- a/client/package.json
+++ b/client/package.json
@@ -8,9 +8,9 @@
"@emotion/react": "11.1.4",
"@emotion/styled": "11.0.0",
"@reduxjs/toolkit": "1.5.0",
- "@taquito/beacon-wallet": "8.0.3-beta.0",
- "@taquito/tzip16": "8.0.3-beta.0",
- "@taquito/taquito": "8.0.3-beta.0",
+ "@taquito/beacon-wallet": "8.0.4-beta.0",
+ "@taquito/tzip16": "8.0.4-beta.0",
+ "@taquito/taquito": "8.0.4-beta.0",
"@types/lodash": "4.14.165",
"@types/react": "16.9.12",
"@types/react-dom": "16.9.0",
diff --git a/client/src/components/Collections/Catalog/TokenGrid.tsx b/client/src/components/Collections/Catalog/TokenGrid.tsx
index 92adefc9..3a650a6b 100644
--- a/client/src/components/Collections/Catalog/TokenGrid.tsx
+++ b/client/src/components/Collections/Catalog/TokenGrid.tsx
@@ -141,7 +141,7 @@ export default function TokenGrid({ state, walletAddress }: TokenGridProps) {
}
const tokens = collection.tokens.filter(
- ({ owner }) => owner === walletAddress
+ ({ owner, sale }) => owner === walletAddress || sale?.seller === walletAddress
);
if (tokens.length === 0) {
diff --git a/client/src/components/Collections/TokenDetail/index.tsx b/client/src/components/Collections/TokenDetail/index.tsx
index 4b3e45af..d6d7e758 100644
--- a/client/src/components/Collections/TokenDetail/index.tsx
+++ b/client/src/components/Collections/TokenDetail/index.tsx
@@ -1,13 +1,21 @@
import React, { useEffect, useState } from 'react';
import { useLocation } from 'wouter';
-import { AspectRatio, Box, Flex, Heading, Image, Text } from '@chakra-ui/react';
import {
- ChevronLeft,
- HelpCircle,
- /* MoreHorizontal, */ Star
-} from 'react-feather';
-import { MinterButton } from '../../common';
-import { TransferTokenButton } from '../../common/TransferToken';
+ AspectRatio,
+ Box,
+ Flex,
+ Heading,
+ Image,
+ Menu,
+ MenuList,
+ Text,
+ useDisclosure
+} from '@chakra-ui/react';
+import { ChevronLeft, HelpCircle, MoreHorizontal, Star } from 'react-feather';
+import { MinterButton, MinterMenuButton, MinterMenuItem } from '../../common';
+import { TransferTokenModal } from '../../common/TransferToken';
+import { SellTokenButton, CancelTokenSaleButton } from '../../common/SellToken';
+import { BuyTokenButton } from '../../common/BuyToken';
import { ipfsUriToGatewayUrl, uriToCid } from '../../../lib/util/ipfs';
import { useSelector, useDispatch } from '../../../reducer';
import {
@@ -117,6 +125,7 @@ interface TokenDetailProps {
function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
const [, setLocation] = useLocation();
const { system, collections: state } = useSelector(s => s);
+ const disclosure = useDisclosure();
const dispatch = useDispatch();
const collection = state.collections[contractAddress];
@@ -195,8 +204,41 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
borderRadius="3px"
py={6}
mb={10}
+ pos="relative"
>
- {system.tzPublicKey && system.tzPublicKey === token.owner ? (
+ {system.tzPublicKey &&
+ (system.tzPublicKey === token.owner ||
+ system.tzPublicKey === token.sale?.seller) ? (
+
+
+
+
+ ) : null}
+
+ {system.tzPublicKey &&
+ (system.tzPublicKey === token.owner ||
+ system.tzPublicKey === token.sale?.seller) ? (
) : null}
+
- {/* TODO: Add dropdown menu that contains transfer/share links */}
- {/* */}
- {/* */}
- {/* */}
{uriToCid(token.artifactUri) || 'No IPFS Hash'}
- {system.status === 'WalletConnected' ? (
-
-
+
+
+
+
+
+ Market status
+
+ {token.sale ? (
+
+ For sale
+
+ ) : (
+
+ Not for sale
+
+ )}
+
+ {token.sale ? (
+
+
+ Price
+
+
+ ꜩ {token.sale.price}
+
+
+ ) : null}
+ {system.tzPublicKey &&
+ (system.tzPublicKey === token.owner ||
+ system.tzPublicKey === token.sale?.seller) ? (
+
+ {token.sale ? (
+
+ ) : (
+
+ )}
+
+ ) : token.sale ? (
+
+ ) : null}
- ) : null}
+
diff --git a/client/src/components/CreateNonFungiblePage/FileUpload.tsx b/client/src/components/CreateNonFungiblePage/FileUpload.tsx
index 773f8bc0..2df38fe1 100644
--- a/client/src/components/CreateNonFungiblePage/FileUpload.tsx
+++ b/client/src/components/CreateNonFungiblePage/FileUpload.tsx
@@ -11,7 +11,7 @@ import {
export function FilePreview({ file }: { file: SelectedFile }) {
const dispatch = useDispatch();
if (/^image\/.*/.test(file.type)) {
- return ;
+ return ;
}
if (/^video\/.*/.test(file.type)) {
const canvasRef = createRef();
@@ -101,8 +101,16 @@ export default function FileUpload() {
>
{state.selectedFile?.objectUrl ? (
-
-
+
+
+
+
) : (
diff --git a/client/src/components/CreateNonFungiblePage/StatusModal.tsx b/client/src/components/CreateNonFungiblePage/StatusModal.tsx
index 4a9f9a5b..e62e556b 100644
--- a/client/src/components/CreateNonFungiblePage/StatusModal.tsx
+++ b/client/src/components/CreateNonFungiblePage/StatusModal.tsx
@@ -6,16 +6,79 @@ import {
Heading,
Modal,
ModalOverlay,
- ModalContent
+ ModalContent,
+ Text
} from '@chakra-ui/react';
-import { CheckCircle } from 'react-feather';
+import { CheckCircle, AlertCircle, X } from 'react-feather';
import { MinterButton } from '../common';
-import { StatusKey } from '../../reducer/slices/status';
+import { Status } from '../../reducer/slices/status';
interface StatusModalProps {
isOpen: boolean;
onClose: () => void;
- status: StatusKey;
+ onRetry: () => void;
+ onCancel: () => void;
+ status: Status;
+}
+
+function Content({ status, onClose, onRetry, onCancel }: StatusModalProps) {
+ if (status.error) {
+ return (
+
+
+
+
+
+ Error Creating Token
+
+
+ onRetry()}>
+ Retry
+
+ onCancel()}
+ display="flex"
+ alignItems="center"
+ ml={4}
+ >
+
+
+
+
+ Close
+
+
+
+
+ );
+ }
+ if (status.status === 'in_transit') {
+ return (
+
+
+
+ Creating token...
+
+
+ );
+ }
+ if (status.status === 'complete') {
+ return (
+
+
+
+
+
+ Token creation complete
+
+ onClose()}>
+ Close
+
+
+ );
+ }
+ return null;
}
export default function StatusModal(props: StatusModalProps) {
@@ -23,7 +86,7 @@ export default function StatusModal(props: StatusModalProps) {
const initialRef = React.useRef(null);
const close = () => {
- if (status === 'complete') {
+ if (status.status === 'complete') {
onClose();
}
};
@@ -41,27 +104,7 @@ export default function StatusModal(props: StatusModalProps) {
>
- {status === 'in_transit' ? (
-
-
-
- Creating token...
-
-
- ) : null}
- {status === 'complete' ? (
-
-
-
-
-
- Token creation complete
-
- close()}>
- Close
-
-
- ) : null}
+
>
diff --git a/client/src/components/CreateNonFungiblePage/index.tsx b/client/src/components/CreateNonFungiblePage/index.tsx
index 56d3c8c7..3ab54cc3 100644
--- a/client/src/components/CreateNonFungiblePage/index.tsx
+++ b/client/src/components/CreateNonFungiblePage/index.tsx
@@ -19,7 +19,7 @@ import {
} from '../../reducer/slices/createNft';
import { mintTokenAction } from '../../reducer/async/actions';
import { validateCreateNftStep } from '../../reducer/validators/createNft';
-import { setStatus } from '../../reducer/slices/status';
+import { clearError, setStatus } from '../../reducer/slices/status';
function ProgressIndicator({ state }: { state: CreateNftState }) {
const stepIdx = steps.indexOf(state.step);
@@ -153,7 +153,16 @@ export default function CreateNonFungiblePage() {
dispatch(setStatus({ method: 'mintToken', status: 'ready' }));
dispatch(clearForm());
}}
- status={status.status}
+ onRetry={() => {
+ dispatch(clearError({ method: 'mintToken' }));
+ dispatch(mintTokenAction());
+ }}
+ onCancel={() => {
+ onClose();
+ dispatch(clearError({ method: 'mintToken' }));
+ dispatch(setStatus({ method: 'mintToken', status: 'ready' }));
+ }}
+ status={status}
/>
diff --git a/client/src/components/common/BuyToken.tsx b/client/src/components/common/BuyToken.tsx
new file mode 100644
index 00000000..bbd41689
--- /dev/null
+++ b/client/src/components/common/BuyToken.tsx
@@ -0,0 +1,112 @@
+import React from 'react';
+import {
+ Box,
+ Button,
+ Flex,
+ Spinner,
+ Heading,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+ Text,
+ useDisclosure
+} from '@chakra-ui/react';
+import { CheckCircle } from 'react-feather';
+import { MinterButton } from '../common';
+import { useSelector, useDispatch } from '../../reducer';
+import { buyTokenAction } from '../../reducer/async/actions';
+import { setStatus } from '../../reducer/slices/status';
+import { Nft } from '../../lib/nfts/queries';
+
+interface BuyTokenButtonProps {
+ contract: string;
+ token: Nft;
+}
+
+export function BuyTokenButton(props: BuyTokenButtonProps) {
+ const { status } = useSelector(s => s.status.buyToken);
+ const dispatch = useDispatch();
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const initialRef = React.useRef(null);
+
+ const onSubmit = async () => {
+ dispatch(
+ buyTokenAction({
+ contract: props.contract,
+ tokenId: props.token.id,
+ tokenSeller: props.token.sale?.seller || "",
+ salePrice: props.token.sale?.price || 0
+ })
+ );
+ };
+
+ const close = () => {
+ if (status !== 'in_transit') {
+ dispatch(setStatus({ method: 'buyToken', status: 'ready' }));
+ onClose();
+ }
+ };
+
+ return (
+ <>
+ Buy now
+
+ close()}
+ initialFocusRef={initialRef}
+ closeOnEsc={false}
+ closeOnOverlayClick={false}
+ onEsc={() => close()}
+ onOverlayClick={() => close()}
+ size="xs"
+ >
+
+
+ {status === 'ready' ? (
+ <>
+ Checkout
+
+
+
+ You are about to purchase {props.token.title} (ꜩ {props.token.sale?.price})
+
+
+
+
+
+ >
+ ) : null}
+ {status === 'in_transit' ? (
+
+
+
+ Purchasing token...
+
+
+ ) : null}
+ {status === 'complete' ? (
+
+
+
+
+
+ Token purchased
+
+ close()}>
+ Close
+
+
+ ) : null}
+
+
+ >
+ );
+}
diff --git a/client/src/components/common/CreateCollection.tsx b/client/src/components/common/CreateCollection.tsx
index 8608dd87..d7e4726c 100644
--- a/client/src/components/common/CreateCollection.tsx
+++ b/client/src/components/common/CreateCollection.tsx
@@ -1,4 +1,9 @@
-import React, { useState, MutableRefObject } from 'react';
+import React, {
+ useState,
+ MutableRefObject,
+ SetStateAction,
+ Dispatch
+} from 'react';
import {
Box,
Text,
@@ -17,20 +22,25 @@ import {
Flex,
Heading
} from '@chakra-ui/react';
-import { CheckCircle, Plus } from 'react-feather';
+import { CheckCircle, Plus, AlertCircle, X } from 'react-feather';
import { MinterButton } from '../common';
-
import { useSelector, useDispatch } from '../../reducer';
import { createAssetContractAction } from '../../reducer/async/actions';
-import { setStatus } from '../../reducer/slices/status';
+import { clearError, setStatus, Status } from '../../reducer/slices/status';
interface FormProps {
initialRef: MutableRefObject;
onSubmit: (form: { contractName: string }) => void;
+ contractName: string;
+ setContractName: Dispatch>;
}
-function Form({ initialRef, onSubmit }: FormProps) {
- const [contractName, setContractName] = useState('');
+function Form({
+ initialRef,
+ onSubmit,
+ contractName,
+ setContractName
+}: FormProps) {
return (
<>
New Collection
@@ -60,18 +70,88 @@ function Form({ initialRef, onSubmit }: FormProps) {
);
}
+interface ContentProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onRetry: () => void;
+ onCancel: () => void;
+ status: Status;
+}
+
+function Content({ status, onClose, onRetry, onCancel }: ContentProps) {
+ if (status.error) {
+ return (
+
+
+
+
+
+ Error Creating Collection
+
+
+ onRetry()}>
+ Retry
+
+ onCancel()}
+ display="flex"
+ alignItems="center"
+ ml={4}
+ >
+
+
+
+
+ Close
+
+
+
+
+ );
+ }
+ if (status.status === 'in_transit') {
+ return (
+
+
+
+ Creating collection...
+
+
+ );
+ }
+ if (status.status === 'complete') {
+ return (
+
+
+
+
+
+ Collection created
+
+ onClose()}>
+ Close
+
+
+ );
+ }
+ return null;
+}
+
export function CreateCollectionButton() {
- const { status } = useSelector(s => s.status.createAssetContract);
+ const status = useSelector(s => s.status.createAssetContract);
const dispatch = useDispatch();
+ const [contractName, setContractName] = useState('');
+
const { isOpen, onOpen, onClose } = useDisclosure();
const initialRef = React.useRef(null);
- const onSubmit = async (form: { contractName: string }) => {
- dispatch(createAssetContractAction(form.contractName));
+ const onSubmit = async () => {
+ dispatch(createAssetContractAction(contractName));
};
const close = () => {
- if (status !== 'in_transit') {
+ if (status.status !== 'in_transit') {
dispatch(setStatus({ method: 'createAssetContract', status: 'ready' }));
onClose();
}
@@ -97,30 +177,31 @@ export function CreateCollectionButton() {
>
- {status === 'ready' ? (
-
- ) : null}
- {status === 'in_transit' ? (
-
-
-
- Creating new collection...
-
-
- ) : null}
- {status === 'complete' ? (
-
-
-
-
-
- Collection created
-
- close()}>
- Close
-
-
- ) : null}
+ {status.status === 'ready' ? (
+
+ ) : (
+ close()}
+ onCancel={() => {
+ onClose();
+ dispatch(clearError({ method: 'createAssetContract' }));
+ dispatch(
+ setStatus({ method: 'createAssetContract', status: 'ready' })
+ );
+ }}
+ onRetry={() => {
+ dispatch(clearError({ method: 'createAssetContract' }));
+ onSubmit();
+ }}
+ />
+ )}
>
diff --git a/client/src/components/common/SellToken.tsx b/client/src/components/common/SellToken.tsx
new file mode 100644
index 00000000..3e8542fe
--- /dev/null
+++ b/client/src/components/common/SellToken.tsx
@@ -0,0 +1,227 @@
+import React, { useState, MutableRefObject } from 'react';
+import {
+ Box,
+ Button,
+ Flex,
+ Spinner,
+ Heading,
+ FormControl,
+ Input,
+ InputGroup,
+ InputLeftElement,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalFooter,
+ ModalBody,
+ ModalCloseButton,
+ Text,
+ useDisclosure
+} from '@chakra-ui/react';
+import { Check, CheckCircle } from 'react-feather';
+import { MinterButton } from '../common';
+import { useSelector, useDispatch } from '../../reducer';
+import { listTokenAction, cancelTokenSaleAction } from '../../reducer/async/actions';
+import { setStatus } from '../../reducer/slices/status';
+
+interface FormProps {
+ initialRef: MutableRefObject;
+ onSubmit: (form: { salePrice: string }) => void;
+}
+
+interface SellTokenButtonProps {
+ contract: string;
+ tokenId: number;
+}
+
+function FormFixedPrice({ initialRef, onSubmit }: FormProps) {
+ const [salePrice, setSalePrice] = useState('');
+ return (
+ <>
+ Set your price
+
+
+
+
+
+
+ setSalePrice(e.target.value)}
+ />
+
+
+
+ onSubmit({ salePrice })}
+ >
+
+
+
+
+
+
+ >
+ );
+}
+
+export function SellTokenButton(props: SellTokenButtonProps) {
+ const { status } = useSelector(s => s.status.listToken);
+ const dispatch = useDispatch();
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const initialRef = React.useRef(null);
+
+ const onSubmit = async (form: { salePrice: string }) => {
+ let salePrice = Math.floor(Number(form.salePrice) * 1000000);
+ if (Number.isNaN(salePrice)) {
+ salePrice = 0;
+ }
+ dispatch(
+ listTokenAction({
+ ...props,
+ salePrice: salePrice
+ })
+ );
+ };
+
+ const close = () => {
+ if (status !== 'in_transit') {
+ dispatch(setStatus({ method: 'listToken', status: 'ready' }));
+ onClose();
+ }
+ };
+
+ return (
+ <>
+ List for sale
+
+ close()}
+ initialFocusRef={initialRef}
+ closeOnEsc={false}
+ closeOnOverlayClick={false}
+ onEsc={() => close()}
+ onOverlayClick={() => close()}
+ >
+
+
+ {status === 'ready' ? (
+
+ ) : null}
+ {status === 'in_transit' ? (
+
+
+
+ Listing token for sale...
+
+
+ ) : null}
+ {status === 'complete' ? (
+
+
+
+
+
+ Listing complete
+
+ close()}>
+ Close
+
+
+ ) : null}
+
+
+ >
+ );
+}
+
+export function CancelTokenSaleButton(props: SellTokenButtonProps) {
+ const { status } = useSelector(s => s.status.cancelTokenSale);
+ const dispatch = useDispatch();
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const initialRef = React.useRef(null);
+
+ const onSubmit = async () => {
+ dispatch(
+ cancelTokenSaleAction({
+ ...props
+ })
+ );
+ };
+
+ const close = () => {
+ if (status !== 'in_transit') {
+ dispatch(setStatus({ method: 'cancelTokenSale', status: 'ready' }));
+ onClose();
+ }
+ };
+
+ return (
+ <>
+ Cancel sale
+
+ close()}
+ initialFocusRef={initialRef}
+ closeOnEsc={false}
+ closeOnOverlayClick={false}
+ onEsc={() => close()}
+ onOverlayClick={() => close()}
+ >
+
+
+ {status === 'ready' ? (
+ <>
+ Are you sure?
+
+
+
+ Are you sure you want to cancel the sale?
+
+
+
+
+
+
+ >
+ ) : null}
+ {status === 'in_transit' ? (
+
+
+
+ Canceling sale...
+
+
+ ) : null}
+ {status === 'complete' ? (
+
+
+
+
+
+ Listing canceled
+
+ close()}>
+ Close
+
+
+ ) : null}
+
+
+ >
+ );
+}
diff --git a/client/src/components/common/TransferToken.tsx b/client/src/components/common/TransferToken.tsx
index 6d1d4ab6..b06a4483 100644
--- a/client/src/components/common/TransferToken.tsx
+++ b/client/src/components/common/TransferToken.tsx
@@ -1,10 +1,14 @@
-import React, { useState, MutableRefObject } from 'react';
+import React, {
+ useState,
+ MutableRefObject,
+ SetStateAction,
+ Dispatch
+} from 'react';
import {
Box,
Flex,
Spinner,
Heading,
- Text,
FormControl,
FormLabel,
Input,
@@ -15,21 +19,24 @@ import {
ModalFooter,
ModalBody,
ModalCloseButton,
- useDisclosure
+ Text,
+ useDisclosure,
+ UseDisclosureReturn
} from '@chakra-ui/react';
-import { Plus, CheckCircle } from 'react-feather';
+import { CheckCircle, AlertCircle, X, Plus } from 'react-feather';
import { MinterButton } from '../common';
import { useSelector, useDispatch } from '../../reducer';
import { transferTokenAction } from '../../reducer/async/actions';
-import { setStatus } from '../../reducer/slices/status';
+import { clearError, setStatus, Status } from '../../reducer/slices/status';
interface FormProps {
initialRef: MutableRefObject;
onSubmit: (form: { toAddress: string }) => void;
+ toAddress: string;
+ setToAddress: Dispatch>;
}
-function Form({ initialRef, onSubmit }: FormProps) {
- const [toAddress, setToAddress] = useState('');
+function Form({ initialRef, onSubmit, toAddress, setToAddress }: FormProps) {
return (
<>
Transfer Token
@@ -59,81 +66,161 @@ function Form({ initialRef, onSubmit }: FormProps) {
);
}
-interface TransferTokenButtonProps {
+interface ContentProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onRetry: () => void;
+ onCancel: () => void;
+ status: Status;
+}
+
+function Content({ status, onClose, onRetry, onCancel }: ContentProps) {
+ if (status.error) {
+ return (
+
+
+
+
+
+ Error Transferring Token
+
+
+ onRetry()}>
+ Retry
+
+ onCancel()}
+ display="flex"
+ alignItems="center"
+ ml={4}
+ >
+
+
+
+
+ Close
+
+
+
+
+ );
+ }
+ if (status.status === 'in_transit') {
+ return (
+
+
+
+ Transferring token...
+
+
+ );
+ }
+ if (status.status === 'complete') {
+ return (
+
+
+
+
+
+ Token transfer complete
+
+ onClose()}>
+ Close
+
+
+ );
+ }
+ return null;
+}
+
+interface TransferTokenModalProps {
contractAddress: string;
tokenId: number;
+ disclosure: UseDisclosureReturn;
}
-export function TransferTokenButton(props: TransferTokenButtonProps) {
- const { status } = useSelector(s => s.status.transferToken);
+export function TransferTokenModal(props: TransferTokenModalProps) {
+ const status = useSelector(s => s.status.transferToken);
const dispatch = useDispatch();
+ const [toAddress, setToAddress] = useState('');
+ const { isOpen, onClose } = props.disclosure;
- const { isOpen, onOpen, onClose } = useDisclosure();
const initialRef = React.useRef(null);
- const onSubmit = async (form: { toAddress: string }) => {
+ const onSubmit = async () => {
dispatch(
transferTokenAction({
contract: props.contractAddress,
tokenId: props.tokenId,
- to: form.toAddress
+ to: toAddress
})
);
};
const close = () => {
- if (status !== 'in_transit') {
+ if (status.status !== 'in_transit') {
dispatch(setStatus({ method: 'transferToken', status: 'ready' }));
onClose();
}
};
+ return (
+ close()}
+ initialFocusRef={initialRef}
+ closeOnEsc={false}
+ closeOnOverlayClick={false}
+ onEsc={() => close()}
+ onOverlayClick={() => close()}
+ >
+
+
+ {status.status === 'ready' ? (
+
+ ) : (
+ close()}
+ onCancel={() => {
+ onClose();
+ dispatch(clearError({ method: 'transferToken' }));
+ dispatch(setStatus({ method: 'transferToken', status: 'ready' }));
+ }}
+ onRetry={() => {
+ dispatch(clearError({ method: 'transferToken' }));
+ onSubmit();
+ }}
+ />
+ )}
+
+
+ );
+}
+
+interface TransferTokenButtonProps {
+ contractAddress: string;
+ tokenId: number;
+}
+
+export function TransferTokenButton(props: TransferTokenButtonProps) {
+ const disclosure = useDisclosure();
return (
<>
-
+
Transfer Token
-
- close()}
- initialFocusRef={initialRef}
- closeOnEsc={false}
- closeOnOverlayClick={false}
- onEsc={() => close()}
- onOverlayClick={() => close()}
- >
-
-
- {status === 'ready' ? (
-
- ) : null}
- {status === 'in_transit' ? (
-
-
-
- Transferring token...
-
-
- ) : null}
- {status === 'complete' ? (
-
-
-
-
-
- Transfer complete
-
- close()}>
- Close
-
-
- ) : null}
-
-
+
>
);
}
diff --git a/client/src/components/common/index.tsx b/client/src/components/common/index.tsx
index fab259d7..8553b3e1 100644
--- a/client/src/components/common/index.tsx
+++ b/client/src/components/common/index.tsx
@@ -4,6 +4,10 @@ import {
ButtonProps,
Link,
LinkProps,
+ MenuButton,
+ MenuButtonProps,
+ MenuItem,
+ MenuItemProps,
useStyleConfig
} from '@chakra-ui/react';
@@ -24,3 +28,19 @@ export function MinterLink(
const styles = useStyleConfig('Link', { size, variant });
return ;
}
+
+export function MinterMenuButton(
+ props: MenuButtonProps & { variant?: string }
+) {
+ const { variant, ...rest } = props;
+ const styles = useStyleConfig('MenuButton', { variant });
+ return ;
+}
+
+export function MinterMenuItem(
+ props: MenuItemProps & { variant?: string }
+) {
+ const { variant, ...rest } = props;
+ const styles = useStyleConfig('MenuItem', { variant });
+ return ;
+}
diff --git a/client/src/index.tsx b/client/src/index.tsx
index 9d1ba20e..fb661835 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -112,6 +112,19 @@ const Button = {
bg: 'brand.red',
color: 'white'
}
+ },
+ tertiaryAction: {
+ bg: 'gray.200',
+ color: 'gray.500',
+ borderRadius: '2px',
+ _hover: {
+ bg: 'gray.100',
+ color: 'gray.400'
+ },
+ _active: {
+ bg: 'gray.100',
+ color: 'gray.400'
+ }
}
}
};
@@ -205,6 +218,26 @@ const theme = extendTheme({
}
}
}
+ },
+ MenuButton: {
+ variants: {
+ primary: {
+ color: 'gray.300',
+ _hover: { color: "brand.blue" },
+ _expanded: { color: "brand.blue" },
+ _focus: { color: "brand.blue" }
+ }
+ }
+ },
+ MenuItem: {
+ variants: {
+ primary: {
+ _focus: {
+ bg: "brand.lightBlue",
+ color: "brand.blue"
+ }
+ }
+ }
}
},
fonts: {
diff --git a/client/src/lib/nfts/actions.ts b/client/src/lib/nfts/actions.ts
index c3ca4335..6a0c9c72 100644
--- a/client/src/lib/nfts/actions.ts
+++ b/client/src/lib/nfts/actions.ts
@@ -116,3 +116,74 @@ export async function transferToken(
])
.send();
}
+
+export async function listTokenForSale(
+ system: SystemWithWallet,
+ marketplaceContract: string,
+ tokenContract: string,
+ tokenId: number,
+ salePrice: number
+) {
+ const contract = await system.toolkit.wallet.at(marketplaceContract);
+ return contract.methods
+ .sell(salePrice, tokenContract, tokenId)
+ .send();
+}
+
+export async function cancelTokenSale(
+ system: SystemWithWallet,
+ marketplaceContract: string,
+ tokenContract: string,
+ tokenId: number
+) {
+ const contractM = await system.toolkit.wallet.at(marketplaceContract);
+ const contractT = await system.toolkit.wallet.at(tokenContract);
+ const batch = await system.toolkit.wallet.batch()
+ .withContractCall(contractM.methods.cancel(system.tzPublicKey, tokenContract, tokenId))
+ .withContractCall(contractT.methods.update_operators([
+ { remove_operator: { owner: system.tzPublicKey, operator: marketplaceContract, token_id: tokenId }}
+ ]));
+ return batch.send();
+}
+
+export async function approveTokenOperator(
+ system: SystemWithWallet,
+ contractAddress: string,
+ tokenId: number,
+ operatorAddress: string
+) {
+ const contract = await system.toolkit.wallet.at(contractAddress);
+ return contract.methods
+ .update_operators([
+ { add_operator: { owner: system.tzPublicKey, operator: operatorAddress, token_id: tokenId }}
+ ])
+ .send();
+}
+
+export async function removeTokenOperator(
+ system: SystemWithWallet,
+ contractAddress: string,
+ tokenId: number,
+ operatorAddress: string
+) {
+ const contract = await system.toolkit.wallet.at(contractAddress);
+ return contract.methods
+ .update_operators([
+ { remove_operator: { owner: system.tzPublicKey, operator: operatorAddress, token_id: tokenId }}
+ ])
+ .send();
+}
+
+export async function buyToken(
+ system: SystemWithWallet,
+ marketplaceContract: string,
+ tokenContract: string,
+ tokenId: number,
+ tokenSeller: string,
+ salePrice: number
+) {
+ const contract = await system.toolkit.wallet.at(marketplaceContract);
+ return contract.methods
+ .buy(tokenSeller, tokenContract, tokenId)
+ .send({ amount: salePrice });
+}
diff --git a/client/src/lib/nfts/queries.ts b/client/src/lib/nfts/queries.ts
index 9e13df00..cfac6c1f 100644
--- a/client/src/lib/nfts/queries.ts
+++ b/client/src/lib/nfts/queries.ts
@@ -12,6 +12,13 @@ function fromHexString(input: string) {
return input;
}
+interface NftSale {
+ seller: string;
+ price: number;
+ mutez: number;
+ type: string;
+}
+
export interface Nft {
id: number;
title: string;
@@ -19,6 +26,7 @@ export interface Nft {
description: string;
artifactUri: string;
metadata: Record;
+ sale?: NftSale;
}
export async function getContractNfts(
@@ -49,6 +57,10 @@ export async function getContractNfts(
if (!tokens) return [];
+ // get tokens listed for sale
+ const fixedPriceStorage = await system.betterCallDev.getContractStorage(system.config.contracts.marketplace.fixedPrice.tez);
+ const fixedPriceSales = await system.betterCallDev.getBigMapKeys(fixedPriceStorage.value);
+
return Promise.all(
tokens.map(
async (token: any): Promise => {
@@ -66,13 +78,29 @@ export async function getContractNfts(
const entry = ledger.filter((v: any) => v.data.key.value === tokenId);
const owner = select(entry, { type: 'address' })?.value;
+ const saleData = fixedPriceSales.filter((v: any) => {
+ return select(v, { name: 'token_for_sale_address' })?.value === address &&
+ select(v, { name: 'token_for_sale_token_id' })?.value === tokenId
+ });
+
+ let sale = undefined;
+ if (saleData.length > 0 && saleData[0].data.value) {
+ sale = {
+ seller: select(saleData, { name: 'sale_seller' })?.value,
+ price: Number.parseInt(saleData[0].data.value.value, 10) / 1000000,
+ mutez: Number.parseInt(saleData[0].data.value.value, 10),
+ type: 'fixedPrice'
+ };
+ }
+
return {
id: parseInt(tokenId, 10),
title: metadata.name,
owner,
description: metadata.description,
artifactUri: metadata.artifactUri,
- metadata: metadata
+ metadata: metadata,
+ sale: sale
};
}
)
diff --git a/client/src/lib/system.ts b/client/src/lib/system.ts
index a1aa2b48..a7ed5b2b 100644
--- a/client/src/lib/system.ts
+++ b/client/src/lib/system.ts
@@ -15,6 +15,11 @@ export interface Config {
};
contracts: {
nftFaucet: string;
+ marketplace: {
+ fixedPrice: {
+ tez: string;
+ }
+ }
};
}
@@ -170,15 +175,21 @@ async function initWallet(
if (!activeAccount) {
if (forceConnect) {
- await wallet.requestPermissions({
- network: {
- type:
- system.config.network === 'edo2net'
- ? (system.config.network as NetworkType)
- : network,
- rpcUrl: system.config.rpc
- }
- });
+ try {
+ await wallet.requestPermissions({
+ network: {
+ type:
+ system.config.network === 'edo2net'
+ ? (system.config.network as NetworkType)
+ : network,
+ rpcUrl: system.config.rpc
+ }
+ });
+ } catch (error) {
+ // requestPermissions failed - reset wallet selection
+ wallet.clearActiveAccount();
+ throw error;
+ }
} else {
return false;
}
diff --git a/client/src/reducer/async/actions.ts b/client/src/reducer/async/actions.ts
index d77ec99d..6ac1634d 100644
--- a/client/src/reducer/async/actions.ts
+++ b/client/src/reducer/async/actions.ts
@@ -3,7 +3,11 @@ import { State } from '..';
import {
createAssetContract,
mintToken,
- transferToken
+ transferToken,
+ listTokenForSale,
+ cancelTokenSale,
+ approveTokenOperator,
+ buyToken
} from '../../lib/nfts/actions';
// import {getNftAssetContract} from '../../lib/nfts/queries'
import { ErrorKind, RejectValue } from './errors';
@@ -14,6 +18,7 @@ import {
uploadIPFSImageWithThumbnail
} from '../../lib/util/ipfs';
import { SelectedFile } from '../slices/createNft';
+import { connectWallet } from './wallet';
type Options = {
state: State;
@@ -186,7 +191,7 @@ export const mintTokenAction = createAsyncThunk<
try {
const op = await mintToken(system, address, metadata);
- await op.confirmation();
+ await op.confirmation(2);
dispatch(getContractNftsQuery(address));
return { contract: address };
} catch (e) {
@@ -213,7 +218,7 @@ export const transferTokenAction = createAsyncThunk<
}
try {
const op = await transferToken(system, contract, tokenId, to);
- await op.confirmation();
+ await op.confirmation(2);
dispatch(getContractNftsQuery(contract));
return { contract: '', tokenId: 0 };
} catch (e) {
@@ -223,3 +228,93 @@ export const transferTokenAction = createAsyncThunk<
});
}
});
+
+export const listTokenAction = createAsyncThunk<
+ { contract: string; tokenId: number, salePrice: number },
+ { contract: string; tokenId: number, salePrice: number },
+ Options
+>('actions/listToken', async (args, api) => {
+ const { getState, rejectWithValue, dispatch } = api;
+ const { contract, tokenId, salePrice } = args;
+ const { system } = getState();
+ const marketplaceContract = system.config.contracts.marketplace.fixedPrice.tez;
+ if (system.status !== 'WalletConnected') {
+ return rejectWithValue({
+ kind: ErrorKind.WalletNotConnected,
+ message: 'Could not list token: no wallet connected'
+ });
+ }
+ try {
+ const op1 = await approveTokenOperator(system, contract, tokenId, marketplaceContract);
+ await op1.confirmation();
+ const op2 = await listTokenForSale(system, marketplaceContract, contract, tokenId, salePrice);
+ await op2.confirmation(2);
+ dispatch(getContractNftsQuery(contract));
+ return { contract: contract, tokenId: tokenId, salePrice: salePrice };
+ } catch (e) {
+ return rejectWithValue({
+ kind: ErrorKind.ListTokenFailed,
+ message: 'List token failed'
+ });
+ }
+});
+
+export const cancelTokenSaleAction = createAsyncThunk<
+ { contract: string; tokenId: number },
+ { contract: string; tokenId: number },
+ Options
+>('actions/cancelTokenSale', async (args, api) => {
+ const { getState, rejectWithValue, dispatch } = api;
+ const { contract, tokenId } = args;
+ const { system } = getState();
+ const marketplaceContract = system.config.contracts.marketplace.fixedPrice.tez;
+ if (system.status !== 'WalletConnected') {
+ return rejectWithValue({
+ kind: ErrorKind.WalletNotConnected,
+ message: 'Could not list token: no wallet connected'
+ });
+ }
+ try {
+ const op = await cancelTokenSale(system, marketplaceContract, contract, tokenId);
+ await op.confirmation(2);
+ dispatch(getContractNftsQuery(contract));
+ return { contract: contract, tokenId: tokenId };
+ } catch (e) {
+ return rejectWithValue({
+ kind: ErrorKind.CancelTokenSaleFailed,
+ message: 'Cancel token sale failed'
+ });
+ }
+});
+
+export const buyTokenAction = createAsyncThunk<
+ { contract: string; tokenId: number },
+ { contract: string; tokenId: number, tokenSeller: string; salePrice: number },
+ Options
+>('actions/buyToken', async (args, api) => {
+ const { getState, rejectWithValue, dispatch } = api;
+ const { contract, tokenId, tokenSeller, salePrice } = args;
+ let { system } = getState();
+ const marketplaceContract = system.config.contracts.marketplace.fixedPrice.tez;
+ if (system.status !== 'WalletConnected') {
+ const res = await dispatch(connectWallet());
+ if (!res.payload || !("wallet" in res.payload)) {
+ return rejectWithValue({
+ kind: ErrorKind.WalletNotConnected,
+ message: 'Could not list token: no wallet connected'
+ });
+ }
+ system = res.payload;
+ }
+ try {
+ const op = await buyToken(system, marketplaceContract, contract, tokenId, tokenSeller, salePrice);
+ await op.confirmation(2);
+ dispatch(getContractNftsQuery(contract));
+ return { contract: contract, tokenId: tokenId };
+ } catch (e) {
+ return rejectWithValue({
+ kind: ErrorKind.BuyTokenFailed,
+ message: 'Purchase token failed'
+ });
+ }
+});
diff --git a/client/src/reducer/async/errors.ts b/client/src/reducer/async/errors.ts
index 6bb5c320..8820e75d 100644
--- a/client/src/reducer/async/errors.ts
+++ b/client/src/reducer/async/errors.ts
@@ -5,6 +5,9 @@ export enum ErrorKind {
CreateNftFormInvalid,
MintTokenFailed,
TransferTokenFailed,
+ ListTokenFailed,
+ CancelTokenSaleFailed,
+ BuyTokenFailed,
GetNftAssetContractFailed,
GetContractNftsFailed,
GetWalletNftAssetContractsFailed,
diff --git a/client/src/reducer/slices/notifications.ts b/client/src/reducer/slices/notifications.ts
index 98c46ccb..998b314c 100644
--- a/client/src/reducer/slices/notifications.ts
+++ b/client/src/reducer/slices/notifications.ts
@@ -2,7 +2,10 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import {
createAssetContractAction,
mintTokenAction,
- transferTokenAction
+ transferTokenAction,
+ listTokenAction,
+ cancelTokenSaleAction,
+ buyTokenAction
} from '../async/actions';
import { connectWallet, disconnectWallet } from '../async/wallet';
import {
@@ -52,6 +55,9 @@ const slice = createSlice({
createAssetContractAction,
mintTokenAction,
transferTokenAction,
+ listTokenAction,
+ cancelTokenSaleAction,
+ buyTokenAction,
getContractNftsQuery,
getNftAssetContractQuery,
getWalletAssetContractsQuery,
diff --git a/client/src/reducer/slices/status.ts b/client/src/reducer/slices/status.ts
index 3e9c9207..6741ac41 100644
--- a/client/src/reducer/slices/status.ts
+++ b/client/src/reducer/slices/status.ts
@@ -2,7 +2,10 @@ import { createSlice, PayloadAction, SerializedError } from '@reduxjs/toolkit';
import {
createAssetContractAction,
mintTokenAction,
- transferTokenAction
+ transferTokenAction,
+ listTokenAction,
+ cancelTokenSaleAction,
+ buyTokenAction
} from '../async/actions';
import {
getContractNftsQuery,
@@ -13,7 +16,7 @@ import { ErrorKind, RejectValue } from '../async/errors';
export type StatusKey = 'ready' | 'in_transit' | 'complete';
-interface Status {
+export interface Status {
status: StatusKey;
error: {
rejectValue: RejectValue;
@@ -25,6 +28,9 @@ export interface StatusState {
createAssetContract: Status;
mintToken: Status;
transferToken: Status;
+ listToken: Status;
+ cancelTokenSale: Status;
+ buyToken: Status;
getContractNfts: Status;
getNftAssetContract: Status;
getWalletAssetContracts: Status;
@@ -38,6 +44,9 @@ const initialState: StatusState = {
createAssetContract: defaultStatus,
mintToken: defaultStatus,
transferToken: defaultStatus,
+ listToken: defaultStatus,
+ cancelTokenSale: defaultStatus,
+ buyToken: defaultStatus,
getContractNfts: defaultStatus,
getNftAssetContract: defaultStatus,
getWalletAssetContracts: defaultStatus
@@ -66,6 +75,9 @@ const slice = createSlice({
methodMap('createAssetContract', createAssetContractAction),
methodMap('mintToken', mintTokenAction),
methodMap('transferToken', transferTokenAction),
+ methodMap('listToken', listTokenAction),
+ methodMap('cancelTokenSale', cancelTokenSaleAction),
+ methodMap('buyToken', buyTokenAction),
methodMap('getContractNfts', getContractNftsQuery),
methodMap('getNftAssetContract', getNftAssetContractQuery),
methodMap('getWalletAssetContracts', getWalletAssetContractsQuery)
@@ -77,17 +89,14 @@ const slice = createSlice({
state[method].status = 'complete';
});
addCase(action.rejected, (state, action) => {
- if (action.payload) {
- state[method].error = {
- rejectValue: action.payload,
- serialized: action.error
- };
- }
+ const rejectValue = action.payload
+ ? action.payload
+ : {
+ kind: ErrorKind.UknownError,
+ message: 'Unknown error'
+ };
state[method].error = {
- rejectValue: {
- kind: ErrorKind.UknownError,
- message: 'Unknown error'
- },
+ rejectValue,
serialized: action.error
};
});
diff --git a/client/yarn.lock b/client/yarn.lock
index 993347e6..4ad0c44d 100644
--- a/client/yarn.lock
+++ b/client/yarn.lock
@@ -2248,77 +2248,77 @@
"@svgr/plugin-svgo" "^4.3.1"
loader-utils "^1.2.3"
-"@taquito/beacon-wallet@8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/beacon-wallet/-/beacon-wallet-8.0.3-beta.0.tgz#1a6debe79160318991fc26687d75457550b4ec01"
- integrity sha512-XIHfgLxMCHQWnuhXNoCDKKExzbCuMMH4dyY1gd7IKM1cHtHVf6WkiMzUh9ulKm8G94VhPRNyrynVlw10iAqmuw==
+"@taquito/beacon-wallet@8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/beacon-wallet/-/beacon-wallet-8.0.4-beta.0.tgz#2e31c63a2288e0eca99ac44bbba741c1c7cb57af"
+ integrity sha512-XJ34C34YSW94d2lkOpEZbcPL1JV4jw3oX3E5sT3B4vdIrfkDh1hLU5dtYOWFAV3YMvpAoR23coCt2fsuo6aYrw==
dependencies:
"@airgap/beacon-sdk" "^2.2.1"
- "@taquito/taquito" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/taquito" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
-"@taquito/http-utils@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/http-utils/-/http-utils-8.0.3-beta.0.tgz#aa26b286cb57df52d8eb18d2235302ad4f52e75e"
- integrity sha512-9z3terTHUvFATleeD65ULRwqSsK10f/c1jZ1aJCnOgFkGdZ+8SHKFFu0MzY5rXRMwIE0yNQR+uqauFLZ970HUQ==
+"@taquito/http-utils@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/http-utils/-/http-utils-8.0.4-beta.0.tgz#12b38bdcd9b406eaadec7ad2264fa446e0a8919c"
+ integrity sha512-yoPrvkZDWZ/25w3VuTKaUMFwnjPEosD4sMrwOwrT8ZC6ZkHzewB6oVyGb9fPviRALzQH4Wj2jV0Gtmnn9FDsDg==
dependencies:
xhr2-cookies "^1.1.0"
-"@taquito/michel-codec@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/michel-codec/-/michel-codec-8.0.3-beta.0.tgz#7b5be2f371885fea01864950648ec37bcae580b4"
- integrity sha512-ike4TTHsRMdg6iE0hypqNyz94x2I24yySdVHU4t19kxuzEJRJDns0SskplveJ5fIlAO7TX/cCJqjumBLIaprVg==
+"@taquito/michel-codec@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/michel-codec/-/michel-codec-8.0.4-beta.0.tgz#84f723cba4f1d27af7ce20903f5a76287a72b419"
+ integrity sha512-cWxewE64vMKRXPeWnanLsYXWYCO2aHiOgsUTdwtFLy9AMgGLOcK04cVQQKmNePWX8NRNJFNDwNUKDvSCKVRxOw==
-"@taquito/michelson-encoder@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/michelson-encoder/-/michelson-encoder-8.0.3-beta.0.tgz#e8919e2b0f0f14edfa03205551c2f30ec4b950c1"
- integrity sha512-CloKcxh57rcrFazSAscOIrKdlcfO0rVXVoAdIVNfWYyWeEhkRZkSySSPLCYiws8MC9jxUlQYvQo/lT+xGA/gzQ==
+"@taquito/michelson-encoder@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/michelson-encoder/-/michelson-encoder-8.0.4-beta.0.tgz#ad078fe49655103a24b109cb17cc2f402486d009"
+ integrity sha512-2iR50/kJVmP0x43OHpPtP6sETtbKItE1EbeGP5eWvlMxUmZxF2k6cQZVCDyfnSIhwKvC+EWaaSfljy9qYWYU5w==
dependencies:
- "@taquito/rpc" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/rpc" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
fast-json-stable-stringify "^2.1.0"
-"@taquito/rpc@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/rpc/-/rpc-8.0.3-beta.0.tgz#801d352263d97510fc90897eb0b132d214f235c6"
- integrity sha512-jrfOR8+RzK9I1VA8VMXJsoy/SGKlO4jN/DFaJHEYZ+KO+woN1bRoOhKKLR15mxe4Zq+P0/Pu+dH1foJn+GbR0w==
+"@taquito/rpc@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/rpc/-/rpc-8.0.4-beta.0.tgz#1e8db49976db17392624893c3cb306fa288c7514"
+ integrity sha512-3L7yaANVJfFJtpcYpZF5JVS9Utrnicyu0hG94gDJlVlugomDRIaURK3tOQ38DF6fus14BHKjOBxChuAp9hQ8gw==
dependencies:
- "@taquito/http-utils" "^8.0.3-beta.0"
+ "@taquito/http-utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
lodash "^4.17.20"
-"@taquito/taquito@8.0.3-beta.0", "@taquito/taquito@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/taquito/-/taquito-8.0.3-beta.0.tgz#c68867ff8145753d8a95b6a69c8a03c0ccc3f10c"
- integrity sha512-IzRoX6JtBvu4WG1m++345I0fZo7ZZ94jfXl1DI7by9vgWpXKt2FM2GCFx1+hpyLdoKxRhZR5+BpNBHbnBXON4w==
+"@taquito/taquito@8.0.4-beta.0", "@taquito/taquito@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/taquito/-/taquito-8.0.4-beta.0.tgz#6cae5116869b55a0ec22173cd0a6b779e03bf9c3"
+ integrity sha512-Z/+7unJ66AX5Wgni4rtiHoxM87dq0Y+pJh2kPLtB3qwmUbd1TyKwSykxJ2ACo2aj35Q426TlDIwV6NCYpquaLw==
dependencies:
- "@taquito/http-utils" "^8.0.3-beta.0"
- "@taquito/michel-codec" "^8.0.3-beta.0"
- "@taquito/michelson-encoder" "^8.0.3-beta.0"
- "@taquito/rpc" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/http-utils" "^8.0.4-beta.0"
+ "@taquito/michel-codec" "^8.0.4-beta.0"
+ "@taquito/michelson-encoder" "^8.0.4-beta.0"
+ "@taquito/rpc" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
rx-sandbox "^1.0.3"
rxjs "^6.6.3"
-"@taquito/tzip16@8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/tzip16/-/tzip16-8.0.3-beta.0.tgz#5c7b156248267aca7406c219ba24cc455a245895"
- integrity sha512-VnP8ZbtUxzKR4jZ3wfN/HbD0PuBzGqbPEtcg+RHDjl+nCeEt533SOyRKZSUkz1WBMjn5qr1s1768bLkACar/xQ==
+"@taquito/tzip16@8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/tzip16/-/tzip16-8.0.4-beta.0.tgz#a12497f1854c890bb1fae2ce0427774b7630356b"
+ integrity sha512-Rd/JnJgHTkrJYCr/2IKg5Fjv6qNHXDU6t9ol68d3pj175gW56C6tjNOeVi26S0C3LWyg9ro0O9QkEe9IgumaAA==
dependencies:
- "@taquito/http-utils" "^8.0.3-beta.0"
- "@taquito/michelson-encoder" "^8.0.3-beta.0"
- "@taquito/rpc" "^8.0.3-beta.0"
- "@taquito/taquito" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/http-utils" "^8.0.4-beta.0"
+ "@taquito/michelson-encoder" "^8.0.4-beta.0"
+ "@taquito/rpc" "^8.0.4-beta.0"
+ "@taquito/taquito" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
crypto-js "^4.0.0"
-"@taquito/utils@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/utils/-/utils-8.0.3-beta.0.tgz#55a21f2dca39d227e1c623af3fb97f5e75aa45cb"
- integrity sha512-59EmJGNSTRZ2wfFxu2EjzJ3BnLnRFC0PUFiey+KyFYXF4Z6TUZTo8KFRM6CNhpeGh3vfWPU+hdsRd+MO4DeNWA==
+"@taquito/utils@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/utils/-/utils-8.0.4-beta.0.tgz#13f0e3bad5ca8099a66c626a601d974bc55e961f"
+ integrity sha512-pOX1majqHppzS3p3YSkRx+juTHND+DEtchVXzMUnUPGgAK2uG+qxEQKO3QItCfK8A/51S2cVPZ801lHiCkbVeA==
dependencies:
blakejs "^1.1.0"
bs58check "^2.1.2"
diff --git a/package.json b/package.json
index 2914c80f..0ed63bca 100644
--- a/package.json
+++ b/package.json
@@ -17,11 +17,11 @@
"bootstrap": "ts-node -P scripts/tsconfig.json scripts/bootstrap-contracts-config.ts"
},
"devDependencies": {
+ "@taquito/signer": "8.0.4-beta.0",
+ "@taquito/taquito": "8.0.4-beta.0",
+ "@tsed/logger": "5.5.2",
"@types/async-retry": "^1.4.2",
"@types/configstore": "^4.0.0",
- "@taquito/signer": "8.0.3-beta.0",
- "@taquito/taquito": "8.0.3-beta.0",
- "@tsed/logger": "5.5.2",
"async-retry": "^1.3.1",
"axios": "0.21.1",
"configstore": "^5.0.1",
diff --git a/scripts/bootstrap-contracts-config.ts b/scripts/bootstrap-contracts-config.ts
index 2b019773..6382e864 100644
--- a/scripts/bootstrap-contracts-config.ts
+++ b/scripts/bootstrap-contracts-config.ts
@@ -6,7 +6,28 @@ import retry from 'async-retry';
import Configstore from 'configstore';
import { MichelsonMap, TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';
+import { OriginateParams } from '@taquito/taquito/dist/types/operations/types';
import { OriginationOperation } from '@taquito/taquito/dist/types/operations/origination-operation';
+import { ContractAbstraction } from '@taquito/taquito/dist/types/contract';
+import { ContractProvider } from '@taquito/taquito/dist/types/contract/interface';
+
+type Contract = ContractAbstraction;
+
+interface BoostrapStorageCallback {
+ (): object
+}
+
+interface BootstrapContractParams {
+ configKey: string;
+ contractFilename: string;
+ contractAlias: string;
+ initStorage: BoostrapStorageCallback;
+}
+
+interface ContractCodeResponse {
+ code: string;
+ url: string;
+}
// Client & Server Config Generation
@@ -34,10 +55,7 @@ function toHexString(input: string) {
return Buffer.from(input).toString('hex');
}
-export async function originateNftFaucet(
- toolkit: TezosToolkit,
- code: string
-): Promise {
+export function initStorageNftFaucet() {
const metadata = new MichelsonMap();
const contents = {
name: 'Minter',
@@ -47,43 +65,35 @@ export async function originateNftFaucet(
};
metadata.set('', toHexString('tezos-storage:contents'));
metadata.set('contents', toHexString(JSON.stringify(contents)));
- return await toolkit.contract.originate({
- code: code,
- storage: {
- assets: {
- ledger: new MichelsonMap(),
- next_token_id: 0,
- operators: new MichelsonMap(),
- token_metadata: new MichelsonMap()
- },
- metadata: metadata
- }
- });
+ return {
+ assets: {
+ ledger: new MichelsonMap(),
+ next_token_id: 0,
+ operators: new MichelsonMap(),
+ token_metadata: new MichelsonMap()
+ },
+ metadata: metadata
+ };
}
-async function exitOnExistingBootstrap(
+async function getContractAddress(
config: Configstore,
toolkit: TezosToolkit,
configKey: string
-): Promise {
- const address = config.get(configKey);
- if (!address) return;
-
- try {
- await toolkit.contract.at(address);
- $log.info(
- `Contract already exists at address ${address}. Skipping origination`
- );
- process.exit(0);
- } catch (e) {
- return;
- }
+): Promise {
+ const existingAddress = config.get(configKey);
+ if (!existingAddress) return "";
+
+ return toolkit.contract
+ .at(existingAddress)
+ .then(() => existingAddress)
+ .catch(() => "");
}
-async function fetchFaucetContractCode() {
+async function fetchContractCode(contractFilename: string): Promise {
const rawRepoUrl = 'https://raw.githubusercontent.com/tqtezos/minter-sdk';
const gitHash = '8f67bb8c2abc12b8e6f8e529e1412262972deab3';
- const contractCodeUrl = `${rawRepoUrl}/${gitHash}/contracts/bin/fa2_multi_nft_faucet.tz`;
+ const contractCodeUrl = `${rawRepoUrl}/${gitHash}/contracts/bin/${contractFilename}`;
const response = await axios.get(contractCodeUrl);
return { code: response.data, url: contractCodeUrl };
}
@@ -122,50 +132,80 @@ function readEnv(): string {
return env;
}
-async function bootstrap(env: string) {
- $log.info(`Bootstrapping ${env} environment config...`);
- const configKey = 'contracts.nftFaucet';
- const config = readConfig(env);
- const toolkit = await createToolkit(config);
-
- $log.info('Connecting to network...');
- await waitForNetwork(toolkit);
- $log.info('Connected');
-
- // Exit the script if the contract address defined in the configuration
- // already exists on chain
- await exitOnExistingBootstrap(config, toolkit, configKey);
+async function bootstrapContract(
+ config: Configstore,
+ toolkit: TezosToolkit,
+ params: BootstrapContractParams
+): Promise {
+ const address = await getContractAddress(config, toolkit, params.configKey);
+ if (address) {
+ $log.info(
+ `${params.contractAlias} contract already exists at address ${address}. Skipping origination.`
+ );
+ return;
+ }
let contract;
try {
- const { code, url: contractCodeUrl } = await fetchFaucetContractCode();
+ const { code, url: contractCodeUrl } = await fetchContractCode(params.contractFilename);
- $log.info(`Originating contract from ${contractCodeUrl} ...`);
+ $log.info(`Originating ${params.contractAlias} contract from ${contractCodeUrl} ...`);
- const origOp = await originateNftFaucet(toolkit, code);
+ const storage = params.initStorage();
+ const origOp = await toolkit.contract.originate({
+ code: code,
+ storage: storage
+ });
+
contract = await origOp.contract();
- $log.info(`Originated nftFaucet contract at address ${contract.address}`);
+ $log.info(`Originated ${params.contractAlias} contract at address ${contract.address}`);
$log.info(` Consumed gas: ${origOp.consumedGas}`);
} catch (error) {
const jsonError = JSON.stringify(error, null, 2);
- $log.error(`nftFaucet origination error ${jsonError}`);
+ $log.error(`${params.contractAlias} origination error ${jsonError}`);
process.exit(1);
}
- config.set(configKey, contract.address);
+ config.set(params.configKey, contract.address);
$log.info(`Updated configuration`);
+}
+
+async function bootstrap(env: string) {
+ $log.info(`Bootstrapping ${env} environment config...`);
+ const configKey = 'contracts.nftFaucet';
+ const config = readConfig(env);
+ const toolkit = await createToolkit(config);
+
+ $log.info('Connecting to network...');
+ await waitForNetwork(toolkit);
+ $log.info('Connected');
+
+ // bootstrap NFT faucet
+ await bootstrapContract(config, toolkit, {
+ configKey: 'contracts.nftFaucet',
+ contractAlias: 'nftFaucet',
+ contractFilename: 'fa2_multi_nft_faucet.tz',
+ initStorage: initStorageNftFaucet
+ });
+
+ // bootstrap marketplace fixed price (tez)
+ await bootstrapContract(config, toolkit, {
+ configKey: 'contracts.marketplace.fixedPrice.tez',
+ contractAlias: 'fixedPriceMarketTez',
+ contractFilename: 'fixed_price_sale_market_tez.tz',
+ initStorage: (() => new MichelsonMap())
+ });
genClientConfig(config);
genServerConfig(config);
-
- process.exit(0);
}
async function main() {
const env = readEnv();
try {
await bootstrap(env);
+ process.exit(0);
} catch (err) {
$log.error(`Error while bootstrapping environment ${env}`);
$log.error(err);
diff --git a/yarn.lock b/yarn.lock
index a79d5f59..73e9d6a7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -34,44 +34,44 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
-"@taquito/http-utils@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/http-utils/-/http-utils-8.0.3-beta.0.tgz#aa26b286cb57df52d8eb18d2235302ad4f52e75e"
- integrity sha512-9z3terTHUvFATleeD65ULRwqSsK10f/c1jZ1aJCnOgFkGdZ+8SHKFFu0MzY5rXRMwIE0yNQR+uqauFLZ970HUQ==
+"@taquito/http-utils@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/http-utils/-/http-utils-8.0.4-beta.0.tgz#12b38bdcd9b406eaadec7ad2264fa446e0a8919c"
+ integrity sha512-yoPrvkZDWZ/25w3VuTKaUMFwnjPEosD4sMrwOwrT8ZC6ZkHzewB6oVyGb9fPviRALzQH4Wj2jV0Gtmnn9FDsDg==
dependencies:
xhr2-cookies "^1.1.0"
-"@taquito/michel-codec@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/michel-codec/-/michel-codec-8.0.3-beta.0.tgz#7b5be2f371885fea01864950648ec37bcae580b4"
- integrity sha512-ike4TTHsRMdg6iE0hypqNyz94x2I24yySdVHU4t19kxuzEJRJDns0SskplveJ5fIlAO7TX/cCJqjumBLIaprVg==
+"@taquito/michel-codec@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/michel-codec/-/michel-codec-8.0.4-beta.0.tgz#84f723cba4f1d27af7ce20903f5a76287a72b419"
+ integrity sha512-cWxewE64vMKRXPeWnanLsYXWYCO2aHiOgsUTdwtFLy9AMgGLOcK04cVQQKmNePWX8NRNJFNDwNUKDvSCKVRxOw==
-"@taquito/michelson-encoder@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/michelson-encoder/-/michelson-encoder-8.0.3-beta.0.tgz#e8919e2b0f0f14edfa03205551c2f30ec4b950c1"
- integrity sha512-CloKcxh57rcrFazSAscOIrKdlcfO0rVXVoAdIVNfWYyWeEhkRZkSySSPLCYiws8MC9jxUlQYvQo/lT+xGA/gzQ==
+"@taquito/michelson-encoder@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/michelson-encoder/-/michelson-encoder-8.0.4-beta.0.tgz#ad078fe49655103a24b109cb17cc2f402486d009"
+ integrity sha512-2iR50/kJVmP0x43OHpPtP6sETtbKItE1EbeGP5eWvlMxUmZxF2k6cQZVCDyfnSIhwKvC+EWaaSfljy9qYWYU5w==
dependencies:
- "@taquito/rpc" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/rpc" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
fast-json-stable-stringify "^2.1.0"
-"@taquito/rpc@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/rpc/-/rpc-8.0.3-beta.0.tgz#801d352263d97510fc90897eb0b132d214f235c6"
- integrity sha512-jrfOR8+RzK9I1VA8VMXJsoy/SGKlO4jN/DFaJHEYZ+KO+woN1bRoOhKKLR15mxe4Zq+P0/Pu+dH1foJn+GbR0w==
+"@taquito/rpc@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/rpc/-/rpc-8.0.4-beta.0.tgz#1e8db49976db17392624893c3cb306fa288c7514"
+ integrity sha512-3L7yaANVJfFJtpcYpZF5JVS9Utrnicyu0hG94gDJlVlugomDRIaURK3tOQ38DF6fus14BHKjOBxChuAp9hQ8gw==
dependencies:
- "@taquito/http-utils" "^8.0.3-beta.0"
+ "@taquito/http-utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
lodash "^4.17.20"
-"@taquito/signer@8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/signer/-/signer-8.0.3-beta.0.tgz#bb479050b70202ad4f5d1c1796404bb7f7daf807"
- integrity sha512-4SS1vBU7Hw9NcEGfdptl6iMWdS83wokAGMwl8ADY2J6UawAoW05DaWcAE/372G9obBzfYlCCgV0QhTgIwvOKWw==
+"@taquito/signer@8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/signer/-/signer-8.0.4-beta.0.tgz#1cb38651f1a6e9fe854f332798a0939464b05b2b"
+ integrity sha512-5GLYYp8Iojx6e1q80cDQ3E6gfXi1o072/fXqk9QM5LhB/buxPDaiuVy4hVo9O9tTud4BiGnNEQU9Ov/f7h9iuA==
dependencies:
- "@taquito/taquito" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/taquito" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
bip39 "^3.0.2"
elliptic "^6.5.3"
@@ -79,24 +79,24 @@
pbkdf2 "^3.1.1"
typedarray-to-buffer "^3.1.5"
-"@taquito/taquito@8.0.3-beta.0", "@taquito/taquito@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/taquito/-/taquito-8.0.3-beta.0.tgz#c68867ff8145753d8a95b6a69c8a03c0ccc3f10c"
- integrity sha512-IzRoX6JtBvu4WG1m++345I0fZo7ZZ94jfXl1DI7by9vgWpXKt2FM2GCFx1+hpyLdoKxRhZR5+BpNBHbnBXON4w==
+"@taquito/taquito@8.0.4-beta.0", "@taquito/taquito@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/taquito/-/taquito-8.0.4-beta.0.tgz#6cae5116869b55a0ec22173cd0a6b779e03bf9c3"
+ integrity sha512-Z/+7unJ66AX5Wgni4rtiHoxM87dq0Y+pJh2kPLtB3qwmUbd1TyKwSykxJ2ACo2aj35Q426TlDIwV6NCYpquaLw==
dependencies:
- "@taquito/http-utils" "^8.0.3-beta.0"
- "@taquito/michel-codec" "^8.0.3-beta.0"
- "@taquito/michelson-encoder" "^8.0.3-beta.0"
- "@taquito/rpc" "^8.0.3-beta.0"
- "@taquito/utils" "^8.0.3-beta.0"
+ "@taquito/http-utils" "^8.0.4-beta.0"
+ "@taquito/michel-codec" "^8.0.4-beta.0"
+ "@taquito/michelson-encoder" "^8.0.4-beta.0"
+ "@taquito/rpc" "^8.0.4-beta.0"
+ "@taquito/utils" "^8.0.4-beta.0"
bignumber.js "^9.0.1"
rx-sandbox "^1.0.3"
rxjs "^6.6.3"
-"@taquito/utils@^8.0.3-beta.0":
- version "8.0.3-beta.0"
- resolved "https://registry.yarnpkg.com/@taquito/utils/-/utils-8.0.3-beta.0.tgz#55a21f2dca39d227e1c623af3fb97f5e75aa45cb"
- integrity sha512-59EmJGNSTRZ2wfFxu2EjzJ3BnLnRFC0PUFiey+KyFYXF4Z6TUZTo8KFRM6CNhpeGh3vfWPU+hdsRd+MO4DeNWA==
+"@taquito/utils@^8.0.4-beta.0":
+ version "8.0.4-beta.0"
+ resolved "https://registry.yarnpkg.com/@taquito/utils/-/utils-8.0.4-beta.0.tgz#13f0e3bad5ca8099a66c626a601d974bc55e961f"
+ integrity sha512-pOX1majqHppzS3p3YSkRx+juTHND+DEtchVXzMUnUPGgAK2uG+qxEQKO3QItCfK8A/51S2cVPZ801lHiCkbVeA==
dependencies:
blakejs "^1.1.0"
bs58check "^2.1.2"