diff --git a/README.md b/README.md
index 0e7fbfa7..bf2c0468 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,7 @@ OpenMinter supports the following networks and software components:
#### 🎨 Multimedia NFTs powered by [TZIP-21](https://tzip.tezosagora.org/proposal/tzip-21/)
#### ⚙️ Smart contracts based on [minter-sdk](https://github.com/tqtezos/minter-sdk)
#### 👛 Wallets compatible with [Beacon](https://www.walletbeacon.io/)
-#### 📖 Indexing via [Better Call Dev][bcdhub]
+#### 📖 Indexing via [tzkt][https://api.tzkt.io/]
#### 🚀 [IPFS](https://ipfs.io/) via a local node or [Pinata](https://pinata.cloud/)
The following dependencies are required to run OpenMinter.
@@ -96,8 +96,7 @@ To install and build the dependences required for local development, run:
$ yarn install
```
-The installation process will fetch toplevel NPM dependences and build
-the `minter-ui-dev` and `minter-api-dev` Docker images.
+The installation process will fetch toplevel NPM dependences
### Running
@@ -113,6 +112,32 @@ To run OpenMinter configured for `mainnet`, run:
yarn start:mainnet
```
+### Bootstrapping Your Own Contracts
+
+OpenMinter ships with a set of contracts on mainnet and testnet that are intended
+only as a reference implementation and demo. In most cases, you will want to
+originate your own contracts to run OpenMinter. OpenMinter includes a configuration
+wizard CLI tool to make this process quick and easy.
+
+To start the configuration wizard, run:
+
+```sh
+yarn bootstrap
+```
+
+Once the configuration wizard is complete, you can run OpenMinter with your
+custom config by running:
+
+```sh
+yarn start:custom
+```
+
+And to build OpenMinter for a production deployment with your custom config, run:
+
+```sh
+yarn build:custom
+```
+
## Sandboxed Mode
Sandboxed mode is available for OpenMinter for local testing purposes. Make sure
diff --git a/config/mainnet.json b/config/mainnet.json
index 65fb36a6..7a5c3563 100644
--- a/config/mainnet.json
+++ b/config/mainnet.json
@@ -5,6 +5,9 @@
"api": "https://api.better-call.dev",
"gui": "https://better-call.dev"
},
+ "tzkt": {
+ "api": "https://staging.api.mainnet.tzkt.io"
+ },
"contracts": {
"nftFaucet": "KT1QcxwB4QyPKfmSwjH1VRxa6kquUjeDWeEy",
"marketplace": {
@@ -13,5 +16,6 @@
}
}
},
- "ipfsApi": "https://minter-api.tqhosted.com"
-}
+ "ipfsApi": "https://minter-api.tqhosted.com",
+ "ipfsGateway": "https://tqtezos.mypinata.cloud"
+}
\ No newline at end of file
diff --git a/config/sandbox.json b/config/sandbox.json
index 0ccd8e68..45263c9c 100644
--- a/config/sandbox.json
+++ b/config/sandbox.json
@@ -17,5 +17,6 @@
}
}
},
- "ipfsApi": "http://localhost:3300"
+ "ipfsApi": "http://localhost:3300",
+ "ipfsGateway": "http://localhost:8080"
}
\ No newline at end of file
diff --git a/config/testnet.json b/config/testnet.json
index 142ba66c..98061180 100644
--- a/config/testnet.json
+++ b/config/testnet.json
@@ -5,6 +5,9 @@
"api": "https://api.better-call.dev",
"gui": "https://better-call.dev"
},
+ "tzkt": {
+ "api": "https://staging.api.edo2net.tzkt.io"
+ },
"contracts": {
"nftFaucet": "KT1Hagc5XQYzkX7HfRbUNXdi4CBfiENzbHiU",
"marketplace": {
@@ -13,5 +16,6 @@
}
}
},
- "ipfsApi": "https://minter-api.tqhosted.com"
-}
+ "ipfsApi": "https://minter-api.tqhosted.com",
+ "ipfsGateway": "https://tqtezos.mypinata.cloud"
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 2c8296a9..e0efb8b0 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,8 @@
{
"name": "client",
- "version": "0.2.0",
+ "version": "0.7.0",
"private": true,
+ "license": "MIT",
"dependencies": {
"@chakra-ui/react": "1.1.2",
"@emotion/core": "10.0.28",
@@ -11,9 +12,9 @@
"@taquito/beacon-wallet": "8.1.0",
"@taquito/signer": "8.1.0",
"@taquito/taquito": "8.1.0",
- "@taquito/tzip16": "8.1.0",
"@taquito/tzip12": "8.1.0",
- "@tqtezos/minter-contracts": "1.0.3",
+ "@taquito/tzip16": "8.1.0",
+ "@tqtezos/minter-contracts": "1.2.0",
"@types/lodash": "4.14.165",
"@types/react": "16.9.12",
"@types/react-dom": "16.9.0",
@@ -21,8 +22,13 @@
"@types/react-redux": "7.1.16",
"axios-retry": "3.1.9",
"buffer": "6.0.3",
+ "clear": "0.1.0",
+ "clui": "0.3.6",
+ "figlet": "1.5.0",
+ "fp-ts": "2.10.3",
"framer-motion": "3.1.4",
"immer": "8.0.0",
+ "io-ts": "2.2.16",
"joi": "17.3.0",
"react": "16.13.1",
"react-dom": "16.13.1",
@@ -30,6 +36,7 @@
"react-feather": "2.0.9",
"react-icons": "4.2.0",
"react-redux": "7.2.2",
+ "shelljs": "0.8.4",
"typescript": "4.1.3",
"wouter": "2.5.1"
},
@@ -39,8 +46,13 @@
"@testing-library/user-event": "7.1.2",
"@tsed/logger": "5.5.2",
"@types/async-retry": "1.4.2",
+ "@types/clear": "0.1.1",
+ "@types/clui": "0.3.0",
"@types/configstore": "4.0.0",
+ "@types/figlet": "1.5.1",
+ "@types/inquirer": "7.3.1",
"@types/jest": "24.0.0",
+ "@types/shelljs": "0.8.8",
"async-retry": "1.3.1",
"axios": "0.21.1",
"configstore": "5.0.1",
@@ -53,10 +65,14 @@
"start:testnet": "cp ./config/testnet.json src/config.json && react-scripts start",
"start:sandbox": "cp ./config/sandbox-bootstrapped.json src/config.json && react-scripts start",
"start:mainnet": "cp ./config/mainnet.json src/config.json && react-scripts start",
+ "start:custom": "cp ./config/custom-bootstrapped.json src/config.json && react-scripts start",
"build": "react-scripts build",
+ "build:custom": "cp ./config/custom-bootstrapped.json src/config.json && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
+ "bootstrap": "ts-node -P scripts/tsconfig.json scripts/bootstrap.ts",
"bootstrap:contracts": "ts-node -P scripts/tsconfig.json scripts/bootstrap-contracts-config.ts",
+ "bootstrap:custom": "yarn bootstrap:contracts custom",
"bootstrap:sandbox": "docker-compose down && docker-compose up -d && yarn bootstrap:contracts sandbox",
"teardown:sandbox": "docker-compose down"
},
diff --git a/public/index.html b/public/index.html
index 1bcb0621..5746d403 100644
--- a/public/index.html
+++ b/public/index.html
@@ -43,6 +43,10 @@
}
});
+
+
diff --git a/scripts/bootstrap-contracts-config.ts b/scripts/bootstrap-contracts-config.ts
index db7bd7e4..31fd28b4 100644
--- a/scripts/bootstrap-contracts-config.ts
+++ b/scripts/bootstrap-contracts-config.ts
@@ -7,7 +7,8 @@ import { MichelsonMap, TezosToolkit } from '@taquito/taquito';
import { InMemorySigner } from '@taquito/signer';
import {
Fa2MultiNftFaucetCode,
- FixedPriceSaleMarketTezCode
+ FixedPriceSaleMarketTezCode,
+ FixedPriceSaleTezFixedFeeCode
} from '@tqtezos/minter-contracts';
function toHexString(input: string) {
@@ -161,19 +162,46 @@ async function bootstrap(env: string) {
});
// bootstrap marketplace fixed price (tez)
- await bootstrapContract(bootstrappedConfig, toolkit, {
- configKey: 'contracts.marketplace.fixedPrice.tez',
- contractAlias: 'fixedPriceMarketTez',
- contractCode: FixedPriceSaleMarketTezCode.code,
- initStorage: () => ({ sales: new MichelsonMap() })
- });
+ const marketplaceFeePercent = config.get("contractOpts.marketplace.fee.percent");
+ const marketplaceFeeAddress = config.get("contractOpts.marketplace.fee.address");
+ if (marketplaceFeePercent && marketplaceFeeAddress) {
+ await bootstrapContract(bootstrappedConfig, toolkit, {
+ configKey: 'contracts.marketplace.fixedPrice.tez',
+ contractAlias: 'fixedPriceMarketTez',
+ contractCode: FixedPriceSaleTezFixedFeeCode.code,
+ initStorage: () => ({
+ admin: {
+ admin: config.get("admin.address"),
+ paused: false
+ },
+ fee: {
+ fee_address: marketplaceFeeAddress,
+ fee_percent: marketplaceFeePercent
+ },
+ sales: new MichelsonMap()
+ })
+ });
+ } else {
+ await bootstrapContract(bootstrappedConfig, toolkit, {
+ configKey: 'contracts.marketplace.fixedPrice.tez',
+ contractAlias: 'fixedPriceMarketTez',
+ contractCode: FixedPriceSaleMarketTezCode.code,
+ initStorage: () => ({
+ admin: {
+ admin: config.get("admin.address"),
+ paused: false
+ },
+ sales: new MichelsonMap()
+ })
+ });
+ }
}
async function main() {
console.log(process.argv[2]);
const envArg = process.argv[2];
let env;
- if (['mainnet', 'testnet', 'sandbox'].includes(envArg)) {
+ if (['mainnet', 'testnet', 'custom', 'sandbox'].includes(envArg)) {
env = envArg;
} else {
env = readEnv();
diff --git a/scripts/bootstrap.ts b/scripts/bootstrap.ts
new file mode 100644
index 00000000..09f8f842
--- /dev/null
+++ b/scripts/bootstrap.ts
@@ -0,0 +1,262 @@
+import path from 'path';
+import Configstore from 'configstore';
+import { validateKeyHash, ValidationResult } from '@taquito/utils';
+import * as figlet from 'figlet';
+import clear from 'clear';
+import chalk from 'chalk';
+import inquirer from 'inquirer';
+import shelljs from 'shelljs';
+import { Spinner } from 'clui';
+
+interface ConfigInput {
+ network: string;
+ rpc: string;
+ rpcCustom?: string;
+ adminPkh: string;
+ adminSk: string;
+ marketplaceHasFee: boolean;
+ marketplaceFeePercent?: number;
+ marketplaceFeeAddress?: string;
+}
+
+const stripAnsi = (input: string): string => {
+ return input.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
+};
+
+// initializes and displays the welcome screen
+const init = () => {
+ clear();
+ console.log(
+ "\n" +
+ chalk.green(figlet.textSync('>OpenMinter', { font: 'ANSI Shadow' })) +
+ "\n\n" +
+ chalk.cyan("Welcome to OpenMinter!") +
+ "\n\n" +
+ "The following questions will help configure and bootstrap a custom\n" +
+ "installation of OpenMinter. " +
+ chalk.yellow("Please have your wallet private key on hand.") + "\n" +
+ "You will be asked for it in order to originate the OpenMinter contracts.\n" +
+ "\n"
+ );
+};
+
+const finish = () => {
+ console.log(
+ "\n" +
+ chalk.bold("Your customized OpenMinter is bootstrapped and ready to go!") + "\n" +
+ "\n" +
+ "Your customized config is located at " + chalk.yellow("config/custom-bootstrapped.json") + "\n" +
+ "We " + chalk.bold("strongly") + " recommend that you create a backup copy" +
+ "of this configuration file.\n" +
+ "\n\n" +
+ "You can now start your customized OpenMinter by running: " + chalk.yellow("yarn start:custom") + "\n" +
+ "\n" +
+ "️🔥🔥🔥 The OpenMinter Team 🔥🔥🔥\n"
+ );
+};
+
+const askConfigQuestions = (): Promise => {
+ const questions = [
+ {
+ name: 'network',
+ type: 'list',
+ choices: ['Mainnet', 'Edonet'],
+ message: 'Select the network to deploy OpenMinter contracts to:',
+ filter: function (val: string) {
+ return val.toLowerCase();
+ },
+ },
+ {
+ name: 'rpc',
+ type: 'list',
+ choices: (input: any) => {
+ let rpcOpts: any[] = [];
+ if (input.network === "mainnet") {
+ rpcOpts = [
+ "https://rpc.tzbeta.net",
+ "https://mainnet.smartpy.io",
+ "https://api.tez.ie/rpc/mainnet",
+ "https://mainnet-tezos.giganode.io",
+ "Other"
+ ]
+ } else if (input.network === "edonet") {
+ rpcOpts = [
+ "https://rpctest.tzbeta.net",
+ "https://edonet.smartpy.io",
+ "https://api.tez.ie/rpc/edonet",
+ "https://edonet-tezos.giganode.io",
+ "Other"
+ ]
+ }
+ return rpcOpts;
+ },
+ message: 'Select an RPC node to connect to:',
+ filter: function (val: string) {
+ return val;
+ },
+ },
+ {
+ name: 'rpcCustom',
+ type: 'input',
+ when: (input: any) => input.rpc === "Other",
+ message: 'Enter the RPC node you want to connect to:',
+ validate: (input: string) => {
+ const regex = new RegExp(/^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi);
+ if (input.match(regex)) {
+ return true;
+ }
+ return "Please enter a valid url (starting with http:// or https://)";
+ },
+ filter: function (val: string) {
+ if (val.substr(-1) === '/') {
+ val = val.substr(0, val.length - 1);
+ }
+ return val;
+ },
+ },
+ {
+ name: 'adminPkh',
+ type: 'input',
+ message: 'Enter the wallet address for the admin of the contracts:',
+ validate: (input: string) => {
+ if (validateKeyHash(input) === ValidationResult.VALID) {
+ return true;
+ }
+ return "Please enter a valid wallet address";
+ }
+ },
+ {
+ name: 'marketplaceHasFee',
+ type: 'confirm',
+ message: 'Do you want to collect a fee on sales in the marketplace?'
+ },
+ {
+ name: 'marketplaceFeePercent',
+ type: 'number',
+ message: 'Enter the marketplace percentage fee (0-100):',
+ when: (input: any) => input.marketplaceHasFee,
+ validate: (input: number) => {
+ if (Number.isNaN(input) || !Number.isInteger(input) || input < 0 || input > 100) {
+ return "Please enter a whole number between 0 and 100";
+ }
+ return true;
+ }
+ },
+ {
+ name: 'marketplaceFeeAddress',
+ type: 'input',
+ message: 'Enter the address of the wallet that will receive the fee:',
+ when: (input: any) => input.marketplaceHasFee,
+ validate: (input: string) => {
+ if (validateKeyHash(input) === ValidationResult.VALID) {
+ return true;
+ }
+ return "Please enter a valid wallet address";
+ }
+ },
+ {
+ name: 'adminSk',
+ type: 'password',
+ message: 'Enter the secret key of the wallet that will originate the contracts:'
+ },
+ ]
+
+ return inquirer.prompt(questions);
+}
+
+const getConfigstore = (): Configstore => {
+ const configFileName = path.join(
+ __dirname,
+ `../config/custom.json`
+ );
+ return new Configstore('minter', {}, { configPath: configFileName });
+}
+
+const getBootstrappedConfigstore = (): Configstore => {
+ const configFileName = path.join(
+ __dirname,
+ `../config/custom-bootstrapped.json`
+ );
+ return new Configstore('minter', {}, { configPath: configFileName });
+}
+
+const saveConfig = (input: ConfigInput) => {
+ const config = getConfigstore();
+ config.set('network', input.network);
+ config.set('rpc', input.rpc === "Other" ? input.rpcCustom : input.rpc);
+ config.set('bcd.api', "https://api.better-call.dev");
+ config.set('bcd.gui', "https://better-call.dev");
+ config.set('tzkt.api', input.network === "mainnet" ? "https://staging.api.mainnet.tzkt.io" : "https://staging.api.edo2net.tzkt.io");
+ config.set('admin.address', input.adminPkh);
+ config.set('admin.secret', input.adminSk);
+ config.set('ipfsApi', "https://minter-api.tqhosted.com");
+ config.set('ipfsGateway', "https://tqtezos.mypinata.cloud");
+
+ if (input.marketplaceHasFee) {
+ config.set("contractOpts.marketplace.fee.percent", input.marketplaceFeePercent);
+ config.set("contractOpts.marketplace.fee.address", input.marketplaceFeeAddress);
+ }
+}
+
+const bootstrapContracts = async (): Promise => {
+ console.log(
+ "\n" +
+ chalk.cyan("Bootstrapping contracts...")
+ );
+
+ shelljs.rm('-f', path.join(
+ __dirname,
+ `../config/custom-bootstrapped.json`
+ ));
+
+ return new Promise((resolve, reject) => {
+ const spinner = new Spinner('Bootstrapping contracts...', ['⣾','⣽','⣻','⢿','⡿','⣟','⣯','⣷']);
+ const child = shelljs.exec('yarn bootstrap:custom', { async: true, silent: true });
+ spinner.start();
+
+ let longest = 0;
+ child.stdout?.on('data', function(data) {
+ const msg = stripAnsi(data).trim();
+ longest = Math.max(longest, msg.length);
+ spinner.message(msg.padEnd(longest, ' '));
+ });
+
+ child.stderr?.on('data', function(data) {
+ spinner.stop();
+ console.log(
+ "Encountered error:" + "\n" +
+ chalk.red(stripAnsi(data).trim())
+ );
+ reject();
+ });
+
+ child.on('exit', (code) => {
+ console.log(
+ "\n" +
+ ' ' + chalk.green('✔ Done bootstrapping contracts')
+ );
+ spinner.stop();
+ resolve();
+ });
+ });
+}
+
+const cleanup = () => {
+ const config = getBootstrappedConfigstore();
+ config.delete("admin.secret");
+}
+
+// Main
+(async () => {
+
+ init();
+
+ saveConfig(await askConfigQuestions());
+
+ await bootstrapContracts();
+
+ cleanup();
+
+ finish();
+
+})();
diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json
index b21a5919..1ea01609 100644
--- a/scripts/tsconfig.json
+++ b/scripts/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "lib": [ "es2015", "dom" ],
"target": "ES5",
"module": "commonjs",
"strict": true,
diff --git a/src/@types/ipfs-http-client.d.ts b/src/@types/ipfs-http-client.d.ts
index c30c2b99..4cd4486f 100644
--- a/src/@types/ipfs-http-client.d.ts
+++ b/src/@types/ipfs-http-client.d.ts
@@ -1,5 +1,4 @@
declare module 'ipfs-http-client' {
-
export type FileContent = any | Blob | string;
export interface Cid {
@@ -16,6 +15,5 @@ declare module 'ipfs-http-client' {
add: (data: FileContent) => Promise;
}
- export default function(any): IpfsClientApi;
+ export default function (any): IpfsClientApi;
}
-
diff --git a/src/@types/jsx-intrinsic.d.ts b/src/@types/jsx-intrinsic.d.ts
new file mode 100644
index 00000000..a434be72
--- /dev/null
+++ b/src/@types/jsx-intrinsic.d.ts
@@ -0,0 +1,9 @@
+declare global {
+ namespace JSX {
+ interface IntrinsicElements {
+ 'model-viewer': any;
+ }
+ }
+}
+
+export {};
diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx
index bb92cc2f..9da7ff9c 100644
--- a/src/components/App/index.tsx
+++ b/src/components/App/index.tsx
@@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import { Switch, Route } from 'wouter';
-import SplashPage from '../SplashPage';
import CreateNonFungiblePage from '../CreateNonFungiblePage';
import CollectionsCatalog from '../Collections/Catalog';
import CollectionDisplay from '../Collections/Catalog/CollectionDisplay';
@@ -11,19 +10,21 @@ import { Flex } from '@chakra-ui/react';
import Notifications from '../common/Notifications';
import { useSelector, useDispatch } from '../../reducer';
import { reconnectWallet } from '../../reducer/async/wallet';
-import { getMarketplaceNftsQuery } from '../../reducer/async/queries';
+// import { getMarketplaceNftsQuery } from '../../reducer/async/queries';
export default function App() {
const dispatch = useDispatch();
- const state = useSelector(
- s => s
- );
+ const state = useSelector(s => s);
- let walletReconnectAttempted = state.system.walletReconnectAttempted
+ let walletReconnectAttempted = state.system.walletReconnectAttempted;
- useEffect(() => {
- dispatch(getMarketplaceNftsQuery(state.marketplace.marketplace.address));
- }, [ state.marketplace.marketplace.address, dispatch ]);
+ // // This causes excessive resource consumption as *all* marketplace data
+ // // loads when the app is mounted, even if the user has not landed or will
+ // // not land on the `/marketplace` view
+ //
+ // useEffect(() => {
+ // dispatch(getMarketplaceNftsQuery(state.marketplace.marketplace.address));
+ // }, [state.marketplace.marketplace.address, dispatch]);
useEffect(() => {
if (!walletReconnectAttempted) {
@@ -31,17 +32,13 @@ export default function App() {
}
}, [walletReconnectAttempted, dispatch]);
- if (!walletReconnectAttempted) {
- return null;
- }
-
return (
-
+
diff --git a/src/components/Collections/Catalog/CollectionDisplay.tsx b/src/components/Collections/Catalog/CollectionDisplay.tsx
index f143081d..796a5c6c 100644
--- a/src/components/Collections/Catalog/CollectionDisplay.tsx
+++ b/src/components/Collections/Catalog/CollectionDisplay.tsx
@@ -12,11 +12,14 @@ import {
Text
} from '@chakra-ui/react';
import { MinterButton } from '../../common';
-import { RefreshCw, ExternalLink, Wind, HelpCircle } from 'react-feather';
+import { ExternalLink, Wind, HelpCircle } from 'react-feather';
import { Token } from '../../../reducer/slices/collections';
-import { ipfsUriToGatewayUrl } from '../../../lib/util/ipfs';
+import { IpfsGatewayConfig, ipfsUriToGatewayUrl } from '../../../lib/util/ipfs';
import { useDispatch, useSelector } from '../../../reducer';
-import { getContractNftsQuery } from '../../../reducer/async/queries';
+import {
+ getContractNftsQuery,
+ getNftAssetContractQuery
+} from '../../../reducer/async/queries';
import CollectionsDropdown from './CollectionsDropdown';
function MediaNotFound() {
@@ -35,14 +38,15 @@ function MediaNotFound() {
);
}
-function TokenImage(props: { src: string }) {
+function TokenImage(props: TokenTileProps) {
+ const src = ipfsUriToGatewayUrl(props.config, props.artifactUri);
const [errored, setErrored] = useState(false);
const [obj, setObj] = useState<{ url: string; type: string } | null>(null);
useEffect(() => {
(async () => {
let blob;
try {
- blob = await fetch(props.src).then(r => r.blob());
+ blob = await fetch(src).then(r => r.blob());
} catch (e) {
return setErrored(true);
}
@@ -51,7 +55,7 @@ function TokenImage(props: { src: string }) {
type: blob.type
});
})();
- }, [props.src]);
+ }, [src]);
if (errored) {
return ;
@@ -62,7 +66,7 @@ function TokenImage(props: { src: string }) {
if (/^image\/.*/.test(obj.type)) {
return (
e.preventDefault()}
onMouseEnter={e => e.currentTarget.play()}
onMouseLeave={e => e.currentTarget.pause()}
+ muted
>
);
}
+ if (props.metadata.formats?.length) {
+ if (
+ props.metadata.formats[0].mimeType === 'model/gltf-binary' ||
+ props.metadata.formats[0].mimeType === 'model/gltf+json'
+ ) {
+ return (
+ <>
+
+ >
+ );
+ }
+ }
+
return ;
}
interface TokenTileProps extends Token {
- network: string;
+ config: IpfsGatewayConfig;
address: string;
}
@@ -116,9 +139,7 @@ function TokenTile(props: TokenTileProps) {
>
-
+
{
if (address !== null) {
- dispatch(getContractNftsQuery(address));
+ dispatch(getNftAssetContractQuery(address)).then(() =>
+ dispatch(getContractNftsQuery(address))
+ );
}
}, [address, dispatch]);
@@ -275,12 +298,7 @@ export default function CollectionDisplay({
base: 4,
md: 0
}}
- >
-
-
-
- Refresh
-
+ >
{tokens.map(token => {
@@ -288,7 +306,7 @@ export default function CollectionDisplay({
);
diff --git a/src/components/Collections/Catalog/CollectionsDropdown.tsx b/src/components/Collections/Catalog/CollectionsDropdown.tsx
index 7e062eb0..10907349 100644
--- a/src/components/Collections/Catalog/CollectionsDropdown.tsx
+++ b/src/components/Collections/Catalog/CollectionsDropdown.tsx
@@ -12,8 +12,7 @@ import {
} from '@chakra-ui/react';
import { useSelector, useDispatch } from '../../../reducer';
import { selectCollection } from '../../../reducer/slices/collections';
-import { ChevronDown, RefreshCw } from 'react-feather';
-import { getContractNftsQuery } from '../../../reducer/async/queries';
+import { ChevronDown } from 'react-feather';
export default function CollectionsDropdown() {
const state = useSelector(s => s.collections);
@@ -73,23 +72,6 @@ export default function CollectionsDropdown() {
- {
- const selectedCollection = state.selectedCollection;
- if (selectedCollection !== null) {
- dispatch(getContractNftsQuery(selectedCollection));
- }
- }}
- padding={2}
- borderRadius="5px"
- border="1px solid"
- borderColor="brand.blue"
- marginLeft={3}
- cursor="pointer"
- >
-
-
);
}
diff --git a/src/components/Collections/Catalog/Sidebar.tsx b/src/components/Collections/Catalog/Sidebar.tsx
index 42fedd83..108c9c2f 100644
--- a/src/components/Collections/Catalog/Sidebar.tsx
+++ b/src/components/Collections/Catalog/Sidebar.tsx
@@ -55,6 +55,7 @@ function CollectionTab({
}
export default function Sidebar() {
+ const tzPublicKey = useSelector(s => s.system.tzPublicKey);
const state = useSelector(s => s.collections);
const dispatch = useDispatch();
return (
@@ -71,12 +72,14 @@ export default function Sidebar() {
>
Featured
- dispatch(selectCollection(address))}
- {...state.collections[state.globalCollection]}
- />
+ {state.collections[state.globalCollection] ? (
+ dispatch(selectCollection(address))}
+ {...state.collections[state.globalCollection]}
+ />
+ ) : null}
{Object.keys(state.collections)
- .filter(address => address !== state.globalCollection)
+ .filter(
+ address =>
+ address !== state.globalCollection &&
+ state.collections[address]?.creator?.address === tzPublicKey
+ )
.map(address => (
s.system);
const collections = useSelector(s => s.collections);
const dispatch = useDispatch();
+ const globalCollection =
+ collections.collections[collections.globalCollection];
+
useEffect(() => {
+ if (!globalCollection) {
+ dispatch(getNftAssetContractQuery(collections.globalCollection));
+ return;
+ }
if (collections.selectedCollection === null) {
dispatch(selectCollection(collections.globalCollection));
+ return;
}
- }, [collections.selectedCollection, collections.globalCollection, dispatch]);
+ }, [
+ globalCollection,
+ collections.selectedCollection,
+ collections.globalCollection,
+ dispatch
+ ]);
useEffect(() => {
- if (system.status !== 'WalletConnected') {
- setLocation('/', { replace: true });
- } else {
+ if (system.status === 'WalletConnected') {
dispatch(getWalletAssetContractsQuery());
}
- }, [system.status, setLocation, dispatch]);
+ }, [system.status, dispatch]);
const selectedCollection = collections.selectedCollection;
- if (system.status !== 'WalletConnected' || !selectedCollection) {
- return null;
+ if (system.walletReconnectAttempted && system.status !== 'WalletConnected') {
+ return (
+
+
+
+
+ Create NFTs on Tezos
+
+
+ {
+ e.preventDefault();
+ dispatch(connectWallet());
+ }}
+ >
+ Connect your wallet
+
+
+
+
+
+ OpenMinter Version v{process.env.REACT_APP_VERSION}
+
+
+
+ GitHub
+
+
+
+
+ );
}
return (
diff --git a/src/components/Collections/TokenDetail/index.tsx b/src/components/Collections/TokenDetail/index.tsx
index f3086da2..7d038631 100644
--- a/src/components/Collections/TokenDetail/index.tsx
+++ b/src/components/Collections/TokenDetail/index.tsx
@@ -11,6 +11,7 @@ import {
Flex,
Heading,
Image,
+ Link,
Menu,
MenuList,
Modal,
@@ -24,7 +25,7 @@ import {
Text,
useDisclosure
} from '@chakra-ui/react';
-import { ChevronLeft, HelpCircle, MoreHorizontal, Star } from 'react-feather';
+import { ChevronLeft, HelpCircle, MoreHorizontal } from 'react-feather';
import { MinterButton, MinterMenuButton, MinterMenuItem } from '../../common';
import { TransferTokenModal } from '../../common/modals/TransferToken';
import { SellTokenButton } from '../../common/modals/SellToken';
@@ -36,8 +37,10 @@ import {
getContractNftsQuery,
getNftAssetContractQuery
} from '../../../reducer/async/queries';
-
+import lk from '../../common/assets/link-icon.svg'
+import tz from '../../common/assets/tezos-sym.svg'
import { Maximize2 } from 'react-feather';
+import { NftMetadata } from '../../../lib/nfts/decoders';
function NotFound() {
return (
@@ -91,12 +94,15 @@ function MediaNotFound() {
function TokenImage(props: {
id?: string;
src: string;
+ metadata: NftMetadata;
width?: string;
maxWidth?: string;
maxHeight?: string;
height?: string;
objectFit?: ResponsiveValue;
- onLoad?: (event: React.SyntheticEvent) => void;
+ cursor?: string;
+ onClick?: (event: React.SyntheticEvent) => void;
+ onLoad?: (event: React.SyntheticEvent) => void;
onFetch?: (type: string) => void;
}) {
const [errored, setErrored] = useState(false);
@@ -129,11 +135,12 @@ function TokenImage(props: {
key={props.id || 'assetImage'}
src={props.src}
objectFit={props.objectFit ?? "scale-down"}
- flex="1"
height={props.height ?? "100%"}
width={props.width}
maxWidth={props.maxWidth}
maxHeight={props.maxHeight ?? 'unset'}
+ cursor={props.cursor}
+ onClick={props.onClick}
onError={() => setErrored(true)}
onLoad={props.onLoad}
/>
@@ -145,17 +152,41 @@ function TokenImage(props: {
);
}
+ if (props.metadata.formats?.length) {
+ if (
+ props.metadata.formats[0].mimeType === 'model/gltf-binary' ||
+ props.metadata.formats[0].mimeType === 'model/gltf+json'
+ ) {
+ return (
+ <>
+
+ >
+ );
+ }
+ }
+
return ;
}
@@ -235,7 +266,7 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
justifyContent="center"
alignItems="center"
position="relative"
- backgroundColor="#333333f9"
+ backgroundColor="#222222f9"
zIndex="2000"
margin="0 !important"
borderRadius="0"
@@ -269,17 +300,35 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
-
+
-
+
{
@@ -288,96 +337,40 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
}}
>
-
+
-
-
- {isOwner ? (
-
- ) : (
- <>>
- )}
-
- {token.sale ? (
- isOwner ? (
-
-
-
- ꜩ
-
-
- {token.sale.price}
-
-
-
-
- ) : (
- <>
-
- {token.sale.price.toFixed(2)}ꜩ
-
-
- >
- )
- ) : isOwner ? (
-
- ) : (
- <>>
- )}
-
-
+
+
+
-
+
+
+
- {isOwner ? (
-
-
-
-
- You own this asset
-
-
+
+
+ {token.title}
+
+
+ {token.sale ? (
+ isOwner ? (
+ <>
+
+ {token.sale.price}
+
+
+
+
+ >
+ ) : (
+ <>
+
+ {token.sale.price.toFixed(2)}
+
+
+
+
+ >
+ )
+ ) : isOwner ? (
+
+
+
+ ) : (
+ <>>
+ )}
+ {isOwner ? (
+
+ ) : (
+ <>>
+ )}
- ) : null}
-
- {token.title}
-
-
- Minter: {token.metadata?.minter || 'Unknown'}
-
+
{token.description || 'No description provided'}
-
-
- Collection
-
- {contractAddress}
-
-
-
- Metadata
+ Token Info
+
+ Minter:
+
+ {token.owner}
+
+
+
+ Collection:
+
+ {contractAddress}
+
+
{token.metadata?.attributes?.map(({ name, value }) => (
{name}:
-
+
{value}
@@ -453,8 +495,8 @@ function TokenDetail({ contractAddress, tokenId }: TokenDetailProps) {
-
+
);
}
-export default TokenDetail;
\ No newline at end of file
+export default TokenDetail;
diff --git a/src/components/CreateNonFungiblePage/CollectionSelect.tsx b/src/components/CreateNonFungiblePage/CollectionSelect.tsx
index a57c6f55..45828bfe 100644
--- a/src/components/CreateNonFungiblePage/CollectionSelect.tsx
+++ b/src/components/CreateNonFungiblePage/CollectionSelect.tsx
@@ -40,6 +40,7 @@ export default function CollectionSelect() {
fontWeight="normal"
py={3}
height="auto"
+ backgroundColor="white"
color={state.collectionAddress ? 'brand.black' : 'brand.gray'}
>
diff --git a/src/components/CreateNonFungiblePage/FileUpload.tsx b/src/components/CreateNonFungiblePage/FileUpload.tsx
index 1b29154a..3b29b637 100644
--- a/src/components/CreateNonFungiblePage/FileUpload.tsx
+++ b/src/components/CreateNonFungiblePage/FileUpload.tsx
@@ -11,7 +11,14 @@ 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();
@@ -19,6 +26,7 @@ export function FilePreview({ file }: { file: SelectedFile }) {
<>
);
}
diff --git a/src/components/Marketplace/Catalog/TokenCard.tsx b/src/components/Marketplace/Catalog/TokenCard.tsx
index 897af4f3..bff46713 100644
--- a/src/components/Marketplace/Catalog/TokenCard.tsx
+++ b/src/components/Marketplace/Catalog/TokenCard.tsx
@@ -1,31 +1,33 @@
import React from 'react';
import { Token } from '../../../reducer/slices/collections';
import { useLocation } from 'wouter';
-import { ipfsUriToGatewayUrl } from '../../../lib/util/ipfs';
-import { AspectRatio, Box, Flex, Text, Heading } from '@chakra-ui/react';
+import { IpfsGatewayConfig } from '../../../lib/util/ipfs';
+import { AspectRatio, Box, Flex } from '@chakra-ui/react';
import { TokenMedia } from '../../common/TokenMedia';
+import tz from '../../common/assets/tezos-sym.svg'
interface TokenCardProps extends Token {
- network: string;
+ config: IpfsGatewayConfig;
}
export default function TokenCard(props: TokenCardProps) {
const [, setLocation] = useLocation();
return (
setLocation(`/collection/${props.address}/token/${props.id}`)
@@ -33,9 +35,7 @@ export default function TokenCard(props: TokenCardProps) {
>
-
+
- {props.title}
- Seller: {props.sale?.seller.substr(0, 5)}...{props.sale?.seller.substr(-5)}
-
-
- Current Price
- {props.sale?.price} ꜩ
+ {props.title}
+
+ {props.sale?.price}
+
);
-}
+}
\ No newline at end of file
diff --git a/src/components/Marketplace/Catalog/index.tsx b/src/components/Marketplace/Catalog/index.tsx
index fdb3f211..e1bd7e81 100644
--- a/src/components/Marketplace/Catalog/index.tsx
+++ b/src/components/Marketplace/Catalog/index.tsx
@@ -1,10 +1,14 @@
import React, { useEffect } from 'react';
-import { Box, Container, Text, Flex, Heading, SimpleGrid, Spinner } from '@chakra-ui/react';
+import { Text, Flex, Heading, SimpleGrid, Spinner, Box } from '@chakra-ui/react';
import { Wind } from 'react-feather';
import { useSelector, useDispatch } from '../../../reducer';
-import { getMarketplaceNftsQuery } from '../../../reducer/async/queries';
+import {
+ getMarketplaceNftsQuery,
+ loadMoreMarketplaceNftsQuery
+} from '../../../reducer/async/queries';
import TokenCard from './TokenCard';
import FeaturedToken from './FeaturedToken';
+import { VisibilityTrigger } from '../../common/VisibilityTrigger';
export default function Catalog() {
const { system, marketplace: state } = useSelector(s => s);
@@ -12,16 +16,18 @@ export default function Catalog() {
useEffect(() => {
dispatch(getMarketplaceNftsQuery(state.marketplace.address));
- }, [ state.marketplace.address, dispatch ]);
+ }, [state.marketplace.address, dispatch]);
- let tokens = state.marketplace.tokens;
- if (tokens === null) {
- tokens = [];
- }
+ const loadMore = () => {
+ dispatch(loadMoreMarketplaceNftsQuery({}));
+ };
+
+ let tokens =
+ state.marketplace.tokens?.filter(x => x.token).map(x => x.token!) ?? [];
return (
{state.marketplace.loaded && tokens.length > 0 ? (
-
-
-
+
+
+
) : null}
-
-
- {!state.marketplace.loaded ? (
-
-
-
- Loading...
+
+ {!state.marketplace.loaded ? (
+
+
+
+ Loading...
-
- ) :
- tokens.length === 0 ? (
-
-
-
-
- No tokens to display in this marketplace
+
+ ) : tokens.length === 0 ? (
+
+
+
+
+ No tokens to display in this marketplace
-
- ) : (
+
+ ) : (
+ <>
+
<>
-
- {tokens.slice(1).map(token => {
- return (
+ {tokens.slice(1).map(token => {
+ return (
+
- );
- })}
-
+
+ );
+ })}
+
>
- )}
-
-
+
+ >
+ )}
+
);
}
diff --git a/src/components/SplashPage/index.tsx b/src/components/SplashPage/index.tsx
deleted file mode 100644
index 6ba81f5e..00000000
--- a/src/components/SplashPage/index.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import React, { useEffect } from 'react';
-import { useLocation } from 'wouter';
-import { Flex, Text, Heading, Image, Link } from '@chakra-ui/react';
-import { MinterButton /* , MinterLink */ } from '../common';
-import logo from '../common/assets/splash-logo.svg';
-import { useSelector, useDispatch } from '../../reducer';
-import { connectWallet } from '../../reducer/async/wallet';
-
-export default function SplashPage() {
- const [, setLocation] = useLocation();
- const system = useSelector(s => s.system);
- const dispatch = useDispatch();
-
- useEffect(() => {
- if (system.status === 'WalletConnected') {
- setLocation('/collections');
- }
- }, [system.status, setLocation]);
-
- return (
-
-
-
-
- Create NFTs on Tezos
-
-
- {
- e.preventDefault();
- dispatch(connectWallet());
- }}
- >
- Connect your wallet
-
-
-
-
-
- OpenMinter Version v{process.env.REACT_APP_VERSION}
-
-
-
- GitHub
-
-
-
-
- );
-}
diff --git a/src/components/common/Header.tsx b/src/components/common/Header.tsx
index e299bb30..f2a48417 100644
--- a/src/components/common/Header.tsx
+++ b/src/components/common/Header.tsx
@@ -17,15 +17,15 @@ import {
DrawerBody,
Heading
} from '@chakra-ui/react';
-import { Plus, Settings, Menu as HamburgerIcon } from 'react-feather';
+import { Plus, Menu as HamburgerIcon } from 'react-feather';
import { RiStore2Line } from 'react-icons/ri';
-// import { IoCubeOutline } from 'react-icons/io5';
import { MdCollections } from 'react-icons/md';
import headerLogo from './assets/header-logo.svg';
import { useSelector, useDispatch } from '../../reducer';
import { connectWallet, disconnectWallet } from '../../reducer/async/wallet';
import { MinterButton } from '.';
import logo from './assets/splash-logo.svg';
+import wallet_icon from './assets/wallet.svg';
interface MobileHeaderLinkProps {
to: string;
@@ -120,12 +120,24 @@ function WalletDisplay() {
const dispatch = useDispatch();
return (
<>
-
+ ) : (
+ {
+ e.preventDefault();
+ dispatch(connectWallet());
+ }}
+ >
+ Connect Wallet
+
+
+ )}
>
);
}
@@ -238,7 +246,13 @@ function NavItems() {
}}
mb={4}
>
- Connect your Wallet
+ Connect Wallet
+
)}
@@ -294,10 +308,8 @@ function NavItems() {
}
export function Header() {
- const [location, setLocation] = useLocation();
- if (location === '/' || location === '') {
- return null;
- }
+ const [, setLocation] = useLocation();
+
return (
{
e.preventDefault();
- setLocation('/collections');
+ setLocation('/marketplace');
}}
cursor="pointer"
/>
@@ -330,7 +342,7 @@ export function Header() {
src={headerLogo}
onClick={e => {
e.preventDefault();
- setLocation('/collections');
+ setLocation('/marketplace');
}}
cursor="pointer"
/>
diff --git a/src/components/common/TokenMedia.tsx b/src/components/common/TokenMedia.tsx
index 38f8dc92..327268cd 100644
--- a/src/components/common/TokenMedia.tsx
+++ b/src/components/common/TokenMedia.tsx
@@ -1,6 +1,14 @@
import React, { useEffect, useState } from 'react';
import { Flex, Image } from '@chakra-ui/react';
import { FiHelpCircle } from 'react-icons/fi';
+import { IpfsGatewayConfig, ipfsUriToGatewayUrl } from '../../lib/util/ipfs';
+import { Token } from '../../reducer/slices/collections';
+
+interface TokenMediaProps extends Token {
+ config: IpfsGatewayConfig;
+ maxW?: string;
+ class?: string;
+}
function MediaNotFound() {
return (
@@ -18,14 +26,15 @@ function MediaNotFound() {
);
}
-export function TokenMedia(props: { src: string, maxW?: string }) {
+export function TokenMedia(props: TokenMediaProps) {
+ const src = ipfsUriToGatewayUrl(props.config, props.artifactUri);
const [errored, setErrored] = useState(false);
const [obj, setObj] = useState<{ url: string; type: string } | null>(null);
useEffect(() => {
(async () => {
let blob;
try {
- blob = await fetch(props.src).then(r => r.blob());
+ blob = await fetch(src).then(r => r.blob());
} catch (e) {
return setErrored(true);
}
@@ -34,7 +43,7 @@ export function TokenMedia(props: { src: string, maxW?: string }) {
type: blob.type
});
})();
- }, [props.src]);
+ }, [src]);
if (errored) {
return ;
@@ -43,15 +52,13 @@ export function TokenMedia(props: { src: string, maxW?: string }) {
if (!obj) return null;
if (/^image\/.*/.test(obj.type)) {
- console.log(props.src)
return (
setErrored(true)}
/>
);
@@ -64,13 +71,31 @@ export function TokenMedia(props: { src: string, maxW?: string }) {
onClick={e => e.preventDefault()}
onMouseEnter={e => e.currentTarget.play()}
onMouseLeave={e => e.currentTarget.pause()}
- height="100%"
- style={{maxWidth:props.maxW}}
+ style={{ maxWidth: props.maxW }}
+ muted
>
);
}
+ if (props.metadata.formats?.length) {
+ if (
+ props.metadata.formats[0].mimeType === 'model/gltf-binary' ||
+ props.metadata.formats[0].mimeType === 'model/gltf+json'
+ ) {
+ return (
+ <>
+
+ >
+ );
+ }
+ }
+
return ;
}
diff --git a/src/components/common/VisibilityTrigger.tsx b/src/components/common/VisibilityTrigger.tsx
new file mode 100644
index 00000000..a41b2f67
--- /dev/null
+++ b/src/components/common/VisibilityTrigger.tsx
@@ -0,0 +1,80 @@
+import React, { useEffect, useRef } from 'react';
+
+// Based on https://github.com/olistic/react-use-visibility#readme MIT License
+function isElementNearViewport(
+ element: HTMLElement,
+ allowedDistanceToViewport = 0
+) {
+ const {
+ top,
+ right,
+ bottom,
+ left,
+ height,
+ width
+ } = element.getBoundingClientRect();
+
+ const windowWidth = window.innerWidth;
+ const windowHeight = window.innerHeight;
+
+ const topCheck = top + height;
+ const leftCheck = left + width;
+ const bottomCheck = bottom - height;
+ const rightCheck = right - width;
+
+ return (
+ topCheck >= -allowedDistanceToViewport &&
+ leftCheck >= -allowedDistanceToViewport &&
+ bottomCheck <= windowHeight + allowedDistanceToViewport &&
+ rightCheck <= windowWidth + allowedDistanceToViewport
+ );
+}
+
+/** Simple Cross Browser Visibility Trigger */
+export const useVisibilityTrigger = (
+ elementRef: { current: undefined | null | HTMLElement },
+ onVisible: () => void,
+ allowedDistanceToViewport = 0
+) => {
+ const hasTriggered = useRef(false);
+
+ useEffect(() => {
+ const intervalId = setInterval(() => {
+ if (hasTriggered.current) {
+ return;
+ }
+ if (!elementRef.current) {
+ return;
+ }
+
+ if (
+ !isElementNearViewport(elementRef.current, allowedDistanceToViewport)
+ ) {
+ return;
+ }
+
+ hasTriggered.current = true;
+ onVisible();
+ }, 100);
+ return () => clearInterval(intervalId);
+ }, [allowedDistanceToViewport, elementRef, onVisible]);
+
+ return {
+ reset: () => {
+ hasTriggered.current = false;
+ }
+ };
+};
+
+export const VisibilityTrigger = ({
+ onVisible,
+ allowedDistanceToViewport
+}: {
+ onVisible: () => void;
+ allowedDistanceToViewport?: number;
+}) => {
+ const divRef = useRef(null as null | HTMLDivElement);
+ useVisibilityTrigger(divRef, onVisible, allowedDistanceToViewport ?? 0);
+
+ return ;
+};
diff --git a/src/components/common/assets/link-icon.svg b/src/components/common/assets/link-icon.svg
new file mode 100644
index 00000000..b0176187
--- /dev/null
+++ b/src/components/common/assets/link-icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/common/assets/tezos-sym.svg b/src/components/common/assets/tezos-sym.svg
new file mode 100644
index 00000000..cb047e0e
--- /dev/null
+++ b/src/components/common/assets/tezos-sym.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/common/assets/wallet.svg b/src/components/common/assets/wallet.svg
new file mode 100644
index 00000000..f24717a6
--- /dev/null
+++ b/src/components/common/assets/wallet.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/src/components/common/index.tsx b/src/components/common/index.tsx
index 8553b3e1..37235999 100644
--- a/src/components/common/index.tsx
+++ b/src/components/common/index.tsx
@@ -37,9 +37,7 @@ export function MinterMenuButton(
return ;
}
-export function MinterMenuItem(
- props: MenuItemProps & { variant?: string }
-) {
+export function MinterMenuItem(props: MenuItemProps & { variant?: string }) {
const { variant, ...rest } = props;
const styles = useStyleConfig('MenuItem', { variant });
return ;
diff --git a/src/components/common/modals/BuyToken.tsx b/src/components/common/modals/BuyToken.tsx
index 4bd25aaf..7e83b5fd 100644
--- a/src/components/common/modals/BuyToken.tsx
+++ b/src/components/common/modals/BuyToken.tsx
@@ -12,8 +12,9 @@ import {
import { MinterButton } from '../../common';
import { useDispatch } from '../../../reducer';
import { buyTokenAction } from '../../../reducer/async/actions';
-import { Nft } from '../../../lib/nfts/queries';
+import { Nft } from '../../../lib/nfts/decoders';
import FormModal, { BaseModalProps, BaseModalButtonProps } from './FormModal';
+import tz from '../assets/tezos-sym.svg'
interface BuyTokenModalProps extends BaseModalProps {
contract: string;
@@ -50,7 +51,7 @@ export function BuyTokenModal(props: BuyTokenModalProps) {
You are about to purchase
{' '}
- {props.token.title} (ꜩ {props.token.sale?.price})
+ {props.token.title} ( {props.token.sale?.price})
diff --git a/src/components/common/modals/FormModal.tsx b/src/components/common/modals/FormModal.tsx
index 334b6613..6c26dec5 100644
--- a/src/components/common/modals/FormModal.tsx
+++ b/src/components/common/modals/FormModal.tsx
@@ -30,6 +30,7 @@ interface ContentProps {
pendingMessage?: React.ReactNode;
pendingAsyncMessage?: React.ReactNode;
completeMessage?: React.ReactNode;
+ errorMessage?: React.ReactNode;
}
function Content(props: ContentProps) {
@@ -41,7 +42,7 @@ function Content(props: ContentProps) {
- Error Creating Collection
+ {props.errorMessage || 'Operation failed'}
onRetry()}>
diff --git a/src/components/common/modals/SellToken.tsx b/src/components/common/modals/SellToken.tsx
index b35f254b..6c9d7f2d 100644
--- a/src/components/common/modals/SellToken.tsx
+++ b/src/components/common/modals/SellToken.tsx
@@ -17,6 +17,7 @@ import { MinterButton } from '../../common';
import { useDispatch } from '../../../reducer';
import { listTokenAction } from '../../../reducer/async/actions';
import FormModal, { BaseModalProps, BaseModalButtonProps } from './FormModal';
+import tz from '../assets/tezos-sym.svg'
interface SellTokenModalProps extends BaseModalProps {
contract: string;
@@ -60,7 +61,7 @@ export function SellTokenModal(props: SellTokenModalProps) {
pointerEvents="none"
color="gray.900"
fontSize="1.2em"
- children="ꜩ"
+ children={}
/>
(storage: S) =>
+ t.type({
+ type: t.string,
+ kind: t.string,
+ tzips: t.union([t.array(t.string), t.undefined]),
+ address: t.string,
+ balance: t.number,
+ creator: t.type({
+ address: t.string
+ }),
+ numContracts: t.number,
+ numDelegations: t.number,
+ numOriginations: t.number,
+ numTransactions: t.number,
+ numReveals: t.number,
+ numMigrations: t.number,
+ firstActivity: t.number,
+ firstActivityTime: t.string,
+ lastActivity: t.number,
+ lastActivityTime: t.string,
+ storage: storage
+ });
+
+// Generic BigMaps
+
+export const BigMapRow = (props: {
+ key: K;
+ value: V;
+}) =>
+ t.type({
+ id: t.number,
+ active: t.boolean,
+ hash: t.string,
+ key: props.key,
+ value: props.value,
+ firstLevel: t.number,
+ lastLevel: t.number,
+ updates: t.number
+ });
+
+export const BigMapUpdateRow = (content: {
+ key: K;
+ value: V;
+}) =>
+ t.type({
+ id: t.number,
+ level: t.number,
+ timestamp: t.string,
+ bigmap: t.number,
+ contract: t.intersection([
+ t.partial({ alias: t.string }),
+ t.type({ address: t.string })
+ ]),
+ path: t.string,
+ action: t.string,
+ content: t.type({ hash: t.string, key: content.key, value: content.value })
+ });
+
+// FA2 BigMaps
+
+export type AssetMetadataBigMap = t.TypeOf;
+export const AssetMetadataBigMap = t.array(
+ BigMapRow({ key: t.string, value: t.string })
+);
+
+export type LedgerBigMap = t.TypeOf;
+export const LedgerBigMap = t.array(
+ BigMapRow({ key: t.string, value: t.string })
+);
+
+export type TokenMetadataBigMap = t.TypeOf;
+export const TokenMetadataBigMap = t.array(
+ BigMapRow({
+ key: t.string,
+ value: t.type({
+ token_id: t.string,
+ token_info: t.type({
+ '': t.string
+ })
+ })
+ })
+);
+
+// FixedPriceSale BigMaps
+
+export type FixedPriceSaleBigMap = t.TypeOf;
+export const FixedPriceSaleBigMap = t.array(
+ BigMapRow({
+ key: t.type({
+ sale_token: t.type({
+ token_for_sale_address: t.string,
+ token_for_sale_token_id: t.string
+ }),
+ sale_seller: t.string
+ }),
+ value: t.string
+ })
+);
+
+// NFT Metadata
+
+export type NftMetadataFormat = t.TypeOf;
+export const NftMetadataFormat = t.partial({
+ uri: t.string,
+ hash: t.string,
+ mimeType: t.string,
+ fileSize: t.number,
+ fileName: t.string,
+ duration: t.string,
+ dimensions: t.partial({
+ value: t.string,
+ unit: t.string
+ }),
+ dataRate: t.partial({
+ value: t.number,
+ unit: t.string
+ })
+});
+
+export type NftMetadataAttribute = t.TypeOf;
+export const NftMetadataAttribute = t.intersection([
+ t.type({ name: t.string, value: t.string }),
+ t.partial({ type: t.string })
+]);
+
+export type NftMetadata = t.TypeOf;
+export const NftMetadata = t.partial({
+ '': t.string,
+ name: t.string,
+ minter: t.string,
+ symbol: t.string,
+ decimals: t.number,
+ rightUri: t.string,
+ artifactUri: t.string,
+ displayUri: t.string,
+ thumbnailUri: t.string,
+ externalUri: t.string,
+ description: t.string,
+ creators: t.array(t.string),
+ contributors: t.array(t.string),
+ publishers: t.array(t.string),
+ date: t.string,
+ blocklevel: t.number,
+ type: t.string,
+ tags: t.array(t.string),
+ genres: t.array(t.string),
+ language: t.string,
+ identifier: t.string,
+ rights: t.string,
+ isTransferable: t.boolean,
+ isBooleanAmount: t.boolean,
+ shouldPreferSymbol: t.boolean,
+ formats: t.array(NftMetadataFormat),
+ attributes: t.array(NftMetadataAttribute)
+});
+
+export type NftSale = t.TypeOf;
+export const NftSale = t.type({
+ id: t.number,
+ seller: t.string,
+ price: t.number,
+ mutez: t.number,
+ type: t.string
+});
+
+export type Nft = t.TypeOf;
+export const Nft = t.intersection([
+ t.type({
+ id: t.number,
+ title: t.string,
+ owner: t.string,
+ description: t.string,
+ artifactUri: t.string,
+ metadata: NftMetadata
+ }),
+ t.partial({
+ sale: NftSale,
+ address: t.string
+ })
+]);
+
+// Contract Metadata
+
+export const AssetContractMetadata = t.type({
+ name: t.string
+});
+
+export type AssetContract = t.TypeOf;
+export const AssetContract = t.intersection([
+ ContractRow(t.unknown),
+ t.type({
+ metadata: AssetContractMetadata
+ })
+]);
diff --git a/src/lib/nfts/queries.ts b/src/lib/nfts/queries.ts
index ca0cc035..a34ed203 100644
--- a/src/lib/nfts/queries.ts
+++ b/src/lib/nfts/queries.ts
@@ -1,10 +1,13 @@
+/* eslint-disable no-redeclare */
import { Buffer } from 'buffer';
-import Joi from 'joi';
+import * as t from 'io-ts';
+import _ from 'lodash';
import { SystemWithToolkit, SystemWithWallet } from '../system';
-import select from '../util/selectObjectByKeys';
-import { ipfsUriToCid } from '../util/ipfs';
-import { ContractAbstraction } from '@taquito/taquito';
-import { tzip12 } from '@taquito/tzip12';
+import { TzKt, Params } from '../service/tzkt';
+import { isLeft } from 'fp-ts/lib/Either';
+import { compact } from 'fp-ts/lib/Array';
+import { getRight } from 'fp-ts/lib/Option';
+import * as D from './decoders';
function fromHexString(input: string) {
if (/^([A-Fa-f0-9]{2})*$/.test(input)) {
@@ -13,272 +16,164 @@ function fromHexString(input: string) {
return input;
}
-interface NftSale {
- seller: string;
- price: number;
- mutez: number;
- type: string;
-}
+//// Data retrieval and decoding functions
-export interface Nft {
- id: number;
- title: string;
- owner: string;
- description: string;
- artifactUri: string;
- metadata: NftMetadata;
- sale?: NftSale;
- address?: string;
+async function getAssetMetadataBigMap(
+ tzkt: TzKt,
+ address: string
+): Promise {
+ const path = 'metadata';
+ const data = await tzkt.getContractBigMapKeys(address, path);
+ const decoded = D.LedgerBigMap.decode(data);
+ if (isLeft(decoded)) {
+ throw Error('Failed to decode `getAssetMetadata` response');
+ }
+ return decoded.right;
}
-const contractCache: Record = {};
-
-export async function getMarketplaceNfts(
- system: SystemWithToolkit | SystemWithWallet,
+async function getLedgerBigMap(
+ tzkt: TzKt,
address: string
-): Promise {
- const storage = await system.betterCallDev.getContractStorage(address);
- const bigMapId = select(storage, {
- type: 'big_map'
- })?.value;
- const tokenSales = await system.betterCallDev.getBigMapKeys(bigMapId);
- const activeSales = tokenSales.filter((v: any) => {
- return v.data.value;
- });
-
- return Promise.all(
- activeSales.map(
- async (tokenSale: any): Promise => {
- const saleAddress = select(tokenSale, { name: 'token_for_sale_address' })?.value;
- const tokenId = parseInt(select(tokenSale, { name: 'token_for_sale_token_id' })?.value, 10);
- const sale = {
- seller: select(tokenSale, { name: 'sale_seller' })?.value,
- price: Number.parseInt(tokenSale.data.value?.value || 0, 10) / 1000000,
- mutez: Number.parseInt(tokenSale.data.value?.value || 0, 10),
- type: 'fixedPrice'
- };
-
- if (!(contractCache[saleAddress] instanceof ContractAbstraction)) {
- contractCache[saleAddress] = await system.toolkit.contract.at(saleAddress, tzip12);
- }
+): Promise {
+ const path = 'assets.ledger';
+ const data = await tzkt.getContractBigMapKeys(address, path);
+ const decoded = D.LedgerBigMap.decode(data);
+ if (isLeft(decoded)) {
+ throw Error('Failed to decode `getLedger` response');
+ }
+ return decoded.right;
+}
- const metadata = await contractCache[saleAddress].tzip12().getTokenMetadata(tokenId);
+async function getTokenMetadataBigMap(
+ tzkt: TzKt,
+ address: string
+): Promise {
+ const path = 'assets.token_metadata';
+ const data = await tzkt.getContractBigMapKeys(address, path);
+ const decoded = D.TokenMetadataBigMap.decode(data);
+ if (isLeft(decoded)) {
+ throw Error('Failed to decode `getTokenMetadata` response');
+ }
+ return decoded.right;
+}
- return {
- address: saleAddress,
- id: tokenId,
- title: metadata.name,
- owner: sale.seller,
- description: metadata.description,
- artifactUri: metadata.artifactUri,
- metadata: metadata,
- sale: sale
- };
- }
- )
- );
+function transformFixedPriceSales(fixedPriceSales: any): t.Mixed[] {
+ fixedPriceSales.forEach((fixedPriceSale: any, i: number) => {
+ if (fixedPriceSale.key.hasOwnProperty('seller')) {
+ fixedPriceSales[i].key['sale_seller'] = fixedPriceSale.key.seller;
+ delete fixedPriceSales[i].key.seller;
+ }
+ });
+ return fixedPriceSales;
}
-export class NftMetadata {
- [index: string]:
- | string
- | undefined
- | number
- | Array
- | boolean;
- ''?: string;
- name?: string;
- minter?: string;
- symbol?: string;
- decimals?: number;
- rightUri?: string;
- artifactUri?: string;
- displayUri?: string;
- thumbnailUri?: string;
- externalUri?: string;
- description?: string;
- creators?: Array;
- contributors?: Array;
- publishers?: Array;
- date?: string;
- blocklevel?: number;
- type?: string;
- tags?: Array;
- genres?: Array;
- language?: string;
- identifier?: string;
- rights?: string;
- isTransferable?: boolean;
- isBooleanAmount?: boolean;
- shouldPreferSymbol?: boolean;
- formats?: Array;
- attributes?: Array;
-
- constructor(
- root?: string,
- name?: string,
- minter?: string,
- symbol?: string,
- decimals?: number,
- rightUri?: string,
- artifactUri?: string,
- displayUri?: string,
- thumbnailUri?: string,
- externalUri?: string,
- description?: string,
- creators?: Array,
- contributors?: Array,
- publishers?: Array,
- date?: string,
- blocklevel?: number,
- type?: string,
- tags?: Array,
- genres?: Array,
- language?: string,
- identifier?: string,
- rights?: string,
- isTransferable?: boolean,
- isBooleanAmount?: boolean,
- shouldPreferSymbol?: boolean,
- formats?: Array,
- attributes?: Array
- ) {
- this[''] = root;
- this.name = name;
- this.minter = minter;
- this.symbol = symbol;
- this.decimals = decimals;
- this.rightUri = rightUri;
- this.artifactUri = artifactUri;
- this.displayUri = displayUri;
- this.thumbnailUri = thumbnailUri;
- this.externalUri = externalUri;
- this.description = description;
- this.creators = creators;
- this.contributors = contributors;
- this.publishers = publishers;
- this.date = date;
- this.blocklevel = blocklevel;
- this.type = type;
- this.tags = tags;
- this.genres = genres;
- this.language = language;
- this.identifier = identifier;
- this.rights = rights;
- this.isTransferable = isTransferable;
- this.isBooleanAmount = isBooleanAmount;
- this.shouldPreferSymbol = shouldPreferSymbol;
- this.formats = formats;
- this.attributes = attributes;
+async function getFixedPriceSalesBigMap(
+ tzkt: TzKt,
+ address: string
+): Promise {
+ let fixedPriceBigMapId;
+ const fixedPriceStorage = await tzkt.getContractStorage(address);
+ if (fixedPriceStorage.hasOwnProperty('sales')) {
+ fixedPriceBigMapId = fixedPriceStorage.sales;
+ } else {
+ fixedPriceBigMapId = fixedPriceStorage; // legacy marketplace contract
+ }
+ if (isLeft(t.number.decode(fixedPriceBigMapId))) {
+ throw Error('Failed to decode `getFixedPriceSales` bigMap ID');
+ }
+ const fixedPriceSales = transformFixedPriceSales(await tzkt.getBigMapKeys(fixedPriceBigMapId));
+ const decoded = D.FixedPriceSaleBigMap.decode(fixedPriceSales);
+ if (isLeft(decoded)) {
+ throw Error('Failed to decode `getFixedPriceSales` response');
}
+ return decoded.right;
}
-export interface NftMetadataFormat {
- uri?: string;
- hash?: string;
- mimeType: string;
- fileSize?: number;
- fileName?: string;
- duration?: string;
- dimensions?: NtfMetadataFormatDimensions;
- dataRate?: NtfMetadataFormatDataRate;
+async function getBigMapUpdates(
+ tzkt: TzKt,
+ params: Params,
+ content: { key: K; value: V }
+) {
+ const bigMapUpdates = await tzkt.getBigMapUpdates(params);
+ const decoder = t.array(D.BigMapUpdateRow(content));
+ const decoded = decoder.decode(bigMapUpdates);
+ if (isLeft(decoded)) {
+ throw Error('Failed to decode `getBigMapUpdates` response');
+ }
+ return decoded.right;
}
-export interface NtfMetadataFormatDataRate {
- value: number;
- unit: string;
-}
-export interface NtfMetadataFormatDimensions {
- value: string;
- unit: string;
+async function getContracts(
+ tzkt: TzKt,
+ params: Params,
+ storage: S
+) {
+ const contracts = await tzkt.getContracts(params);
+ const contractsArray = t.array(t.unknown).decode(contracts);
+ if (isLeft(contractsArray)) {
+ throw Error('Failed to decode `getContracts` response');
+ }
+ const decodedArray = contractsArray.right.map(D.ContractRow(storage).decode);
+ return compact(decodedArray.map(getRight));
}
-export interface NftMetadataAttribute {
- name: string | null;
- value: string | null;
- type?: string;
+async function getContract(
+ tzkt: TzKt,
+ address: string,
+ params: Params,
+ storage: S
+) {
+ const contract = await tzkt.getContract(address, params);
+ const decoded = D.ContractRow(storage).decode(contract);
+ if (isLeft(decoded)) {
+ throw Error('Failed to decode `getContracts` response');
+ }
+ return decoded.right;
}
+//// Main query functions
+
export async function getContractNfts(
system: SystemWithToolkit | SystemWithWallet,
address: string
-): Promise {
- const storage = await system.betterCallDev.getContractStorage(address);
-
- const ledgerBigMapId = select(storage, {
- type: 'big_map',
- name: 'ledger'
- })?.value;
-
- if (ledgerBigMapId === undefined || ledgerBigMapId === null) return [];
-
- const tokensBigMapId = select(storage, {
- type: 'big_map',
- name: 'token_metadata'
- })?.value;
-
- if (tokensBigMapId === undefined || ledgerBigMapId === null) return [];
-
- const ledger = await system.betterCallDev.getBigMapKeys(ledgerBigMapId);
-
- if (!ledger) return [];
-
- const tokens = await system.betterCallDev.getBigMapKeys(tokensBigMapId);
-
- if (!tokens) return [];
-
- // get tokens listed for sale
- const fixedPriceStorage = await system.betterCallDev.getContractStorage(
- system.config.contracts.marketplace.fixedPrice.tez
- );
- const fixedPriceBigMapId = select(fixedPriceStorage, {
- type: 'big_map'
- })?.value;
- const fixedPriceSales = await system.betterCallDev.getBigMapKeys(
- fixedPriceBigMapId
- );
+): Promise {
+ const ledger = await getLedgerBigMap(system.tzkt, address);
+ const tokens = await getTokenMetadataBigMap(system.tzkt, address);
+ const mktAddress = system.config.contracts.marketplace.fixedPrice.tez;
+ const tokenSales = await getFixedPriceSalesBigMap(system.tzkt, mktAddress);
+ const activeSales = tokenSales.filter(sale => sale.active);
return Promise.all(
tokens.map(
- async (token: any): Promise => {
- const tokenId = select(token, { name: 'token_id' })?.value;
- const metadataMap = select(token, { name: 'token_info' })?.children;
- let metadata = metadataMap.reduce((acc: any, next: any) => {
- return { ...acc, [next.name]: fromHexString(next.value) };
- }, {});
-
- if (ipfsUriToCid(metadata['""'])) {
- const resolvedMetadata = await system.resolveMetadata(metadata['""']);
- metadata = { ...metadata, ...resolvedMetadata.metadata };
- } else if (ipfsUriToCid(metadata[''])) {
- const resolvedMetadata = await system.resolveMetadata(metadata['']);
- metadata = { ...metadata, ...resolvedMetadata.metadata };
- }
-
- 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'
- };
- }
+ async (token): Promise => {
+ const { token_id: tokenId, token_info: tokenInfo } = token.value;
+
+ // TODO: Write decoder function for data retrieval
+ const decodedInfo = _.mapValues(tokenInfo, fromHexString) as any;
+ const resolvedInfo = await system.resolveMetadata(
+ decodedInfo[''],
+ address
+ );
+ const metadata = { ...decodedInfo, ...resolvedInfo.metadata };
+
+ const saleData = activeSales.find(
+ v =>
+ v.key.sale_token.token_for_sale_address === address &&
+ v.key.sale_token.token_for_sale_token_id === tokenId
+ );
+
+ const sale = saleData && {
+ id: saleData.id,
+ seller: saleData.key.sale_seller,
+ price: Number.parseInt(saleData.value, 10) / 1000000,
+ mutez: Number.parseInt(saleData.value, 10),
+ type: 'fixedPrice'
+ };
return {
id: parseInt(tokenId, 10),
+ owner: ledger.find(e => e.key === tokenId)?.value!,
title: metadata.name,
- owner,
description: metadata.description,
artifactUri: metadata.artifactUri,
metadata: metadata,
@@ -289,57 +184,84 @@ export async function getContractNfts(
);
}
-export interface AssetContract {
- address: string;
- metadata: Record;
-}
-
-const metadataSchema = Joi.object({
- name: Joi.string().required().disallow(null)
-});
-
export async function getNftAssetContract(
system: SystemWithToolkit | SystemWithWallet,
address: string
-): Promise {
- const bcd = system.betterCallDev;
- const storage = await bcd.getContractStorage(address);
-
- const metadataBigMapId = select(storage, {
- type: 'big_map',
- name: 'metadata'
- })?.value;
+): Promise {
+ const contract = await getContract(system.tzkt, address, {}, t.unknown);
+ const metaBigMap = await getAssetMetadataBigMap(system.tzkt, address);
+ const metaUri = metaBigMap.find(v => v.key === '')?.value;
+ if (!metaUri) {
+ throw Error(`Could not extract metadata URI from ${address} storage`);
+ }
- const metaBigMap = await system.betterCallDev.getBigMapKeys(metadataBigMapId);
- const metaUri = select(metaBigMap, { key_string: '' })?.value.value;
- const { metadata } = await system.resolveMetadata(fromHexString(metaUri));
+ const { metadata } = await system.resolveMetadata(
+ fromHexString(metaUri),
+ address
+ );
+ const decoded = D.AssetContractMetadata.decode(metadata);
- const { error } = metadataSchema.validate(metadata, { allowUnknown: true });
- if (error) {
+ if (isLeft(decoded)) {
throw Error('Metadata validation failed');
}
- return { address, metadata };
+ return { ...contract, metadata: decoded.right };
}
-export async function getWalletNftAssetContracts(system: SystemWithWallet) {
- const bcd = system.betterCallDev;
- const response = await bcd.getWalletContracts(system.tzPublicKey);
- const assetContracts = response.items.filter(
- (i: any) =>
- Object.keys(i.body).includes('tags') &&
- i.body.tags.includes('fa2') &&
- Object.keys(i.body).includes('entrypoints') &&
- i.body.entrypoints.includes('balance_of') &&
- i.body.entrypoints.includes('mint') &&
- i.body.entrypoints.includes('transfer') &&
- i.body.entrypoints.includes('update_operators')
+export async function getWalletNftAssetContracts(
+ system: SystemWithWallet
+): Promise {
+ const contracts = await getContracts(
+ system.tzkt,
+ {
+ creator: system.tzPublicKey,
+ includeStorage: 'true'
+ },
+ t.unknown
+ );
+
+ const addresses = _.uniq(
+ contracts
+ .filter(c => c.kind === 'asset' && c.tzips?.includes('fa2'))
+ .map(c => c.address)
);
- const results = [];
- for (let assetContract of assetContracts) {
+ const results: D.AssetContract[] = [];
+
+ if (addresses.length === 0) {
+ return results;
+ }
+
+ const assetBigMapRows = (
+ await getBigMapUpdates(
+ system.tzkt,
+ {
+ path: 'metadata',
+ action: 'add_key',
+ 'contract.in': addresses.join(','),
+ limit: '10000'
+ },
+ {
+ key: t.string,
+ value: t.string
+ }
+ )
+ ).filter(v => v.content.key === '');
+
+ for (const row of assetBigMapRows) {
+ const contract = contracts.find(c => c.address === row.contract.address);
+ if (!contract) {
+ continue;
+ }
try {
- const result = await getNftAssetContract(system, assetContract.value);
- results.push(result);
+ const metaUri = row.content.value;
+ const { metadata } = await system.resolveMetadata(
+ fromHexString(metaUri),
+ contract.address
+ );
+ const decoded = D.AssetContractMetadata.decode(metadata);
+ if (!isLeft(decoded)) {
+ results.push({ ...contract, metadata: decoded.right });
+ }
} catch (e) {
console.log(e);
}
@@ -347,3 +269,124 @@ export async function getWalletNftAssetContracts(system: SystemWithWallet) {
return results;
}
+
+export type MarketplaceNftLoadingData = {
+ loaded: boolean;
+ error?: string;
+ token: null | D.Nft;
+ tokenSale: D.FixedPriceSaleBigMap[number];
+ tokenMetadata: undefined | string;
+};
+
+export async function getMarketplaceNfts(
+ system: SystemWithToolkit | SystemWithWallet,
+ address: string
+): Promise {
+ const tokenSales = await getFixedPriceSalesBigMap(system.tzkt, address);
+ const activeSales = tokenSales.filter(v => v.active);
+ const addresses = _.uniq(
+ activeSales.map(s => s.key.sale_token.token_for_sale_address)
+ );
+
+ const uniqueAddresses = Array.from(new Set(addresses));
+
+ if (uniqueAddresses.length === 0) {
+ return [];
+ }
+
+ const tokenBigMapRows = await getBigMapUpdates(
+ system.tzkt,
+ {
+ path: 'assets.token_metadata',
+ action: 'add_key',
+ 'contract.in': addresses.join(','),
+ limit: '10000'
+ },
+ {
+ key: t.string,
+ value: t.type({
+ token_id: t.string,
+ token_info: t.record(t.string, t.string)
+ })
+ }
+ );
+
+ // Sort descending (newest first)
+ const salesToView = [...activeSales].reverse();
+ const salesWithTokenMetadata = salesToView
+ .map(x => ({
+ tokenSale: x,
+ tokenItem: tokenBigMapRows.find(
+ item =>
+ x.key.sale_token.token_for_sale_address === item.contract.address &&
+ x.key.sale_token.token_for_sale_token_id ===
+ item.content.value.token_id + ''
+ )
+ }))
+ .map(x => ({
+ loaded: false,
+ token: null,
+ tokenSale: x.tokenSale,
+ tokenMetadata: x.tokenItem?.content?.value?.token_info['']
+ }));
+
+ return salesWithTokenMetadata;
+}
+
+export const loadMarketplaceNft = async (
+ system: SystemWithToolkit | SystemWithWallet,
+ tokenLoadData: MarketplaceNftLoadingData
+): Promise => {
+ const { token, loaded, tokenSale, tokenMetadata } = tokenLoadData;
+ const result = { ...tokenLoadData };
+
+ if (token || loaded) {
+ return result;
+ }
+ result.loaded = true;
+
+ try {
+ const {
+ token_for_sale_address: saleAddress,
+ token_for_sale_token_id: tokenIdStr
+ } = tokenSale.key.sale_token;
+
+ const tokenId = parseInt(tokenIdStr, 10);
+ const mutez = Number.parseInt(tokenSale.value, 10);
+ const sale = {
+ id: tokenSale.id,
+ seller: tokenSale.key.sale_seller,
+ price: mutez / 1000000,
+ mutez: mutez,
+ type: 'fixedPrice'
+ };
+
+ if (!tokenMetadata) {
+ result.error = "Couldn't retrieve tokenMetadata";
+ console.error("Couldn't retrieve tokenMetadata", { tokenSale });
+ return result;
+ }
+
+ const { metadata } = (await system.resolveMetadata(
+ fromHexString(tokenMetadata),
+ saleAddress
+ )) as any;
+
+ result.token = {
+ address: saleAddress,
+ id: tokenId,
+ title: metadata.name || '',
+ owner: sale.seller,
+ description: metadata.description || '',
+ artifactUri: metadata.artifactUri || '',
+ metadata: metadata,
+ sale: sale
+ };
+
+ return result;
+ } catch (err) {
+ result.error = "Couldn't load token";
+ console.error("Couldn't load token", { tokenSale, err });
+ return result;
+ }
+};
diff --git a/src/lib/service/bcd.ts b/src/lib/service/bcd.ts
index 4c4c0dd0..2fa0c1b9 100644
--- a/src/lib/service/bcd.ts
+++ b/src/lib/service/bcd.ts
@@ -1,11 +1,8 @@
import axios, { AxiosResponse } from 'axios';
import { Config } from '../system';
-export async function getBigMapKeys(
- config: Config,
- id: number
-) {
- let response : AxiosResponse;
+export async function getBigMapKeys(config: Config, id: number) {
+ let response: AxiosResponse;
let result = [];
let offset = 0;
const uri = `${config.bcd.api}/v1/bigmap/${config.network}/${id}/keys`;
@@ -33,29 +30,12 @@ export async function getContractStorage(config: Config, address: string) {
return response.data;
}
-export async function getContractOperations(
- config: Config,
- address: string,
- since?: Date
-) {
- const from = since ? `?from=${since.getTime()}` : '';
- const uri = `${config.bcd.api}/v1/contract/${config.network}/${address}/operations${from}`;
- const response = await axios.get(uri);
- return response.data;
-}
-
export async function getWalletContracts(config: Config, address: string) {
const uri = `${config.bcd.api}/v1/search?q=${address}&i=contract&n=${config.network}&g=0&s=0`;
const response = await axios.get(uri);
return response.data;
}
-export async function getAccountMetadata(config: Config, address: string) {
- const uri = `${config.bcd.api}/v1/account/${config.network}/${address}/metadata`;
- const response = await axios.get(uri);
- return response.data;
-}
-
export class BetterCallDev {
config: Config;
@@ -75,15 +55,7 @@ export class BetterCallDev {
return getContractStorage(this.config, address);
}
- getContractOperations(address: string, since?: Date) {
- return getContractOperations(this.config, address, since);
- }
-
getWalletContracts(address: string) {
return getWalletContracts(this.config, address);
}
-
- getAccountMetadata(address: string) {
- return getAccountMetadata(this.config, address);
- }
}
diff --git a/src/lib/service/tzkt.ts b/src/lib/service/tzkt.ts
new file mode 100644
index 00000000..55e1f699
--- /dev/null
+++ b/src/lib/service/tzkt.ts
@@ -0,0 +1,102 @@
+import axios from 'axios';
+import { Config } from '../system';
+
+export type Params = Record;
+
+function mkQueryParams(params: Params | undefined) {
+ const finalParams = { limit: '10000', ...params };
+ return new URLSearchParams(finalParams).toString();
+}
+
+export async function getBigMapKeys(
+ config: Config,
+ id: number,
+ params?: Params
+) {
+ const uri = `${config.tzkt.api}/v1/bigmaps/${id}/keys?${mkQueryParams(
+ params
+ )}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
+export async function getBigMapUpdates(config: Config, params?: Params) {
+ const uri = `${config.tzkt.api}/v1/bigmaps/updates?${mkQueryParams(params)}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
+export async function getContracts(config: Config, params?: Params) {
+ const uri = `${config.tzkt.api}/v1/contracts?${mkQueryParams(params)}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
+export async function getContract(
+ config: Config,
+ address: string,
+ params?: Params
+) {
+ const uri = `${config.tzkt.api}/v1/contracts/${address}?${mkQueryParams(
+ params
+ )}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
+export async function getContractBigMapKeys(
+ config: Config,
+ address: string,
+ name: string,
+ params?: Params
+) {
+ const uri = `${
+ config.tzkt.api
+ }/v1/contracts/${address}/bigmaps/${name}/keys?${mkQueryParams(params)}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
+export async function getContractStorage(
+ config: Config,
+ address: string,
+ params?: Params
+) {
+ const uri = `${
+ config.tzkt.api
+ }/v1/contracts/${address}/storage?${mkQueryParams(params)}`;
+ const response = await axios.get(uri);
+ return response.data;
+}
+
+export class TzKt {
+ config: Config;
+
+ constructor(config: Config) {
+ this.config = config;
+ }
+
+ getBigMapKeys(id: number, params?: Params) {
+ return getBigMapKeys(this.config, id, params);
+ }
+
+ getBigMapUpdates(params?: Params) {
+ return getBigMapUpdates(this.config, params);
+ }
+
+ getContracts(params?: Params) {
+ return getContracts(this.config, params);
+ }
+
+ getContract(address: string, params?: Params) {
+ return getContract(this.config, address, params);
+ }
+
+ getContractBigMapKeys(address: string, name: string, params?: Params) {
+ return getContractBigMapKeys(this.config, address, name, params);
+ }
+
+ getContractStorage(address: string, params?: Params) {
+ return getContractStorage(this.config, address, params);
+ }
+}
diff --git a/src/lib/system.ts b/src/lib/system.ts
index 1f4c91e2..6488ff65 100644
--- a/src/lib/system.ts
+++ b/src/lib/system.ts
@@ -1,4 +1,10 @@
-import { TezosToolkit, MichelCodecPacker, Context } from '@taquito/taquito';
+import {
+ TezosToolkit,
+ MichelCodecPacker,
+ Context,
+ ContractAbstraction,
+ ContractProvider
+} from '@taquito/taquito';
import { BeaconWallet } from '@taquito/beacon-wallet';
import { MetadataProvider, DEFAULT_HANDLERS } from '@taquito/tzip16';
import { Tzip12Module } from '@taquito/tzip12';
@@ -6,6 +12,8 @@ import CustomIpfsHttpHandler from './util/taquito-custom-ipfs-http-handler';
import { BetterCallDev } from './service/bcd';
import * as tzUtils from './util/tezosToolkit';
import { DAppClientOptions, NetworkType } from '@airgap/beacon-sdk';
+import { TzKt } from './service/tzkt';
+import { isIpfsUri } from './util/ipfs';
export interface Config {
rpc: string;
@@ -14,6 +22,9 @@ export interface Config {
api: string;
gui: string;
};
+ tzkt: {
+ api: string;
+ };
contracts: {
nftFaucet: string;
marketplace: {
@@ -23,6 +34,7 @@ export interface Config {
};
};
ipfsApi: string;
+ ipfsGateway: string;
}
export enum Status {
@@ -35,6 +47,7 @@ export interface SystemConfigured {
status: Status.Configured;
config: Config;
betterCallDev: BetterCallDev;
+ tzkt: TzKt;
toolkit: null;
wallet: null;
walletReconnectAttempted: boolean;
@@ -42,13 +55,15 @@ export interface SystemConfigured {
}
type ResolveMetadata = (
- uri: string
+ uri: string,
+ address: string
) => ReturnType;
export interface SystemWithToolkit {
status: Status.ToolkitConnected;
config: Config;
betterCallDev: BetterCallDev;
+ tzkt: TzKt;
toolkit: TezosToolkit;
resolveMetadata: ResolveMetadata;
wallet: null;
@@ -60,6 +75,7 @@ export interface SystemWithWallet {
status: Status.WalletConnected;
config: Config;
betterCallDev: BetterCallDev;
+ tzkt: TzKt;
toolkit: TezosToolkit;
resolveMetadata: ResolveMetadata;
wallet: BeaconWallet;
@@ -82,6 +98,7 @@ export function configure(config: Config): SystemConfigured {
status: Status.Configured,
config: compatibilityConfig,
betterCallDev: new BetterCallDev(compatibilityConfig),
+ tzkt: new TzKt(compatibilityConfig),
toolkit: null,
wallet: null,
walletReconnectAttempted: false,
@@ -94,28 +111,28 @@ function createMetadataResolver(
toolkit: TezosToolkit,
contractAddress: string
): ResolveMetadata {
- const ipfsGateway =
- system.config.network === 'sandboxnet'
- ? 'localhost:8080'
- : 'gateway.pinata.cloud';
- const gatewayProtocol =
- system.config.network === 'sandboxnet' ? 'http' : 'https';
+ const ipfsUrl = system.config.ipfsGateway;
+ const ipfsGateway = ipfsUrl.replace(/^https?:\/\//, '');
+ const gatewayProtocol = ipfsUrl.startsWith('https') ? 'https' : 'http';
+
const ipfsHandler = new CustomIpfsHttpHandler(ipfsGateway, gatewayProtocol);
DEFAULT_HANDLERS.set('ipfs', ipfsHandler);
const provider = new MetadataProvider(DEFAULT_HANDLERS);
const context = new Context(toolkit.rpc);
- // This is a performance optimization: We're only resolving off-chain
- // metadata, however the storage handler requires a ContractAbstraction
- // instance present - if we fetch a contract on each invokation, the time
- // to resolution can take several hundred milliseconds.
- //
- // TODO: Is it possible to only fetch contracts at the storage resolver level
- // and make an "off-chain" metadata resolver that excludes the need for a
- // ContractAbstraction instance?
+
const defaultContract = toolkit.contract.at(contractAddress);
- return async uri => {
- const contract = await defaultContract;
- return provider.provideMetadata(contract, uri, context);
+ type Contract = ContractAbstraction;
+ const contractCache: Record = {};
+
+ return async (uri, address) => {
+ if (isIpfsUri(uri)) {
+ const contract = await defaultContract;
+ return provider.provideMetadata(contract, uri, context);
+ }
+ if (!contractCache[address]) {
+ contractCache[address] = await toolkit.contract.at(address);
+ }
+ return provider.provideMetadata(contractCache[address], uri, context);
};
}
diff --git a/src/lib/util/ipfs.ts b/src/lib/util/ipfs.ts
index c065b476..b5ff1311 100644
--- a/src/lib/util/ipfs.ts
+++ b/src/lib/util/ipfs.ts
@@ -33,6 +33,10 @@ export async function uploadIPFSImageWithThumbnail(api: string, file: File) {
// URI Utils
+export function isIpfsUri(uri: string) {
+ return /^ipfs:\/\/.+/.test(uri);
+}
+
export function ipfsUriToCid(uri: string) {
const baseRegex = /^ipfs:\/\//;
const ipfsRegex = new RegExp(baseRegex.source + '.+');
@@ -42,13 +46,10 @@ export function ipfsUriToCid(uri: string) {
return null;
}
-export function ipfsUriToGatewayUrl(network: string, uri: string) {
- const ipfsHost =
- network === 'sandboxnet'
- ? 'http://localhost:8080'
- : 'https://gateway.pinata.cloud';
+export type IpfsGatewayConfig = { ipfsGateway: string };
+export function ipfsUriToGatewayUrl(config: IpfsGatewayConfig, uri: string) {
const cid = ipfsUriToCid(uri);
- return cid ? `${ipfsHost}/ipfs/${cid}` : uri;
+ return cid ? `${config.ipfsGateway}/ipfs/${cid}` : uri;
}
export function uriToCid(uri: string) {
diff --git a/src/lib/util/tezosToolkit.ts b/src/lib/util/tezosToolkit.ts
index 9bdbb08a..87d6c2d5 100644
--- a/src/lib/util/tezosToolkit.ts
+++ b/src/lib/util/tezosToolkit.ts
@@ -1,11 +1,14 @@
-import { TezosToolkit } from "@taquito/taquito";
+import { TezosToolkit } from '@taquito/taquito';
-export const setConfirmationPollingInterval = async (tzToolkit: TezosToolkit) => {
+export const setConfirmationPollingInterval = async (
+ tzToolkit: TezosToolkit
+) => {
const constants = await tzToolkit.rpc.getConstants();
-
+
// Polling interval has to be smaller than the time between block
// or TezosToolkit throws an exception. Here we pick 1/5 of the time
// between blocks.
- const confirmationPollingIntervalSecond = Number(constants.time_between_blocks[0]) / 5;
- tzToolkit.setProvider({ config: { confirmationPollingIntervalSecond } });
-}
\ No newline at end of file
+ const confirmationPollingIntervalSecond =
+ Number(constants.time_between_blocks[0]) / 5;
+ tzToolkit.setProvider({ config: { confirmationPollingIntervalSecond } });
+};
diff --git a/src/reducer/async/actions.ts b/src/reducer/async/actions.ts
index db381f30..abb6382a 100644
--- a/src/reducer/async/actions.ts
+++ b/src/reducer/async/actions.ts
@@ -18,7 +18,7 @@ import {
} from '../../lib/util/ipfs';
import { SelectedFile } from '../slices/createNft';
import { connectWallet } from './wallet';
-import { NftMetadata } from '../../lib/nfts/queries';
+import { NftMetadata } from '../../lib/nfts/decoders';
import { SystemWithToolkit, SystemWithWallet } from '../../lib/system';
import { notifyPending, notifyFulfilled } from '../slices/notificationsActions';
@@ -34,8 +34,18 @@ export const readFileAsDataUrlAction = createAsyncThunk<
>('action/readFileAsDataUrl', async ({ ns, file }, { rejectWithValue }) => {
const readFile = new Promise<{ ns: string; result: SelectedFile }>(
(resolve, reject) => {
- const { name, type, size } = file;
+ let { name, type, size } = file;
const reader = new FileReader();
+
+ if (!type) {
+ if (name.substr(-4) === '.glb') {
+ type = 'model/gltf-binary';
+ }
+ if (name.substr(-5) === '.gltf') {
+ type = 'model/gltf+json';
+ }
+ }
+
reader.onload = e => {
const buffer = e.target?.result;
if (!buffer || !(buffer instanceof ArrayBuffer)) {
@@ -98,28 +108,23 @@ function appendStateMetadata(
metadata: NftMetadata,
system: SystemWithToolkit | SystemWithWallet
) {
- const appendedMetadata = { ...metadata };
- appendedMetadata.name = state.fields.name as string;
+ const appendedMetadata: NftMetadata = {
+ ...metadata,
+ name: state.fields.name as string,
+ minter: system.tzPublicKey || undefined,
+ description: state.fields.description || undefined,
+ attributes: []
+ };
- if (state.fields.description) {
- appendedMetadata.description = state.fields.description;
- }
-
- for (let row of state.attributes) {
- if (row.name !== null && row.value !== null) {
- const keys = Object.getOwnPropertyNames(new NftMetadata());
- if (keys.indexOf(row.name) !== -1) {
- appendedMetadata[row.name as keyof NftMetadata] = row.value;
- } else {
- if (!appendedMetadata.attributes) appendedMetadata.attributes = [];
- appendedMetadata.attributes.push({ name: row.name, value: row.value });
- }
+ return state.attributes.reduce((acc, row) => {
+ const keys = Object.keys(NftMetadata.props);
+ const key = keys.find(k => k === row.name) as keyof NftMetadata;
+ if (key && NftMetadata.props[key].decode(row.value)._tag === 'Right') {
+ return { ...acc, [key]: row.value };
}
- }
-
- appendedMetadata.minter = system.tzPublicKey || '';
-
- return appendedMetadata;
+ const attribute = { name: row.name, value: row.value };
+ return { ...acc, attributes: [...acc.attributes!, attribute] };
+ }, appendedMetadata);
}
export const mintTokenAction = createAsyncThunk<
@@ -205,8 +210,8 @@ export const mintTokenAction = createAsyncThunk<
ipfsMetadata.thumbnailUri = imageResponse.data.thumbnail.ipfsUri;
ipfsMetadata.formats = [
{
- fileSize: imageResponse.headers['content-length'],
- mimeType: imageResponse.headers['content-type']
+ fileSize: fileResponse.headers['content-length'],
+ mimeType: fileResponse.headers['content-type']
}
];
} else {
@@ -214,8 +219,8 @@ export const mintTokenAction = createAsyncThunk<
ipfsMetadata.artifactUri = fileResponse.data.ipfsUri;
ipfsMetadata.formats = [
{
- fileSize: fileResponse.headers['content-length'],
- mimeType: fileResponse.headers['content-type']
+ fileSize: fileResponse.data.size,
+ mimeType: file.type
}
];
}
diff --git a/src/reducer/async/queries.ts b/src/reducer/async/queries.ts
index 232183e0..97e02b53 100644
--- a/src/reducer/async/queries.ts
+++ b/src/reducer/async/queries.ts
@@ -2,12 +2,13 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import { State } from '../index';
import {
getNftAssetContract,
- AssetContract,
getContractNfts,
getMarketplaceNfts,
- Nft,
- getWalletNftAssetContracts
+ getWalletNftAssetContracts,
+ MarketplaceNftLoadingData,
+ loadMarketplaceNft
} from '../../lib/nfts/queries';
+import { Nft, AssetContract } from '../../lib/nfts/decoders';
import { ErrorKind, RejectValue } from './errors';
type Opts = { state: State; rejectValue: RejectValue };
@@ -34,14 +35,16 @@ export const getContractNftsQuery = createAsyncThunk<
string,
Opts
>('query/getContractNfts', async (address, { getState, rejectWithValue }) => {
- const { system } = getState();
+ const { system, collections } = getState();
try {
const tokens = await getContractNfts(system, address);
return { address, tokens };
} catch (e) {
return rejectWithValue({
kind: ErrorKind.GetContractNftsFailed,
- message: `Failed to retrieve contract nfts from: ${address}`
+ message: `Failed to retrieve contract nfts from: ${
+ collections.collections[address]?.metadata?.name ?? address
+ }`
});
}
});
@@ -64,6 +67,7 @@ export const getWalletAssetContractsQuery = createAsyncThunk<
try {
return await getWalletNftAssetContracts(system);
} catch (e) {
+ console.log(e);
return rejectWithValue({
kind: ErrorKind.GetWalletNftAssetContractsFailed,
message: "Failed to retrieve wallet's asset contracts"
@@ -73,18 +77,59 @@ export const getWalletAssetContractsQuery = createAsyncThunk<
);
export const getMarketplaceNftsQuery = createAsyncThunk<
- { tokens: Nft[] },
+ { tokens: MarketplaceNftLoadingData[] },
string,
Opts
->('query/getMarketplaceNfts', async (address, { getState, rejectWithValue }) => {
- const { system } = getState();
- try {
- const tokens = await getMarketplaceNfts(system, address);
- return { tokens };
- } catch (e) {
- return rejectWithValue({
- kind: ErrorKind.GetMarketplaceNftsFailed,
- message: `Failed to retrieve marketplace nfts from: ${address}`
- });
+>(
+ 'query/getMarketplaceNfts',
+ async (address, { getState, rejectWithValue }) => {
+ const { system } = getState();
+ try {
+ const tokens = await getMarketplaceNfts(system, address);
+
+ // Load 9 initially (1-feature + at least 2 rows)
+ for (const i in tokens.slice(0, 9)) {
+ tokens[i] = await loadMarketplaceNft(system, tokens[i]);
+ }
+
+ return { tokens };
+ } catch (e) {
+ return rejectWithValue({
+ kind: ErrorKind.GetMarketplaceNftsFailed,
+ message: `Failed to retrieve marketplace nfts from: ${address}`
+ });
+ }
}
-});
+);
+
+export const loadMoreMarketplaceNftsQuery = createAsyncThunk<
+ { tokens: MarketplaceNftLoadingData[] },
+ {},
+ Opts
+>(
+ 'query/loadMoreMarketplaceNftsQuery',
+ async (_, { getState, rejectWithValue }) => {
+ const { system, marketplace } = getState();
+ try {
+ const tokens = marketplace.marketplace.tokens ?? [];
+
+ // Load 8 more (at least 2 rows)
+ const iStart = tokens.findIndex(x => !x.loaded);
+ const iEnd = iStart + 8;
+
+ // Need to rebuild the array
+ const tokensAfter = await Promise.all(
+ tokens.map(async (x, i) =>
+ i >= iStart && i < iEnd ? await loadMarketplaceNft(system, x) : x
+ )
+ );
+
+ return { tokens: tokensAfter };
+ } catch (e) {
+ return rejectWithValue({
+ kind: ErrorKind.GetMarketplaceNftsFailed,
+ message: `Failed to load marketplace nfts`
+ });
+ }
+ }
+);
diff --git a/src/reducer/slices/collections.ts b/src/reducer/slices/collections.ts
index 0559d197..111a38e1 100644
--- a/src/reducer/slices/collections.ts
+++ b/src/reducer/slices/collections.ts
@@ -4,7 +4,7 @@ import {
getNftAssetContractQuery,
getWalletAssetContractsQuery
} from '../async/queries';
-import { Nft, AssetContract } from '../../lib/nfts/queries';
+import { Nft, AssetContract } from '../../lib/nfts/decoders';
import config from '../../config.json';
//// State
@@ -28,21 +28,10 @@ type Reducer = CaseReducer>;
// Data
-const globalCollectionAddress = config.contracts.nftFaucet;
-
export const initialState: CollectionsState = {
selectedCollection: null,
- globalCollection: globalCollectionAddress,
- collections: {
- [globalCollectionAddress]: {
- address: globalCollectionAddress,
- metadata: {
- name: 'Minter'
- },
- tokens: null,
- loaded: false
- }
- }
+ globalCollection: config.contracts.nftFaucet,
+ collections: {}
};
//// Reducers & Slice
@@ -59,14 +48,22 @@ const populateCollectionR: PopulateCollection = (state, { payload }) => {
const updateCollectionsR: Reducer = (state, action) => {
for (let coll of action.payload) {
if (!state.collections[coll.address]) {
- state.collections[coll.address] = { ...coll, tokens: null, loaded: false };
+ state.collections[coll.address] = {
+ ...coll,
+ tokens: null,
+ loaded: false
+ };
}
}
};
const updateCollectionR: Reducer = (state, { payload }) => {
if (!state.collections[payload.address]) {
- state.collections[payload.address] = { ...payload, tokens: null, loaded: false };
+ state.collections[payload.address] = {
+ ...payload,
+ tokens: null,
+ loaded: false
+ };
}
};
diff --git a/src/reducer/slices/createNft.ts b/src/reducer/slices/createNft.ts
index da318edf..ba3ac6fe 100644
--- a/src/reducer/slices/createNft.ts
+++ b/src/reducer/slices/createNft.ts
@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
-import { NftMetadataAttribute } from '../../lib/nfts/queries';
+import { NftMetadataAttribute } from '../../lib/nfts/decoders';
import { readFileAsDataUrlAction } from '../async/actions';
// State
@@ -96,7 +96,7 @@ const slice = createSlice({
state.displayImageFile = null;
},
addMetadataRow(state) {
- state.attributes.push({ name: null, value: null });
+ state.attributes.push({ name: '', value: '' });
},
updateMetadataRowName(state, action: UpdateRowNameAction) {
if (state.attributes[action.payload.key]) {
diff --git a/src/reducer/slices/marketplace.ts b/src/reducer/slices/marketplace.ts
index 4d557e3d..880fbb97 100644
--- a/src/reducer/slices/marketplace.ts
+++ b/src/reducer/slices/marketplace.ts
@@ -1,6 +1,10 @@
import { createSlice, PayloadAction, CaseReducer } from '@reduxjs/toolkit';
-import { getMarketplaceNftsQuery } from '../async/queries';
-import { Nft } from '../../lib/nfts/queries';
+import {
+ getMarketplaceNftsQuery,
+ loadMoreMarketplaceNftsQuery
+} from '../async/queries';
+import { Nft } from '../../lib/nfts/decoders';
+import { MarketplaceNftLoadingData } from '../../lib/nfts/queries';
import config from '../../config.json';
//// State
@@ -11,7 +15,7 @@ export type Token = Nft;
export interface Marketplace {
address: string;
- tokens: Token[] | null;
+ tokens: MarketplaceNftLoadingData[] | null;
loaded: boolean;
}
@@ -35,7 +39,7 @@ export const initialState: MarketplaceState = {
//// Reducers & Slice
-type PopulateMarketplace = Reducer<{ tokens: Token[] }>;
+type PopulateMarketplace = Reducer<{ tokens: MarketplaceNftLoadingData[] }>;
const populateMarketplaceR: PopulateMarketplace = (state, { payload }) => {
state.marketplace.tokens = payload.tokens;
@@ -50,11 +54,10 @@ const slice = createSlice({
},
extraReducers: ({ addCase }) => {
addCase(getMarketplaceNftsQuery.fulfilled, populateMarketplaceR);
+ addCase(loadMoreMarketplaceNftsQuery.fulfilled, populateMarketplaceR);
}
});
-export const {
- populateMarketplace
-} = slice.actions;
+export const { populateMarketplace } = slice.actions;
export default slice;
diff --git a/src/reducer/slices/system.ts b/src/reducer/slices/system.ts
index 7d0b3c56..7036716d 100644
--- a/src/reducer/slices/system.ts
+++ b/src/reducer/slices/system.ts
@@ -1,7 +1,11 @@
import { Minter, SystemWithToolkit, SystemWithWallet } from '../../lib/system';
import { createSlice } from '@reduxjs/toolkit';
import config from '../../config.json';
-import { connectWallet, disconnectWallet, reconnectWallet } from '../async/wallet';
+import {
+ connectWallet,
+ disconnectWallet,
+ reconnectWallet
+} from '../async/wallet';
const initialState = Minter.connectToolkit(Minter.configure(config)) as
| SystemWithToolkit
diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts
index b09523f1..3385f545 100644
--- a/src/serviceWorker.ts
+++ b/src/serviceWorker.ts
@@ -28,10 +28,7 @@ type Config = {
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
- const publicUrl = new URL(
- process.env.PUBLIC_URL,
- window.location.href
- );
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
diff --git a/tsconfig.json b/tsconfig.json
index 7f2c9dd3..7902a906 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -18,10 +18,6 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
-
- // The below makes typescript more strict about types
- // Consider changing them if you run into problems with
- // existing libraries
"noImplicitAny": true,
"strictNullChecks": true,
"noImplicitThis": true,
diff --git a/yarn.lock b/yarn.lock
index db1e2579..ad286cc3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2404,10 +2404,10 @@
resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-7.1.2.tgz#3a71bb8a45a1e08b71a54c9efcee9927f3895e80"
integrity sha512-lDyCVxxgX5lrgCa75ELCfWcdEDyfisjqoDIM3YsghQ+lyViIac/qT67qabQ/HmoVxyikFKovjKwWdn3b/oKhZA==
-"@tqtezos/minter-contracts@1.0.3":
- version "1.0.3"
- resolved "https://registry.yarnpkg.com/@tqtezos/minter-contracts/-/minter-contracts-1.0.3.tgz#e46e6706f72d5a200c2be96585d67aa211ee048d"
- integrity sha512-FmOlwRSCR38o41Kcnlxs0D27K2/D/yHt9iUJ+mfPOVAvAut/Lq4IWfRpZIbdQ4oWTY/Wb2//XjsZ9ptzOB+4Aw==
+"@tqtezos/minter-contracts@1.2.0":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@tqtezos/minter-contracts/-/minter-contracts-1.2.0.tgz#5b98addeb6428e7cba27a35475fa601a32df6737"
+ integrity sha512-KrgdApZnHzTzedUjsNzWxEWYisyeXvd2NIfUj+CcmzL00708EgCbX1zXOo5qLcsRKuffpJGbeARH32rWKUGRAw==
"@tsed/logger@5.5.2":
version "5.5.2"
@@ -2473,11 +2473,33 @@
"@types/filesystem" "*"
"@types/har-format" "*"
+"@types/clear@0.1.1":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@types/clear/-/clear-0.1.1.tgz#f5b5217d74f540682dce941f6f6323768ab1bc91"
+ integrity sha512-Wu6DxCnSjFiqymbTeyb63VdU1oKYW0qCnmOSBjpMyuvcuvI9keXfS6RbEcKYqUY0dPOLa34qV+XHAdgiRzPBtg==
+
+"@types/cli-color@*":
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/@types/cli-color/-/cli-color-2.0.0.tgz#dc64e32da0fb9ea1814300fb468a58e833ce71a6"
+ integrity sha512-E2Oisr73FjwxMHkYU6RcN9P9mmrbG4TNQMIebWhazYxOgWRzA7s4hM+DtAs6ZwiwKFbPst42v1XUAC1APIhRJA==
+
+"@types/clui@0.3.0":
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/@types/clui/-/clui-0.3.0.tgz#a7512770d50c06b403e018c46f850a0064e53c74"
+ integrity sha512-4GM6iuKwOs4Lq2qUWbxw8LMiamh6YXEuPq4uKeYd7SfFWNK1sNsw41M9GjIhwbIRBOaVgxkutZLLdfZwSNDwtg==
+ dependencies:
+ "@types/cli-color" "*"
+
"@types/configstore@4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/configstore/-/configstore-4.0.0.tgz#cb718f9507e9ee73782f40d07aaca1cd747e36fa"
integrity sha512-SvCBBPzOIe/3Tu7jTl2Q8NjITjLmq9m7obzjSyb8PXWWZ31xVK6w4T6v8fOx+lrgQnqk3Yxc00LDolFsSakKCA==
+"@types/figlet@1.5.1":
+ version "1.5.1"
+ resolved "https://registry.yarnpkg.com/@types/figlet/-/figlet-1.5.1.tgz#a9f06317b8900b7d3ad8d8bb577ec26225ac16a8"
+ integrity sha512-dwOwRPJY122FcWRdiXic+H72AOD+Cx69NO6Z9STtm9eIvM47qBe0vXdD/w4ad+ygIqvSTdnQyeMwlxotDjlvPg==
+
"@types/filesystem@*":
version "0.0.29"
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.29.tgz#ee3748eb5be140dcf980c3bd35f11aec5f7a3748"
@@ -2490,7 +2512,7 @@
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3"
integrity sha1-wFTor02d11205jq8dviFFocU1LM=
-"@types/glob@^7.1.1":
+"@types/glob@*", "@types/glob@^7.1.1":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183"
integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==
@@ -2511,6 +2533,14 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
+"@types/inquirer@7.3.1":
+ version "7.3.1"
+ resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d"
+ integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g==
+ dependencies:
+ "@types/through" "*"
+ rxjs "^6.4.0"
+
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762"
@@ -2657,6 +2687,14 @@
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==
+"@types/shelljs@0.8.8":
+ version "0.8.8"
+ resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.8.8.tgz#e439c69929b88a2c8123c1a55e09eb708315addf"
+ integrity sha512-lD3LWdg6j8r0VRBFahJVaxoW0SIcswxKaFUrmKl33RJVeeoNYQAz4uqCJ5Z6v4oIBOsC5GozX+I5SorIKiTcQA==
+ dependencies:
+ "@types/glob" "*"
+ "@types/node" "*"
+
"@types/stack-utils@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
@@ -2690,6 +2728,13 @@
"@types/testing-library__dom" "*"
pretty-format "^25.1.0"
+"@types/through@*":
+ version "0.0.30"
+ resolved "https://registry.yarnpkg.com/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895"
+ integrity sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==
+ dependencies:
+ "@types/node" "*"
+
"@types/tinycolor2@1.4.2":
version "1.4.2"
resolved "https://registry.yarnpkg.com/@types/tinycolor2/-/tinycolor2-1.4.2.tgz#721ca5c5d1a2988b4a886e35c2ffc5735b6afbdf"
@@ -4168,6 +4213,21 @@ clean-stack@^2.0.0:
resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b"
integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==
+clear@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/clear/-/clear-0.1.0.tgz#b81b1e03437a716984fd7ac97c87d73bdfe7048a"
+ integrity sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==
+
+cli-color@0.3.2:
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-0.3.2.tgz#75fa5f728c308cc4ac594b05e06cc5d80daccd86"
+ integrity sha1-dfpfcowwjMSsWUsF4GzF2A2szYY=
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.2"
+ memoizee "0.3.x"
+ timers-ext "0.1.x"
+
cli-cursor@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307"
@@ -4223,6 +4283,13 @@ clone-deep@^4.0.1:
kind-of "^6.0.2"
shallow-clone "^3.0.0"
+clui@0.3.6:
+ version "0.3.6"
+ resolved "https://registry.yarnpkg.com/clui/-/clui-0.3.6.tgz#8e1e5cea7332a6e54083f59da0ccbe1d6f2fa787"
+ integrity sha512-Z4UbgZILlIAjkEkZiDOa2aoYjohKx7fa6DxIh6cE9A6WNWZ61iXfQc6CmdC9SKdS5nO0P0UyQ+WfoXfB65e3HQ==
+ dependencies:
+ cli-color "0.3.2"
+
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -4872,6 +4939,13 @@ d@1, d@^1.0.1:
es5-ext "^0.10.50"
type "^1.0.1"
+d@~0.1.1:
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
+ integrity sha1-2hhMU10Y2O57oqoim5FACfrhEwk=
+ dependencies:
+ es5-ext "~0.10.2"
+
damerau-levenshtein@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791"
@@ -5391,7 +5465,7 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
-es5-ext@^0.10.35, es5-ext@^0.10.50:
+es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.11, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46, es5-ext@~0.10.5, es5-ext@~0.10.6:
version "0.10.53"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.53.tgz#93c5a3acfdbef275220ad72644ad02ee18368de1"
integrity sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==
@@ -5409,6 +5483,15 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3:
es5-ext "^0.10.35"
es6-symbol "^3.1.1"
+es6-iterator@~0.1.3:
+ version "0.1.3"
+ resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-0.1.3.tgz#d6f58b8c4fc413c249b4baa19768f8e4d7c8944e"
+ integrity sha1-1vWLjE/EE8JJtLqhl2j45NfIlE4=
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.5"
+ es6-symbol "~2.0.1"
+
es6-symbol@^3.1.1, es6-symbol@~3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18"
@@ -5417,6 +5500,24 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.3:
d "^1.0.1"
ext "^1.1.2"
+es6-symbol@~2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-2.0.1.tgz#761b5c67cfd4f1d18afb234f691d678682cb3bf3"
+ integrity sha1-dhtcZ8/U8dGK+yNPaR1nhoLLO/M=
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.5"
+
+es6-weak-map@~0.1.4:
+ version "0.1.4"
+ resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-0.1.4.tgz#706cef9e99aa236ba7766c239c8b9e286ea7d228"
+ integrity sha1-cGzvnpmqI2undmwjnIueKG6n0ig=
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.6"
+ es6-iterator "~0.1.3"
+ es6-symbol "~2.0.1"
+
escalade@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -5682,6 +5783,14 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+event-emitter@~0.3.4:
+ version "0.3.5"
+ resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
+ integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
eventemitter3@^4.0.0:
version "4.0.7"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
@@ -5935,6 +6044,11 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
+figlet@1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/figlet/-/figlet-1.5.0.tgz#2db4d00a584e5155a96080632db919213c3e003c"
+ integrity sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww==
+
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@@ -6153,6 +6267,11 @@ forwarded@~0.1.2:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
+fp-ts@2.10.3:
+ version "2.10.3"
+ resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.10.3.tgz#79fe4c7b876d0137a98e287737c3faf63bdcd1dd"
+ integrity sha512-Lq9XweGms3tAmCh1AAxBG+1PfBY1zKQ3kD52q3Db6SgoA4xIUKLFZQBhmuZ7fCGmhUPZF32rlSX2/QBP0VMdjg==
+
fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@@ -6341,7 +6460,7 @@ glob-to-regexp@^0.3.0:
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab"
integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=
-glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
+glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
version "7.1.6"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
@@ -6927,6 +7046,11 @@ internal-slot@^1.0.2:
has "^1.0.3"
side-channel "^1.0.2"
+interpret@^1.0.0:
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e"
+ integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==
+
invariant@^2.2.2, invariant@^2.2.4:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
@@ -6939,6 +7063,11 @@ invert-kv@^2.0.0:
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
+io-ts@2.2.16:
+ version "2.2.16"
+ resolved "https://registry.yarnpkg.com/io-ts/-/io-ts-2.2.16.tgz#597dffa03db1913fc318c9c6df6931cb4ed808b2"
+ integrity sha512-y5TTSa6VP6le0hhmIyN0dqEXkrZeJLeC5KApJq6VLci3UEKF80lZ+KuoUs02RhBxNWlrqSNxzfI7otLX1Euv8Q==
+
ip-regex@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9"
@@ -7045,6 +7174,13 @@ is-core-module@^2.1.0:
dependencies:
has "^1.0.3"
+is-core-module@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.3.0.tgz#d341652e3408bca69c4671b79a0954a3d349f887"
+ integrity sha512-xSphU2KG9867tsYdLD4RWQ1VqdFl4HTO9Thf3I/3dLEfr0dbPTWKsuCKrgqMljg4nPE+Gq0VCnzT3gr0CyBmsw==
+ dependencies:
+ has "^1.0.3"
+
is-data-descriptor@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56"
@@ -8246,6 +8382,13 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+lru-queue@0.1:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
+ integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=
+ dependencies:
+ es5-ext "~0.10.2"
+
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
@@ -8335,6 +8478,19 @@ mem@^4.0.0:
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
+memoizee@0.3.x:
+ version "0.3.10"
+ resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.3.10.tgz#4eca0d8aed39ec9d017f4c5c2f2f6432f42e5c8f"
+ integrity sha1-TsoNiu057J0Bf0xcLy9kMvQuXI8=
+ dependencies:
+ d "~0.1.1"
+ es5-ext "~0.10.11"
+ es6-weak-map "~0.1.4"
+ event-emitter "~0.3.4"
+ lru-queue "0.1"
+ next-tick "~0.2.2"
+ timers-ext "0.1"
+
memory-fs@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552"
@@ -8643,6 +8799,16 @@ neo-async@^2.5.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
+next-tick@1:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
+ integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
+next-tick@~0.2.2:
+ version "0.2.2"
+ resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-0.2.2.tgz#75da4a927ee5887e39065880065b7336413b310d"
+ integrity sha1-ddpKkn7liH45BliABltzNkE7MQ0=
+
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
@@ -10593,6 +10759,13 @@ realpath-native@^1.1.0:
dependencies:
util.promisify "^1.0.0"
+rechoir@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"
+ integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=
+ dependencies:
+ resolve "^1.1.6"
+
recursive-readdir@2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
@@ -10853,6 +11026,14 @@ resolve@1.15.0:
dependencies:
path-parse "^1.0.6"
+resolve@^1.1.6:
+ version "1.20.0"
+ resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975"
+ integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==
+ dependencies:
+ is-core-module "^2.2.0"
+ path-parse "^1.0.6"
+
resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.8.1:
version "1.19.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c"
@@ -10959,6 +11140,13 @@ rx-sandbox@^1.0.3:
expect "^26.6.1"
jest-matcher-utils "^26.6.1"
+rxjs@^6.4.0:
+ version "6.6.7"
+ resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
+ integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==
+ dependencies:
+ tslib "^1.9.0"
+
rxjs@^6.5.3, rxjs@^6.6.0, rxjs@^6.6.3:
version "6.6.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.3.tgz#8ca84635c4daa900c0d3967a6ee7ac60271ee552"
@@ -11229,6 +11417,15 @@ shell-quote@1.7.2:
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2"
integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==
+shelljs@0.8.4:
+ version "0.8.4"
+ resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2"
+ integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ==
+ dependencies:
+ glob "^7.0.0"
+ interpret "^1.0.0"
+ rechoir "^0.6.2"
+
shellwords@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
@@ -11913,6 +12110,14 @@ timers-browserify@^2.0.4:
dependencies:
setimmediate "^1.0.4"
+timers-ext@0.1, timers-ext@0.1.x:
+ version "0.1.7"
+ resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6"
+ integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==
+ dependencies:
+ es5-ext "~0.10.46"
+ next-tick "1"
+
timsort@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"