feat: Add utility code for NFT Features (#4256)

* import nft utilities

* re-add featureflag provider

* add back in useCurrencyBalanceString fn

* remove static assets for separate pr

* remove resolutions, swap dev and regular dependencies, respond to comments

* remove currently unused dependencies and resynth .lock

* build: update lockfile

* build: update lockfile

* remove env check

* useCurrencyBalanceString as fn

* remove supported_Wallets until wallet component merged in

* make Atoms an interface

* update abis

* remove outdated comment

* update usedebounce hook

* remove useDarkMode

* remove useLazyEffect

* remove getEtherscan helper fn

* remove useLastWallet

* remove useWindowDimensions

* refactor hooks

* move hooks from nft to general folder

* add walletBalanceInterface and remove wrongNetwork hook

* remove empty obj

* remove ethers imports

* fixed comparison

* same line eslint ignore

* gtag removed

* revert

* revert

* build: update lockfile

* remove walletinfo interface

* remove newline

* remove tslinst exception from isMobile

* remove hiding linter warnings

* remove unused util

* fix linter warnings

Co-authored-by: Charles Bachmeier <charlie@genie.xyz>
Co-authored-by: Zach Pomerantz <zzmp@uniswap.org>
This commit is contained in:
Charles Bachmeier 2022-08-10 16:38:23 -07:00 committed by GitHub
parent 783f197463
commit 8dbc91ee6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 7436 additions and 213 deletions

19
craco.config.cjs Normal file

@ -0,0 +1,19 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
babel: {
plugins: ['@vanilla-extract/babel-plugin'],
},
webpack: {
plugins: [new VanillaExtractPlugin()],
configure: (webpackConfig) => {
const instanceOfMiniCssExtractPlugin = webpackConfig.plugins.find(
(plugin) => plugin instanceof MiniCssExtractPlugin
)
if (instanceOfMiniCssExtractPlugin !== undefined) instanceOfMiniCssExtractPlugin.options.ignoreOrder = true
return webpackConfig
},
},
}

@ -14,11 +14,11 @@
"i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
"start": "react-scripts start",
"build": "react-scripts build",
"start": "craco start",
"build": "craco build",
"serve": "serve build -l 3000",
"lint": "yarn eslint .",
"test": "react-scripts test --coverage",
"test": "craco test --coverage",
"cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e"
},
@ -55,6 +55,7 @@
]
},
"devDependencies": {
"@craco/craco": "6.4.3",
"@ethersproject/experimental": "^5.4.0",
"@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/typescript": "1.22.3",
@ -85,6 +86,7 @@
"@types/styled-components": "^5.1.25",
"@types/testing-library__cypress": "^5.0.5",
"@types/ua-parser-js": "^0.7.35",
"@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4",
"@typescript-eslint/parser": "^4",
@ -104,7 +106,9 @@
"react-scripts": "^4.0.3",
"serve": "^11.3.2",
"typechain": "^5.0.0",
"typescript": "^4.4.3"
"typescript": "^4.4.3",
"@vanilla-extract/babel-plugin": "^1.1.7",
"@vanilla-extract/webpack-plugin": "^2.1.11"
},
"dependencies": {
"@amplitude/analytics-browser": "^0.5.1",
@ -115,7 +119,9 @@
"@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0",
"@looksrare/sdk": "^0.7.1",
"@metamask/jazzicon": "^2.0.0",
"@opensea/seaport-js": "^1.0.2",
"@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3",
@ -135,6 +141,10 @@
"@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0",
"@vanilla-extract/css": "^1.7.2",
"@vanilla-extract/css-utils": "^0.1.2",
"@vanilla-extract/dynamic": "^2.0.2",
"@vanilla-extract/sprinkles": "^1.4.1",
"@visx/axis": "^2.12.2",
"@visx/event": "^2.6.0",
"@visx/glyph": "^2.10.0",
@ -156,11 +166,13 @@
"array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4",
"cids": "^1.0.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.2.0",
"d3": "^7.6.1",
"d3-curve-circlecorners": "^0.1.6",
"ethers": "^5.1.4",
"firebase": "^9.1.3",
"focus-visible": "^5.2.0",
"fortmatic": "^2.4.0",
"graphql": "^15.5.0",
"graphql-request": "^3.4.0",
@ -185,9 +197,11 @@
"react-is": "^17.0.2",
"react-markdown": "^4.3.1",
"react-popper": "^2.2.3",
"react-query": "^3.39.1",
"react-redux": "^8.0.2",
"react-router-dom": "^6.3.0",
"react-spring": "^8.0.27",
"react-table": "^7.8.0",
"react-use-gesture": "^6.0.14",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5",
@ -200,11 +214,14 @@
"ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5",
"use-resize-observer": "^9.0.2",
"uuid": "^8.3.2",
"video-extensions": "^1.2.0",
"wcag-contrast": "^3.0.0",
"web-vitals": "^2.1.0",
"workbox-core": "^6.1.0",
"workbox-navigation-preload": "^6.1.0",
"workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0"
"workbox-routing": "^6.1.0",
"zustand": "^4.0.0-rc.1"
}
}

@ -1,15 +1,143 @@
[
{
"constant": true,
"inputs": [
{
"internalType": "string",
"name": "uri_",
"type": "string"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "values",
"type": "uint256[]"
}
],
"name": "TransferBatch",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "TransferSingle",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "string",
"name": "value",
"type": "string"
},
{
"indexed": true,
"internalType": "uint256",
"name": "id",
"type": "uint256"
}
],
"name": "URI",
"type": "event"
},
{
"inputs": [
{
"internalType": "address",
"name": "_owner",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "_id",
"name": "id",
"type": "uint256"
}
],
@ -21,16 +149,165 @@
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address[]",
"name": "accounts",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"name": "balanceOfBatch",
"outputs": [
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeBatchTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_id",
"name": "",
"type": "uint256"
}
],
@ -42,7 +319,6 @@
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
}

@ -1,5 +1,320 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "punkContract",
"type": "address"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "approved",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "ApprovalForAll",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "address",
"name": "proxy",
"type": "address"
}
],
"name": "ProxyRegistered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": true,
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Unpaused",
"type": "event"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "approve",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "baseURI",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "punkIndex",
"type": "uint256"
}
],
"name": "burn",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "getApproved",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
}
],
"name": "isApprovedForAll",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "uint256",
"name": "punkIndex",
"type": "uint256"
}
],
"name": "mint",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
@ -15,10 +330,263 @@
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "pause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "user",
"type": "address"
}
],
"name": "proxyInfo",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "punkContract",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "registerProxy",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "_data",
"type": "bytes"
}
],
"name": "safeTransferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "bool",
"name": "approved",
"type": "bool"
}
],
"name": "setApprovalForAll",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "string",
"name": "baseUri",
"type": "string"
}
],
"name": "setBaseURI",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
},
{
"internalType": "uint256",
"name": "index",
"type": "uint256"
}
],
"name": "tokenOfOwnerByIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"internalType": "uint256",
@ -34,7 +602,72 @@
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "unpause",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
}
]

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useRef, useState } from 'react'
/**
* Debounces updates to a value.
@ -24,3 +24,34 @@ export default function useDebounce<T>(value: T, delay: number): T {
return debouncedValue
}
export function useDebouncedCallback<A extends unknown[]>(callback: (...args: A) => void, wait: number) {
// track args & timeout handle between calls
const argsRef = useRef<A>()
const timeout = useRef<ReturnType<typeof setTimeout>>()
function cleanup() {
if (timeout.current) {
clearTimeout(timeout.current)
}
}
// make sure our timeout gets cleared if
// our consuming component gets unmounted
useEffect(() => cleanup, [])
return function debouncedCallback(...args: A) {
// capture latest args
argsRef.current = args
// clear debounce timer
cleanup()
// start waiting again
timeout.current = setTimeout(() => {
if (argsRef.current) {
callback(...argsRef.current)
}
}, wait)
}
}

@ -0,0 +1,17 @@
import { useEffect } from 'react'
/**
* Disable scroll of an element based on condition
*/
export function useImperativeDisableScroll({ element, disabled }: { element?: HTMLElement; disabled?: boolean }) {
useEffect(() => {
if (!element) return
element.style.overflowY = disabled ? 'hidden' : 'auto'
return () => {
element.style.overflowY = 'auto'
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disabled])
}

@ -0,0 +1,26 @@
import { RefObject, useEffect, useRef } from 'react'
export function useOnClickOutside<T extends HTMLElement>(
node: RefObject<T | undefined>,
handler: undefined | (() => void)
) {
const handlerRef = useRef<undefined | (() => void)>(handler)
useEffect(() => {
handlerRef.current = handler
}, [handler])
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (node.current?.contains(e.target as Node) ?? false) {
return
}
if (handlerRef.current) handlerRef.current()
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [node])
}

27
src/hooks/useTimeout.ts Normal file

@ -0,0 +1,27 @@
import { useEffect, useState } from 'react'
const getReturnValues = (countDown: number): [number, number, number, number] => {
// calculate time left
const days = Math.floor(countDown / (1000 * 60 * 60 * 24))
const hours = Math.floor((countDown % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((countDown % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((countDown % (1000 * 60)) / 1000)
return [days, hours, minutes, seconds]
}
export const useTimeout = (targetDate: Date) => {
const countDownDate = new Date(targetDate).getTime()
const [countDown, setCountDown] = useState<number>(countDownDate - new Date().getTime())
useEffect(() => {
const interval = setInterval(() => {
setCountDown(countDownDate - new Date().getTime())
}, 1000)
return () => clearInterval(interval)
}, [countDownDate])
return getReturnValues(countDown)
}

@ -8,6 +8,7 @@ import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom'
@ -25,6 +26,8 @@ import UserUpdater from './state/user/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme'
import RadialGradientByChainUpdater from './theme/RadialGradientByChainUpdater'
const queryClient = new QueryClient()
if (!!window.ethereum) {
window.ethereum.autoRefreshOnNetworkChange = false
}
@ -49,6 +52,7 @@ createRoot(container).render(
<StrictMode>
<Provider store={store}>
<FeatureFlagsProvider>
<QueryClientProvider client={queryClient}>
<HashRouter>
<LanguageProvider>
<Web3Provider>
@ -64,6 +68,7 @@ createRoot(container).render(
</Web3Provider>
</LanguageProvider>
</HashRouter>
</QueryClientProvider>
</FeatureFlagsProvider>
</Provider>
</StrictMode>

@ -141,3 +141,7 @@ export default function useCurrencyBalance(
useMemo(() => [currency], [currency])
)[0]
}
export function useCurrencyBalanceString(account: string): string {
return useNativeCurrencyBalances(account ? [account] : [])?.[account ?? '']?.toSignificant(3) ?? ''
}

@ -0,0 +1,886 @@
[
{
"inputs": [
{
"internalType": "address",
"name": "_marketRegistry",
"type": "address"
},
{
"internalType": "address",
"name": "_converter",
"type": "address"
},
{
"internalType": "address",
"name": "_guardian",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "GOV",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_affiliate",
"type": "address"
}
],
"name": "addAffiliate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_marketId",
"type": "uint256"
}
],
"name": "addSponsoredMarket",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "affiliates",
"outputs": [
{
"internalType": "address",
"name": "affiliate",
"type": "address"
},
{
"internalType": "bool",
"name": "isActive",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "baseFees",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "closeAllTrades",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "converter",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "guardian",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "marketRegistry",
"outputs": [
{
"internalType": "contract MarketRegistry",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address[]",
"name": "tokenAddrs",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"internalType": "struct GenieSwap.ERC20Details",
"name": "erc20Details",
"type": "tuple"
},
{
"components": [
{
"internalType": "address",
"name": "tokenAddr",
"type": "address"
},
{
"internalType": "address[]",
"name": "to",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"internalType": "struct SpecialTransferHelper.ERC721Details[]",
"name": "erc721Details",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "address",
"name": "tokenAddr",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"internalType": "struct GenieSwap.ERC1155Details[]",
"name": "erc1155Details",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "bytes",
"name": "conversionData",
"type": "bytes"
}
],
"internalType": "struct GenieSwap.ConverstionDetails[]",
"name": "converstionDetails",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "uint256",
"name": "marketId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "tradeData",
"type": "bytes"
}
],
"internalType": "struct MarketRegistry.TradeDetails[]",
"name": "tradeDetails",
"type": "tuple[]"
},
{
"internalType": "address[]",
"name": "dustTokens",
"type": "address[]"
},
{
"internalType": "uint256[2]",
"name": "feeDetails",
"type": "uint256[2]"
}
],
"name": "multiAssetSwap",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address[]",
"name": "tokenAddrs",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"internalType": "struct GenieSwap.ERC20Details",
"name": "erc20Details",
"type": "tuple"
},
{
"components": [
{
"internalType": "address",
"name": "tokenAddr",
"type": "address"
},
{
"internalType": "address[]",
"name": "to",
"type": "address[]"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
}
],
"internalType": "struct SpecialTransferHelper.ERC721Details[]",
"name": "erc721Details",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "address",
"name": "tokenAddr",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
}
],
"internalType": "struct GenieSwap.ERC1155Details[]",
"name": "erc1155Details",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "bytes",
"name": "conversionData",
"type": "bytes"
}
],
"internalType": "struct GenieSwap.ConverstionDetails[]",
"name": "converstionDetails",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "uint256",
"name": "marketId",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "tradeData",
"type": "bytes"
}
],
"internalType": "struct MarketRegistry.TradeDetails[]",
"name": "tradeDetails",
"type": "tuple[]"
},
{
"internalType": "address[]",
"name": "dustTokens",
"type": "address[]"
},
{
"internalType": "uint256",
"name": "sponsoredMarketIndex",
"type": "uint256"
}
],
"name": "multiAssetSwapWithoutFee",
"outputs": [],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "",
"type": "uint256[]"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC1155BatchReceived",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC1155Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
},
{
"internalType": "uint256",
"name": "",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "",
"type": "bytes"
}
],
"name": "onERC721Received",
"outputs": [
{
"internalType": "bytes4",
"name": "",
"type": "bytes4"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "openForFreeTrades",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "openForTrades",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "punkProxy",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "uint256[]",
"name": "amounts",
"type": "uint256[]"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "rescueERC1155",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "rescueERC20",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "asset",
"type": "address"
},
{
"internalType": "uint256[]",
"name": "ids",
"type": "uint256[]"
},
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "rescueERC721",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "recipient",
"type": "address"
}
],
"name": "rescueETH",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_baseFees",
"type": "uint256"
}
],
"name": "setBaseFees",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_converter",
"type": "address"
}
],
"name": "setConverter",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract MarketRegistry",
"name": "_marketRegistry",
"type": "address"
}
],
"name": "setMarketRegistry",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IERC20",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "setOneTimeApproval",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "_openForFreeTrades",
"type": "bool"
}
],
"name": "setOpenForFreeTrades",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bool",
"name": "_openForTrades",
"type": "bool"
}
],
"name": "setOpenForTrades",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "setUp",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "sponsoredMarkets",
"outputs": [
{
"internalType": "uint256",
"name": "marketId",
"type": "uint256"
},
{
"internalType": "bool",
"name": "isActive",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes4",
"name": "interfaceId",
"type": "bytes4"
}
],
"name": "supportsInterface",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_affiliateIndex",
"type": "uint256"
},
{
"internalType": "address",
"name": "_affiliate",
"type": "address"
},
{
"internalType": "bool",
"name": "_IsActive",
"type": "bool"
}
],
"name": "updateAffiliate",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_guardian",
"type": "address"
}
],
"name": "updateGuardian",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_marketIndex",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "_marketId",
"type": "uint256"
},
{
"internalType": "bool",
"name": "_isActive",
"type": "bool"
}
],
"name": "updateSponsoredMarket",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]

@ -0,0 +1,595 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "punksOfferedForSale",
"outputs": [
{
"name": "isForSale",
"type": "bool"
},
{
"name": "punkIndex",
"type": "uint256"
},
{
"name": "seller",
"type": "address"
},
{
"name": "minValue",
"type": "uint256"
},
{
"name": "onlySellTo",
"type": "address"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "enterBidForPunk",
"outputs": [],
"payable": true,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
},
{
"name": "minPrice",
"type": "uint256"
}
],
"name": "acceptBidForPunk",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "addresses",
"type": "address[]"
},
{
"name": "indices",
"type": "uint256[]"
}
],
"name": "setInitialOwners",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "withdraw",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "imageHash",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "nextPunkIndexToAssign",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "punkIndexToAddress",
"outputs": [
{
"name": "",
"type": "address"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "standard",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "uint256"
}
],
"name": "punkBids",
"outputs": [
{
"name": "hasBid",
"type": "bool"
},
{
"name": "punkIndex",
"type": "uint256"
},
{
"name": "bidder",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "allInitialOwnersAssigned",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "allPunksAssigned",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "buyPunk",
"outputs": [],
"payable": true,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "transferPunk",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "withdrawBidForPunk",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "setInitialOwner",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
},
{
"name": "minSalePriceInWei",
"type": "uint256"
},
{
"name": "toAddress",
"type": "address"
}
],
"name": "offerPunkForSaleToAddress",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "punksRemainingToAssign",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
},
{
"name": "minSalePriceInWei",
"type": "uint256"
}
],
"name": "offerPunkForSale",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "getPunk",
"outputs": [],
"payable": false,
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "",
"type": "address"
}
],
"name": "pendingWithdrawals",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "punkIndex",
"type": "uint256"
}
],
"name": "punkNoLongerForSale",
"outputs": [],
"payable": false,
"type": "function"
},
{
"inputs": [],
"payable": true,
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "punkIndex",
"type": "uint256"
}
],
"name": "Assign",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "punkIndex",
"type": "uint256"
}
],
"name": "PunkTransfer",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "punkIndex",
"type": "uint256"
},
{
"indexed": false,
"name": "minValue",
"type": "uint256"
},
{
"indexed": true,
"name": "toAddress",
"type": "address"
}
],
"name": "PunkOffered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "punkIndex",
"type": "uint256"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": true,
"name": "fromAddress",
"type": "address"
}
],
"name": "PunkBidEntered",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "punkIndex",
"type": "uint256"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": true,
"name": "fromAddress",
"type": "address"
}
],
"name": "PunkBidWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "punkIndex",
"type": "uint256"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
},
{
"indexed": true,
"name": "fromAddress",
"type": "address"
},
{
"indexed": true,
"name": "toAddress",
"type": "address"
}
],
"name": "PunkBought",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "punkIndex",
"type": "uint256"
}
],
"name": "PunkNoLongerForSale",
"type": "event"
}
]

@ -0,0 +1,153 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [{ "name": "", "type": "string" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "initialAddressSet",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "addr", "type": "address" }],
"name": "endGrantAuthentication",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "addr", "type": "address" }],
"name": "revokeAuthentication",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "pending",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "contracts",
"outputs": [{ "name": "", "type": "bool" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "owner",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "delegateProxyImplementation",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [{ "name": "", "type": "address" }],
"name": "proxies",
"outputs": [{ "name": "", "type": "address" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "addr", "type": "address" }],
"name": "startGrantAuthentication",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [],
"name": "registerProxy",
"outputs": [{ "name": "proxy", "type": "address" }],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "DELAY_PERIOD",
"outputs": [{ "name": "", "type": "uint256" }],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "authAddress", "type": "address" }],
"name": "grantInitialAuthentication",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [{ "name": "newOwner", "type": "address" }],
"name": "transferOwnership",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{ "inputs": [], "payable": false, "stateMutability": "nonpayable", "type": "constructor" },
{
"anonymous": false,
"inputs": [{ "indexed": true, "name": "previousOwner", "type": "address" }],
"name": "OwnershipRenounced",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{ "indexed": true, "name": "previousOwner", "type": "address" },
{ "indexed": true, "name": "newOwner", "type": "address" }
],
"name": "OwnershipTransferred",
"type": "event"
}
]

19
src/nft/css/atoms.ts Normal file

@ -0,0 +1,19 @@
import clsx from 'clsx'
import * as resetStyles from './reset.css'
import { Sprinkles, sprinkles } from './sprinkles.css'
export interface Atoms extends Sprinkles {
// reset is used by the Box component when its expected to behave as something other than a div, ie button, a, or span
reset?: keyof JSX.IntrinsicElements
}
export const atoms = ({ reset, ...rest }: Atoms) => {
if (!reset) return sprinkles(rest)
const elementReset = resetStyles.element[reset as keyof typeof resetStyles.element]
const sprinklesClasses = sprinkles(rest)
return clsx(resetStyles.base, elementReset, sprinklesClasses)
}

119
src/nft/css/common.css.ts Normal file

@ -0,0 +1,119 @@
import { style } from '@vanilla-extract/css'
import { sprinkles, themeVars } from './sprinkles.css'
export const center = sprinkles({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
})
export const row = sprinkles({
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
})
export const column = sprinkles({
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
})
export const section = style([
sprinkles({
paddingLeft: { mobile: '16', desktopL: '0' },
paddingRight: { mobile: '16', desktopL: '0' },
}),
{ maxWidth: '1000px', margin: '0 auto' },
])
// TYPOGRAPHY
export const header1 = sprinkles({ fontWeight: 'normal', fontSize: '36' })
export const header2 = sprinkles({ fontWeight: 'normal', fontSize: '28' })
export const headlineSmall = sprinkles({ fontWeight: 'normal', fontSize: '20' })
export const subhead = sprinkles({ fontWeight: 'medium', fontSize: '16' })
export const subheadSmall = sprinkles({ fontWeight: 'medium', fontSize: '14' })
export const body = sprinkles({ fontSize: '16' })
export const bodySmall = sprinkles({
fontSize: '14',
})
export const caption = sprinkles({ fontWeight: 'bold', fontSize: '12' })
export const badge = style([sprinkles({ fontWeight: 'semibold', fontSize: '10' }), { letterSpacing: '0.5px' }])
export const buttonTextLarge = sprinkles({ fontWeight: 'medium', fontSize: '28' })
export const buttonTextMedium = sprinkles({ fontWeight: 'medium', fontSize: '16' })
export const buttonMedium = style([
buttonTextMedium,
sprinkles({
backgroundColor: 'blue',
borderRadius: '12',
color: 'explicitWhite',
transition: '250',
boxShadow: { hover: 'elevation' },
}),
{
cursor: 'pointer',
padding: '14px 18px',
border: 'none',
':hover': {
cursor: 'pointer',
},
':disabled': {
cursor: 'auto',
opacity: '0.3',
},
},
])
export const buttonReset = sprinkles({
border: 'none',
background: 'none',
cursor: 'pointer',
})
export const disabled = style([
{
padding: '19px 17px',
boxSizing: 'border-box',
textAlign: 'left',
},
sprinkles({
color: 'placeholder',
fontWeight: 'medium',
background: 'whitesmoke',
borderRadius: '14',
borderStyle: 'none',
width: 'full',
fontSize: '16',
}),
])
export const buttonTextSmall = sprinkles({ fontWeight: 'normal', fontSize: '14' })
export const buttonSmall = style([
buttonTextSmall,
sprinkles({
background: 'lightGray',
borderRadius: '12',
fontSize: '12',
color: 'genieBlue',
transition: '250',
boxShadow: { hover: 'elevation' },
}),
{
padding: '2px 8px',
border: 'none',
':hover': {
cursor: 'pointer',
},
':disabled': {
cursor: 'auto',
color: themeVars.colors.white,
backgroundColor: themeVars.colors.medGray,
},
},
])
export const imageHover = style({
transform: 'scale(1.25)',
})

@ -0,0 +1,26 @@
import { assignInlineVars } from '@vanilla-extract/dynamic'
import { Theme, themeVars } from './sprinkles.css'
const resolveTheme = (theme: Theme | (() => Theme)) => (typeof theme === 'function' ? theme() : theme)
export function cssObjectFromTheme(
theme: Theme | (() => Theme),
{ extends: baseTheme }: { extends?: Theme | (() => Theme) } = {}
) {
const resolvedThemeVars = {
...assignInlineVars(themeVars, resolveTheme(theme)),
}
if (!baseTheme) {
return resolvedThemeVars
}
const resolvedBaseThemeVars = assignInlineVars(themeVars, resolveTheme(baseTheme))
const filteredVars = Object.fromEntries(
Object.entries(resolvedThemeVars).filter(([varName, value]) => value !== resolvedBaseThemeVars[varName])
)
return filteredVars
}

@ -0,0 +1,8 @@
import { cssObjectFromTheme } from './cssObjectFromTheme'
import { Theme } from './sprinkles.css'
export function cssStringFromTheme(theme: Theme | (() => Theme), options: { extends?: Theme | (() => Theme) } = {}) {
return Object.entries(cssObjectFromTheme(theme, options))
.map(([key, value]) => `${key}:${value};`)
.join('')
}

@ -0,0 +1,21 @@
import { keyframes, style } from '@vanilla-extract/css'
import { darken } from 'polished'
export const loadingAnimation = keyframes({
'0%': {
backgroundPosition: '100% 50%',
},
'100%': {
backgroundPosition: '0% 50%',
},
})
export const loadingBlock = style([
{
animation: `${loadingAnimation} 1.5s infinite`,
animationFillMode: 'both',
background: `linear-gradient(to left, #7C85A24D 25%, ${darken(0.8, '#7C85A24D')} 50%, #7C85A24D 75%)`,
backgroundSize: '400%',
willChange: 'background-position',
},
])

106
src/nft/css/reset.css.ts Normal file

@ -0,0 +1,106 @@
import 'focus-visible'
import { style } from '@vanilla-extract/css'
const hideFocusRingsDataAttribute = '[data-js-focus-visible] &:focus:not([data-focus-visible-added])'
export const base = style({
boxSizing: 'border-box',
selectors: {
[`${hideFocusRingsDataAttribute}`]: {
outline: 'none',
},
},
verticalAlign: 'baseline',
WebkitTapHighlightColor: 'transparent',
})
const list = style({
listStyle: 'none',
})
const quote = style({
quotes: 'none',
selectors: {
'&:before, &:after': {
content: "''",
},
},
})
const table = style({
borderCollapse: 'collapse',
borderSpacing: 0,
})
const appearance = style({
appearance: 'none',
})
const field = style([
appearance,
{
'::placeholder': {
opacity: 1,
},
outline: 'none',
},
])
const mark = style({
backgroundColor: 'transparent',
color: 'inherit',
})
const select = style([
field,
{
':disabled': {
opacity: 1,
},
selectors: {
'&::-ms-expand': {
display: 'none',
},
},
},
])
const input = style([
field,
{
selectors: {
'&::-ms-clear': {
display: 'none',
},
'&::-webkit-search-cancel-button': {
WebkitAppearance: 'none',
},
'&::-webkit-inner-spin-button, &::-webkit-inner-spin-button ': {
WebkitAppearance: 'none',
},
},
WebkitAppearance: 'none',
MozAppearance: 'textfield',
':focus': {
outline: 'none',
},
},
])
const a = style({
textDecoration: 'none',
})
export const element = {
a,
blockquote: quote,
input,
mark,
ol: list,
q: quote,
select,
table,
textarea: field,
ul: list,
}

@ -0,0 +1,366 @@
import { createGlobalTheme } from '@vanilla-extract/css'
import { createGlobalThemeContract } from '@vanilla-extract/css'
import { createSprinkles, defineProperties } from '@vanilla-extract/sprinkles'
const themeContractValues = {
colors: {
// Pavel's colors, mostly used for the wallet connection. TODO Some may need to be changed / removed
error: '',
textDisconnect: '',
modalBackdrop: '',
backgroundSecondary: '',
modalClose: '',
text: '',
modalTextSecondary: '',
// Bryan's colors from Figma that vary dark vs light
blackBlue: '',
darkGray: '',
medGray: '',
lightGray: '',
white: '',
darkGray10: '',
blackBlue20: '',
explicitWhite: '',
magicGradient: '',
placeholder: '',
lightGrayButton: '',
lightGrayContainer: ',',
lightGrayOverlay: '',
// Opacities of black and whit
white95: '',
white90: '',
white80: '',
},
shadows: {
menu: '',
genieBlue: '',
elevation: '',
tooltip: '',
},
}
export type Theme = typeof themeContractValues
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>
}
export type ThemePartial = DeepPartial<Theme>
export const themeVars = createGlobalThemeContract(themeContractValues, (_, path) => `genie-${path.join('-')}`)
const dimensions = {
'0': '0',
'2': '2',
'4': '4px',
'8': '8px',
'16': '16px',
'18': '18px',
'20': '20px',
'24': '24px',
'26': '26px',
'28': '28px',
'32': '32px',
'36': '36px',
'40': '40px',
'42': '42px',
'44': '44px',
'48': '48px',
'52': '52px',
'54': '54px',
'56': '56px',
'60': '60px',
'64': '64px',
'72': '72px',
'100': '100px',
'120': '120px',
'160': '160px',
'276': '276px',
'288': '288px',
'292': '292px',
'386': '386px',
half: '50%',
full: '100%',
min: 'min-content',
max: 'max-content',
viewHeight: '100vh',
viewWidth: '100vw',
auto: 'auto',
inherit: 'inherit',
}
const spacing = {
'0': '0',
'1': '1px',
'2': '2px',
'4': '4px',
'6': '6px',
'8': '8px',
'10': '10px',
'12': '12px',
'14': '14px',
'16': '16px',
'18': '18px',
'20': '20px',
'24': '24px',
'28': '28px',
'32': '32px',
'36': '36px',
'40': '40px',
'48': '48px',
'50': '50px',
'52': '52px',
'60': '60px',
'64': '64px',
'82': '82px',
'72': '72px',
'88': '88px',
'100': '100px',
'104': '104px',
'136': '136px',
'150': '150px',
'1/2': '50%',
auto: 'auto',
unset: 'unset',
}
export const vars = createGlobalTheme(':root', {
color: {
...themeVars.colors,
genieBlue: '#4C82FB',
fallbackGradient: 'linear-gradient(270deg, #D1D5DB 0%, #F6F6F6 100%)',
dropShadow: '0px 4px 16px rgba(70, 115, 250, 0.4)',
green: '#209853',
orange: '#FA2C38',
// Pavel's colors, TODO probably remove them after Pavel continues Genie List
black: 'black',
whitesmoke: '#F5F5F5',
blue: '#4C82FB',
explicitBlackBlue: '#0E111A',
gray: '#CBCEDC',
transculent: '#7F7F7F',
transparent: 'transparent',
none: 'none',
// new uniswap colors:
blue400: '#4C82FB',
pink400: '#FB118E',
red700: '#530f10',
red400: '#FA2C38',
green200: '#5CFE9D',
green400: '#1A9550',
grey900: '#0E111A',
grey700: '#293249',
grey500: '#5E6887',
grey400: '#7C85A2',
grey300: '#99A1BD',
grey200: '#B7BED4',
grey100: '#DDE3F7',
grey50: '#EDEFF7',
},
border: {
transculent: '1.5px solid rgba(0, 0, 0, 0.1)',
none: 'none',
},
radii: {
menu: '16px',
modal: '24px',
'0': '0px',
'4': '4px',
'8': '8px',
'10': '10px',
'12': '12px',
'14': '14px',
'16': '16px',
'20': '20px',
'30': '30px',
'40': '40px',
'100': '100px',
round: '9999px',
},
fontSize: {
'0': '0',
'10': '10px',
'12': '12px',
'14': '14px',
'16': '16px',
'20': '20px',
'24': '24px',
'28': '28px',
'34': '34px',
'36': '36px',
'40': '40px',
'48': '48px',
'60': '60px',
'96': '96px',
},
fontWeight: {
normal: '400',
medium: '500',
semibold: '600',
bold: '700',
black: '900',
},
time: {
'250': '250ms',
'500': '500ms',
},
fonts: {
body: 'Inter, sans-serif',
heading: 'Adieu, sans-serif',
},
})
const flexAlignment = [
'flex-start',
'center',
'flex-end',
'stretch',
'baseline',
'space-around',
'space-between',
] as const
const overflow = ['hidden', 'inherit', 'scroll', 'visible', 'auto'] as const
const borderWidth = ['1px', '1.5px', '2px', '4px']
const borderStyle = ['none', 'solid'] as const
export const breakpoints = {
tabletSm: 656,
tablet: 708,
tabletL: 784,
tabletXl: 830,
desktop: 948,
desktopL: 1030,
desktopXl: 1260,
}
const layoutStyles = defineProperties({
conditions: {
mobile: {},
tabletSm: { '@media': `screen and (min-width: ${breakpoints.tabletSm}px)` },
tablet: { '@media': `screen and (min-width: ${breakpoints.tablet})` },
tabletL: { '@media': `screen and (min-width: ${breakpoints.tabletL}px)` },
tabletXl: { '@media': `screen and (min-width: ${breakpoints.tabletXl}px)` },
desktop: { '@media': `screen and (min-width: ${breakpoints.desktop}px)` },
desktopL: { '@media': `screen and (min-width: ${breakpoints.desktopL}px)` },
desktopXl: { '@media': `screen and (min-width: ${breakpoints.desktopXl}px)` },
},
defaultCondition: 'mobile',
properties: {
alignItems: flexAlignment,
alignSelf: flexAlignment,
justifyItems: flexAlignment,
justifySelf: flexAlignment,
placeItems: flexAlignment,
placeContent: flexAlignment,
fontSize: vars.fontSize,
fontWeight: vars.fontWeight,
marginBottom: spacing,
marginLeft: spacing,
marginRight: spacing,
marginTop: spacing,
width: dimensions,
height: dimensions,
maxWidth: dimensions,
minWidth: dimensions,
maxHeight: dimensions,
minHeight: dimensions,
paddingBottom: spacing,
paddingLeft: spacing,
paddingRight: spacing,
paddingTop: spacing,
padding: spacing,
bottom: spacing,
left: spacing,
right: spacing,
top: spacing,
margin: spacing,
zIndex: ['auto', '0', '1', '2', '3'],
gap: spacing,
flexShrink: spacing,
flex: ['1', '2', '3'],
flexWrap: ['nowrap', 'wrap', 'wrap-reverse'],
display: ['none', 'block', 'flex', 'inline-flex', 'inline-block', 'grid', 'inline'],
whiteSpace: ['nowrap'],
textOverflow: ['ellipsis'],
textAlign: ['left', 'right', 'center', 'justify'],
visibility: ['visible', 'hidden'],
flexDirection: ['row', 'column', 'column-reverse'],
justifyContent: flexAlignment,
position: ['absolute', 'fixed', 'relative', 'sticky', 'static'],
objectFit: ['contain', 'cover'],
order: [0, 1],
} as const,
shorthands: {
paddingX: ['paddingLeft', 'paddingRight'],
paddingY: ['paddingTop', 'paddingBottom'],
marginX: ['marginLeft', 'marginRight'],
marginY: ['marginTop', 'marginBottom'],
},
})
const colorStyles = defineProperties({
conditions: {
default: {},
hover: { selector: '&:hover' },
active: { selector: '&:active' },
focus: { selector: '&:focus' },
before: { selector: '&:before' },
placeholder: { selector: '&::placeholder' },
},
defaultCondition: 'default',
properties: {
color: vars.color,
background: vars.color,
borderColor: vars.color,
borderBottomColor: vars.color,
borderTopColor: vars.color,
backgroundColor: vars.color,
outlineColor: vars.color,
fill: vars.color,
},
})
const unresponsiveProperties = defineProperties({
conditions: {
default: {},
hover: { selector: '&:hover' },
active: { selector: '&:active' },
before: { selector: '&:before' },
},
defaultCondition: 'default',
properties: {
cursor: ['default', 'pointer', 'auto'],
borderStyle,
borderBottomStyle: borderStyle,
borderRadius: vars.radii,
borderTopLeftRadius: vars.radii,
borderTopRightRadius: vars.radii,
borderBottomLeftRadius: vars.radii,
borderBottomRightRadius: vars.radii,
border: vars.border,
borderBottom: vars.border,
borderTop: vars.border,
borderWidth,
borderBottomWidth: borderWidth,
fontFamily: vars.fonts,
overflow,
overflowX: overflow,
overflowY: overflow,
boxShadow: { ...themeVars.shadows, none: 'none', dropShadow: vars.color.dropShadow },
lineHeight: ['1', 'auto'],
transition: vars.time,
transitionDuration: vars.time,
animationDuration: vars.time,
},
})
export type UnresponsiveProperties = keyof typeof unresponsiveProperties
export const sprinkles = createSprinkles(layoutStyles, colorStyles, unresponsiveProperties)
export type Sprinkles = Parameters<typeof sprinkles>[0]

14
src/nft/hooks/index.ts Normal file

@ -0,0 +1,14 @@
export * from './useCart'
export * from './useCollectionFilters'
export * from './useFiltersExpanded'
export * from './useGenieList'
export * from './useIsMobile'
export * from './useMarketplaceSelect'
export * from './useNFTSelect'
export * from './useSearchHistory'
export * from './useSelectAsset'
export * from './useSellAsset'
export * from './useSellPageState'
export * from './useSweep'
export * from './useWalletBalance'
export * from './useWalletCollections'

20
src/nft/hooks/useCart.ts Normal file

@ -0,0 +1,20 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
interface CartState {
cartExpanded: boolean
toggleCart: () => void
}
export const useCart = create<CartState>()(
devtools(
(set) => ({
cartExpanded: false,
toggleCart: () =>
set(({ cartExpanded }) => ({
cartExpanded: !cartExpanded,
})),
}),
{ name: 'useCart' }
)
)

@ -0,0 +1,97 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
export enum SortBy {
LowToHigh,
HighToLow,
RareToCommon,
CommonToRare,
}
export const SortByPointers = {
[SortBy.HighToLow]: 'highest',
[SortBy.LowToHigh]: 'lowest',
[SortBy.RareToCommon]: 'rare',
[SortBy.CommonToRare]: 'common',
}
export type Trait = {
trait_type: string
trait_value: string
trait_count: number
floorPrice?: number
}
interface State {
traits: Trait[]
markets: string[]
minPrice: number | ''
maxPrice: number | ''
minRarity: number | ''
maxRarity: number | ''
marketCount: Record<string, number>
buyNow: boolean
search: string
sortBy: SortBy
showFullTraitName: { shouldShow: boolean; trait_value?: string; trait_type: string }
}
type Actions = {
setMarketCount: (_: Record<string, number>) => void
addMarket: (market: string) => void
removeMarket: (market: string) => void
addTrait: (trait: Trait) => void
removeTrait: (trait: Trait) => void
reset: () => void
setMinPrice: (price: number | '') => void
setMaxPrice: (price: number | '') => void
setMinRarity: (range: number | '') => void
setMaxRarity: (range: number | '') => void
setBuyNow: (bool: boolean) => void
setSearch: (term: string) => void
setSortBy: (sortBy: SortBy) => void
toggleShowFullTraitName: (show: { shouldShow: boolean; trait_value: string; trait_type: string }) => void
}
export type CollectionFilters = State & Actions
export const initialCollectionFilterState: State = {
minPrice: '',
maxPrice: '',
minRarity: '',
maxRarity: '',
traits: [],
markets: [],
marketCount: {},
buyNow: true,
search: '',
sortBy: SortBy.LowToHigh,
showFullTraitName: { shouldShow: false, trait_value: '', trait_type: '' },
}
export const useCollectionFilters = create<CollectionFilters>()(
devtools(
(set) => ({
...initialCollectionFilterState,
setSortBy: (sortBy) => set({ sortBy }),
setSearch: (search) => set({ search }),
setBuyNow: (buyNow) => set({ buyNow }),
setMarketCount: (marketCount) => set({ marketCount }),
addMarket: (market) => set(({ markets }) => ({ markets: [...markets, market] })),
removeMarket: (market) => set(({ markets }) => ({ markets: markets.filter((_market) => market !== _market) })),
addTrait: (trait) => set(({ traits }) => ({ traits: [...traits, trait] })),
removeTrait: (trait) =>
set(({ traits }) => ({
traits: traits.filter((x) => JSON.stringify(x) !== JSON.stringify(trait)),
})),
reset: () => set(() => ({ traits: [], minRarity: '', maxRarity: '', markets: [] })),
setMinPrice: (price) => set(() => ({ minPrice: price })),
setMaxPrice: (price) => set(() => ({ maxPrice: price })),
setMinRarity: (range) => set(() => ({ minRarity: range })),
setMaxRarity: (range) => set(() => ({ maxRarity: range })),
toggleShowFullTraitName: ({ shouldShow, trait_value, trait_type }) =>
set(() => ({ showFullTraitName: { shouldShow, trait_value, trait_type } })),
}),
{ name: 'useCollectionTraits' }
)
)

@ -0,0 +1,30 @@
import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface State {
isExpanded: boolean
setExpanded: (expanded: boolean) => void
}
const useFiltersExpandedStore = create<State>()(
persist(
devtools(
(set) => ({
isExpanded: false,
setExpanded: (expanded) =>
set(() => ({
isExpanded: expanded,
})),
}),
{ name: 'useFiltersExpanded' }
),
{ name: 'useFiltersExpanded' }
)
)
export const useFiltersExpanded = (): [boolean, (expanded: boolean) => void] => {
const isExpanded = useFiltersExpandedStore((s) => s.isExpanded)
const setExpanded = useFiltersExpandedStore((s) => s.setExpanded)
return [isExpanded, setExpanded]
}

@ -0,0 +1,21 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
interface GenieListState {
looksRareNonce: number
setLooksRareNonce: (nonce: number) => void
getLooksRareNonce: () => number
}
export const useGenieList = create<GenieListState>()(
devtools((set, get) => ({
looksRareNonce: 0,
setLooksRareNonce: (nonce) =>
set(() => {
return { looksRareNonce: nonce }
}),
getLooksRareNonce: () => {
return get().looksRareNonce
},
}))
)

@ -0,0 +1,25 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { breakpoints } from '../css/sprinkles.css'
interface IsMobileState {
isMobile: boolean
width: number
setMobileWidth: (width: number) => void
}
export const useIsMobile = create<IsMobileState>()(
devtools(
(set) => ({
isMobile: true,
width: 800,
setMobileWidth: (width: number) =>
set(() => ({
width,
isMobile: width < breakpoints.tabletSm,
})),
}),
{ name: 'isMobile' }
)
)

@ -0,0 +1,24 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
export type MarketplaceOption = { name: string; icon: string }
interface State {
options: MarketplaceOption[]
select: (o: MarketplaceOption) => void
}
export const useMarketplaceSelect = create<State>()(
devtools(
(set) => ({
options: [],
select: (option) =>
set(({ options }) => {
if (options.find((o) => option.name === o.name))
return { options: options.filter((x) => x.name !== option.name) }
else return { options: [...options, option] }
}),
}),
{ name: 'useMarketplaceSelect' }
)
)

@ -0,0 +1,52 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { OpenSeaAsset } from '../types'
interface SelectNFTState {
/**
* NFTs selected by a user
*/
selectedNFTs: (OpenSeaAsset & { price?: number })[]
selectNFT: (nft: OpenSeaAsset & { price?: number }) => void
reset: () => void
setUniversalPrice: (price: number) => void
toggleUniversalPrice: (v: boolean) => void
setSingleNFTPrice: (id: number, price: number) => void
isUniversalPrice: boolean
}
export const useNFTSelect = create<SelectNFTState>()(
devtools(
(set) => ({
selectedNFTs: [],
isUniversalPrice: false,
selectNFT: (nft) =>
set(({ selectedNFTs }) => {
if (selectedNFTs.length === 0) return { selectedNFTs: [nft] }
else if (!!selectedNFTs.find((x) => x.id === nft.id))
return { selectedNFTs: selectedNFTs.filter((n) => n.id !== nft.id) }
else return { selectedNFTs: [...selectedNFTs, nft] }
}),
reset: () => set(() => ({ selectedNFTs: [] })),
toggleUniversalPrice: (v) => set(() => ({ isUniversalPrice: v })),
setUniversalPrice: (price) =>
set(({ selectedNFTs }) => {
return {
selectedNFTs: selectedNFTs.map((n) => ({ ...n, price })),
isUniversalPrice: true,
}
}),
setSingleNFTPrice: (id, price) =>
set(({ selectedNFTs }) => {
const found = selectedNFTs.find((i) => i.id === id)
return {
selectedNFTs: [...selectedNFTs.filter((n) => n.id !== id), { ...found, price }],
}
}),
}),
{ name: 'useNFTSelect' }
)
)

@ -0,0 +1,24 @@
import { FungibleToken, GenieCollection } from 'nft/types'
import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'
interface SearchHistoryProps {
history: (FungibleToken | GenieCollection)[]
addItem: (item: FungibleToken | GenieCollection) => void
}
export const useSearchHistory = create<SearchHistoryProps>()(
persist(
devtools((set) => ({
history: [],
addItem: (item: FungibleToken | GenieCollection) => {
set(({ history }) => {
const historyCopy = [...history]
if (historyCopy.length === 0 || historyCopy[0].address !== item.address) historyCopy.unshift(item)
return { history: historyCopy }
})
},
})),
{ name: 'useSearchHistory' }
)
)

@ -0,0 +1,37 @@
import { v4 as uuidv4 } from 'uuid'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { GenieAsset } from '../types'
interface SelectAssetState {
selectedAssets: GenieAsset[]
selectAsset: (asset: GenieAsset) => void
removeAsset: (asset: GenieAsset) => void
reset: () => void
}
export const useSelectAsset = create<SelectAssetState>()(
devtools((set) => ({
selectedAssets: [],
selectAsset: (asset) =>
set(({ selectedAssets }) => {
const assetWithId = { id: uuidv4(), ...asset }
if (selectedAssets.length === 0) return { selectedAssets: [assetWithId] }
else return { selectedAssets: [...selectedAssets, assetWithId] }
}),
removeAsset: (asset) => {
set(({ selectedAssets }) => {
if (selectedAssets.length === 0) return { selectedAssets: [] }
else selectedAssets.find((x) => x.tokenId === asset.tokenId && x.address === asset.address)
const assetsCopy = [...selectedAssets]
assetsCopy.splice(
selectedAssets.findIndex((n) => n.tokenId === asset.tokenId && n.address === asset.address),
1
)
return { selectedAssets: assetsCopy }
})
},
reset: () => set(() => ({ selectedAssets: [] })),
}))
)

@ -0,0 +1,149 @@
import { v4 as uuidv4 } from 'uuid'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { ListingMarket, ListingWarning, WalletAsset } from '../types'
interface SellAssetState {
sellAssets: WalletAsset[]
selectSellAsset: (asset: WalletAsset) => void
removeSellAsset: (asset: WalletAsset) => void
reset: () => void
setGlobalExpiration: (expirationTime: number) => void
setAssetListPrice: (asset: WalletAsset, price: string, marketplace?: ListingMarket) => void
setGlobalMarketplaces: (marketplaces: ListingMarket[]) => void
removeAssetMarketplace: (asset: WalletAsset, marketplace: ListingMarket) => void
addMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning) => void
removeMarketplaceWarning: (asset: WalletAsset, warning: ListingWarning, setGlobalOverride?: boolean) => void
removeAllMarketplaceWarnings: () => void
}
export const useSellAsset = create<SellAssetState>()(
devtools(
(set) => ({
sellAssets: [],
selectSellAsset: (asset) =>
set(({ sellAssets }) => {
const assetWithId = { id: uuidv4(), ...asset }
if (sellAssets.length === 0) return { sellAssets: [assetWithId] }
else return { sellAssets: [...sellAssets, assetWithId] }
}),
removeSellAsset: (asset) => {
set(({ sellAssets }) => {
if (sellAssets.length === 0) return { sellAssets: [] }
else sellAssets.find((x) => x.id === asset.id)
const assetsCopy = [...sellAssets]
assetsCopy.splice(
sellAssets.findIndex((n) => n.id === asset.id),
1
)
return { sellAssets: assetsCopy }
})
},
reset: () => set(() => ({ sellAssets: [] })),
setGlobalExpiration: (expirationTime) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
assetsCopy.map((asset) => {
asset.expirationTime = expirationTime
return asset
})
return { sellAssets: assetsCopy }
})
},
setAssetListPrice: (asset, price, marketplace?) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
if (marketplace) {
const listingIndex = asset.newListings?.findIndex(
(listing) => listing.marketplace.name === marketplace.name
)
if (asset.newListings && listingIndex != null && listingIndex > -1) {
asset.newListings[listingIndex] = { price, marketplace, overrideFloorPrice: false }
if (listingIndex === 0) asset.marketAgnosticPrice = price
} else asset.newListings?.push({ price, marketplace, overrideFloorPrice: false })
} else asset.marketAgnosticPrice = price
const index = sellAssets.findIndex((n) => n.id === asset.id)
assetsCopy[index] = asset
return { sellAssets: assetsCopy }
})
},
setGlobalMarketplaces: (marketplaces) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
assetsCopy.map((asset) => {
asset.marketplaces = marketplaces
asset.newListings = []
for (const marketplace of marketplaces) {
const listingIndex = asset.newListings.findIndex(
(listing) => listing.marketplace.name === marketplace.name
)
const newListing = {
price: asset.marketAgnosticPrice,
marketplace,
overrideFloorPrice: false,
}
listingIndex > -1 ? (asset.newListings[listingIndex] = newListing) : asset.newListings.push(newListing)
}
return asset
})
return { sellAssets: assetsCopy }
})
},
removeAssetMarketplace: (asset, marketplace) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
const assetIndex = sellAssets.indexOf(asset)
const marketplaceIndex =
asset.marketplaces?.findIndex((oldMarket) => oldMarket.name === marketplace.name) ?? -1
const listingIndex = asset.newListings?.findIndex((listing) => listing.marketplace.name === marketplace.name)
const assetCopy = JSON.parse(JSON.stringify(asset))
if (marketplaceIndex > -1) {
assetCopy.marketplaces.splice(marketplaceIndex, 1)
assetCopy.newListings.splice(listingIndex, 1)
}
assetsCopy.splice(assetIndex, 1, assetCopy)
return { sellAssets: assetsCopy }
})
},
addMarketplaceWarning: (asset, warning) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
asset.listingWarnings?.push(warning)
const index = sellAssets.findIndex((n) => n.id === asset.id)
assetsCopy[index] = asset
return { sellAssets: assetsCopy }
})
},
removeMarketplaceWarning: (asset, warning, setGlobalOverride?) => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
if (asset.listingWarnings === undefined || asset.newListings === undefined) return { sellAssets: assetsCopy }
const warningIndex =
asset.listingWarnings?.findIndex((n) => n.marketplace.name === warning.marketplace.name) ?? -1
asset.listingWarnings?.splice(warningIndex, 1)
if (warning?.message?.includes('LISTING BELOW FLOOR')) {
if (setGlobalOverride) {
asset.newListings?.forEach((listing) => (listing.overrideFloorPrice = true))
} else {
const listingIndex =
asset.newListings?.findIndex((n) => n.marketplace.name === warning.marketplace.name) ?? -1
asset.newListings[listingIndex].overrideFloorPrice = true
}
}
const index = sellAssets.findIndex((n) => n.id === asset.id)
assetsCopy[index] = asset
return { sellAssets: assetsCopy }
})
},
removeAllMarketplaceWarnings: () => {
set(({ sellAssets }) => {
const assetsCopy = [...sellAssets]
assetsCopy.map((asset) => (asset.listingWarnings = []))
return { sellAssets: assetsCopy }
})
},
}),
{ name: 'useSelectAsset' }
)
)

@ -0,0 +1,25 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { SellPageStateType } from '../types'
interface sellPageState {
/**
* State of user settings
*/
state: SellPageStateType
setSellPageState: (state: SellPageStateType) => void
}
export const useSellPageState = create<sellPageState>()(
devtools(
(set) => ({
state: SellPageStateType.SELECTING,
setSellPageState: (newState) =>
set(() => ({
state: newState,
})),
}),
{ name: 'useSellPageState' }
)
)

@ -0,0 +1,165 @@
import { Interface } from '@ethersproject/abi'
import { BigNumber } from '@ethersproject/bignumber'
import { hexStripZeros } from '@ethersproject/bytes'
import { ContractReceipt } from '@ethersproject/contracts'
import { JsonRpcSigner } from '@ethersproject/providers'
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import ERC721 from '../../abis/erc721.json'
import ERC1155 from '../../abis/erc1155.json'
import CryptoPunksMarket from '../abis/CryptoPunksMarket.json'
import { GenieAsset, RouteResponse, RoutingItem, TxResponse, TxStateType, UpdatedGenieAsset } from '../types'
import { combineBuyItemsWithTxRoute } from '../utils/txRoute/combineItemsWithTxRoute'
// Shortens a given txHash. With standard charsToShorten var of 4, a hash will become 0x1234...1234
export const shortenTxHash = (txHash: string, charsToShorten = 4, addCharsToBack = 0): string => {
return `${txHash.substring(0, charsToShorten + 2)}...${txHash.substring(
txHash.length - charsToShorten,
txHash.length - (charsToShorten + addCharsToBack)
)}`
}
interface TxState {
state: TxStateType
setState: (state: TxStateType) => void
txHash: string
clearTxHash: () => void
sendTransaction: (
signer: JsonRpcSigner,
selectedAssets: UpdatedGenieAsset[],
transactionData: RouteResponse
) => Promise<TxResponse | undefined>
}
export const useSendTransaction = create<TxState>()(
devtools(
(set) => ({
state: TxStateType.New,
txHash: '',
clearTxHash: () => set({ txHash: '' }),
setState: (newState) => set(() => ({ state: newState })),
sendTransaction: async (signer, selectedAssets, transactionData) => {
const address = await signer.getAddress()
try {
const txNoGasLimit = {
to: transactionData.to,
value: BigNumber.from(transactionData.valueToSend),
data: transactionData.data,
}
const gasLimit = (await signer.estimateGas(txNoGasLimit)).mul(105).div(100)
// tx['gasLimit'] = gasLimit
const tx = { ...txNoGasLimit, gasLimit } // TODO test this works when firing off tx
set({ state: TxStateType.Signing })
const res = await signer.sendTransaction(tx)
set({ state: TxStateType.Confirming })
set({ txHash: res.hash })
const txReceipt = await res.wait()
//tx was mined successfully
if (txReceipt.status === 1) {
const nftsPurchased = findNFTsPurchased(txReceipt, address, selectedAssets, transactionData.route)
const nftsNotPurchased = findNFTsNotPurchased(selectedAssets, nftsPurchased)
set({ state: TxStateType.Success })
return {
nftsPurchased,
nftsNotPurchased,
txReceipt,
}
} else {
set({ state: TxStateType.Failed })
return {
nftsPurchased: [],
nftsNotPurchased: selectedAssets,
txReceipt,
}
}
} catch (e) {
console.log('Error creating multiAssetSwap Transaction', e)
if (e.code === 4001) {
set({ state: TxStateType.Denied })
} else {
set({ state: TxStateType.Invalid })
}
return
}
},
}),
{ name: 'useSendTransactionState' }
)
)
const findNFTsPurchased = (
txReceipt: ContractReceipt,
signerAddress: string,
toBuy: GenieAsset[],
txRoute: RoutingItem[]
): UpdatedGenieAsset[] => {
if (!txReceipt.logs) {
return []
}
const erc721Interface = new Interface(ERC721)
const erc1155Interface = new Interface(ERC1155)
const cryptopunksMarketInterface = new Interface(CryptoPunksMarket)
// Find successfully purchased NFTs (and assign to state nftsPurchased) by parsing events
const transferErc721BuyEvents = txReceipt.logs.filter(
(x) =>
x.topics[0] === erc721Interface.getEventTopic('Transfer') &&
hexStripZeros(x.topics[2]).toLowerCase() === signerAddress.toLowerCase()
)
const transferredErc721 = transferErc721BuyEvents.map((x) => ({
address: x.address,
tokenId: parseInt(x.topics[3]).toString(),
}))
const transferErc1155BuyEvents = txReceipt.logs.filter(
(x) =>
x.topics[0] === erc1155Interface.getEventTopic('TransferSingle') &&
hexStripZeros(x.topics[3]).toLowerCase() === signerAddress.toLowerCase()
)
const transferredErc1155 = transferErc1155BuyEvents.map((x) => ({
address: x.address,
tokenId: erc1155Interface.parseLog(x).args[3].toString(),
}))
// Find transferred CryptoPunks
const transferCryptopunkEvents = txReceipt.logs.filter(
(x) =>
x.topics[0] === cryptopunksMarketInterface.getEventTopic('PunkTransfer') &&
hexStripZeros(x.topics[2]).toLowerCase() === signerAddress.toLowerCase()
)
const transferredCryptopunks = transferCryptopunkEvents.map((x) => ({
address: x.address,
tokenId: cryptopunksMarketInterface.parseLog(x).args[2].toString(),
}))
const allTransferred = [...transferredErc721, ...transferredErc1155, ...transferredCryptopunks]
const transferredItems = toBuy.filter((assetToBuy) => {
return allTransferred.some(
(purchasedNft) =>
assetToBuy.address.toLowerCase() === purchasedNft.address.toLowerCase() &&
parseInt(assetToBuy.tokenId).toString() === purchasedNft.tokenId
)
})
return combineBuyItemsWithTxRoute(transferredItems, txRoute)
}
const findNFTsNotPurchased = (toBuy: GenieAsset[], nftsPurchased: UpdatedGenieAsset[]) => {
const nftsNotPurchased: Array<UpdatedGenieAsset> = []
toBuy.forEach((selectedAsset) => {
const purchasedNft = nftsPurchased.find(
(x) => x.address.toLowerCase() === selectedAsset.address.toLowerCase() && x.tokenId === selectedAsset.tokenId
)
if (!purchasedNft) {
nftsNotPurchased.push(selectedAsset)
}
})
return nftsNotPurchased
}

37
src/nft/hooks/useSweep.ts Normal file

@ -0,0 +1,37 @@
import create from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { GenieAsset } from '../types'
interface SweepState {
sweepAssets: GenieAsset[]
setSweepAssets: (assets: GenieAsset[]) => void
removeSweepAsset: (asset: GenieAsset) => void
reset: () => void
}
export const useSweep = create<SweepState>()(
persist(
devtools((set) => ({
sweepAssets: [],
setSweepAssets: (assets) =>
set(() => {
return { sweepAssets: assets }
}),
removeSweepAsset: (asset) => {
set(({ sweepAssets }) => {
if (sweepAssets.length === 0) return { sweepAssets: [] }
else sweepAssets.find((x) => x.tokenId === asset.tokenId && x.address === asset.address)
const assetsCopy = [...sweepAssets]
assetsCopy.splice(
sweepAssets.findIndex((n) => n.tokenId === asset.tokenId && n.address === asset.address),
1
)
return { sweepAssets: assetsCopy }
})
},
reset: () => set(() => ({ sweepAssets: [] })),
})),
{ name: 'useSweep' }
)
)

@ -0,0 +1,31 @@
import { BigNumber } from '@ethersproject/bignumber'
import { Web3Provider } from '@ethersproject/providers'
import { parseEther } from '@ethersproject/units'
import { useWeb3React } from '@web3-react/core'
import { useNativeCurrencyBalances } from 'state/connection/hooks'
interface WalletBalanceProps {
address: string
balance: string
weiBalance: BigNumber
provider: Web3Provider | undefined
}
export function useWalletBalance(): WalletBalanceProps {
const { account: address, provider } = useWeb3React()
const balanceString = useNativeCurrencyBalances(address ? [address] : [])?.[address ?? '']?.toSignificant(3) || '0'
return address == null
? {
address: '',
balance: '0',
weiBalance: parseEther('0'),
provider: undefined,
}
: {
address,
balance: balanceString,
weiBalance: parseEther(balanceString),
provider,
}
}

@ -0,0 +1,73 @@
import create from 'zustand'
import { devtools } from 'zustand/middleware'
import { WalletAsset, WalletCollection } from '../types'
interface WalletCollectionState {
walletAssets: WalletAsset[]
walletCollections: WalletCollection[]
displayAssets: WalletAsset[]
collectionFilters: string[]
listFilter: string
setWalletAssets: (assets: WalletAsset[]) => void
setWalletCollections: (collections: WalletCollection[]) => void
setCollectionFilters: (address: string) => void
clearCollectionFilters: () => void
setListFilter: (value: string) => void
setDisplayAssets: (walletAssets: WalletAsset[], listFilter: string) => void
}
export const useWalletCollections = create<WalletCollectionState>()(
devtools(
(set) => ({
walletAssets: [],
walletCollections: [],
displayAssets: [],
collectionFilters: [],
listFilter: 'All',
setWalletAssets: (assets) =>
set(() => {
return {
walletAssets: assets?.filter((asset) => asset.asset_contract?.schema_name === 'ERC721'),
}
}),
setWalletCollections: (collections) =>
set(() => {
return { walletCollections: collections }
}),
setCollectionFilters: (address) =>
set(({ collectionFilters }) => {
if (collectionFilters.length === 0) return { collectionFilters: [address] }
else if (!!collectionFilters.find((x) => x === address))
return { collectionFilters: collectionFilters.filter((n) => n !== address) }
else return { collectionFilters: [...collectionFilters, address] }
}),
clearCollectionFilters: () =>
set(() => {
return { collectionFilters: [] }
}),
setListFilter: (value) =>
set(() => {
return { listFilter: value }
}),
setDisplayAssets: (walletAssets, listFilter) =>
set(() => {
return { displayAssets: filterWalletAssets(walletAssets, listFilter) }
}),
}),
{ name: 'useWalletCollections' }
)
)
const filterWalletAssets = (walletAssets: WalletAsset[], listFilter: string) => {
let displayAssets = walletAssets
if (listFilter === 'Listed')
displayAssets = displayAssets?.filter((x) => {
return x.listing_date !== null
})
if (listFilter === 'Unlisted')
displayAssets = displayAssets?.filter((x) => {
return x.listing_date === null
})
return displayAssets
}

@ -0,0 +1,24 @@
import { ActivityEventResponse, ActivityFilter } from '../../types'
export const ActivityFetcher = async (
contractAddress: string,
filters?: ActivityFilter,
cursor?: string
): Promise<ActivityEventResponse> => {
const filterParam =
filters && filters.eventTypes
? `&${filters.eventTypes?.map((eventType) => `event_types[]=${eventType}`).join('&')}`
: ''
const url = `${
process.env.REACT_APP_GENIE_V3_API_URL
}/collections/${contractAddress}/activity?limit=25${filterParam}${cursor ? `&cursor=${cursor}` : ''}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await r.json()
return data.data
}

@ -0,0 +1,126 @@
import { parseEther } from '@ethersproject/units'
import { Trait } from '../../hooks/useCollectionFilters'
import { AssetPayload, CollectionSort, GenieAsset } from '../../types'
export const formatTraits = (traits: Trait[]) => {
const traitObj: Record<string, string[]> = {}
const nonMetaTraits = traits.filter((el) => el.trait_type !== 'Number of traits')
for (const trait of nonMetaTraits) {
if (!traitObj[trait.trait_type]) traitObj[trait.trait_type] = [trait.trait_value]
else traitObj[trait.trait_type].push(trait.trait_value)
}
return traitObj
}
const formatPrice = (x: number | string) => parseEther(x.toString()).toString()
export const AssetsFetcher = async ({
contractAddress,
tokenId,
sort,
markets,
price,
rarityRange,
traits,
searchText,
notForSale,
pageParam,
}: {
contractAddress: string
tokenId?: string
offset?: number
sort?: CollectionSort
markets?: string[]
price?: { high?: number | string; low?: number | string; symbol: string }
rarityRange?: Record<string, unknown>
traits?: Trait[]
searchText?: string
notForSale?: boolean
pageParam: number
}): Promise<GenieAsset[] | undefined> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/assets`
const payload: AssetPayload = {
filters: {
address: contractAddress.toLowerCase(),
traits: {},
searchText,
notForSale,
tokenId,
...rarityRange,
},
fields: {
address: 1,
name: 1,
id: 1,
imageUrl: 1,
currentPrice: 1,
currentUsdPrice: 1,
paymentToken: 1,
animationUrl: 1,
notForSale: 1,
rarity: 1,
tokenId: 1,
},
limit: 25,
offset: pageParam * 25,
}
if (sort) {
payload.sort = sort
}
if (markets) {
payload.markets = markets
}
const numberOfTraits = traits?.filter((trait) => trait.trait_type === 'Number of traits')
if (numberOfTraits) {
payload.filters.numTraits = numberOfTraits.map((el) => ({ traitCount: el.trait_value }))
}
if (traits) {
payload.filters.traits = formatTraits(traits)
}
const low = price?.low ? parseFloat(formatPrice(price.low)) : undefined
const high = price?.high ? parseFloat(formatPrice(price.high)) : undefined
// Only consider sending eth price filters when searching
// across listed assets
if (!notForSale) {
if (low || high) {
payload.filters.currentEthPrice = {}
}
if (low && payload.filters.currentEthPrice) {
payload.filters.currentEthPrice.$gte = low
}
if (high && payload.filters.currentEthPrice) {
payload.filters.currentEthPrice.$lte = high
}
}
try {
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await r.json()
// Unfortunately have to include totalCount into each element. The fetcher
// for swr infinite must return an array.
for (const x of data.data) {
x.totalCount = data.totalCount
x.numTraitsByAmount = data.numTraitsByAmount
}
// Uncomment the lines belo if you want to simulate a delay
// await (async () => await new Promise((resolve) => setTimeout(resolve, 50000)))();
return data.data
} catch (e) {
console.log(e)
return
}
}

@ -0,0 +1,32 @@
export const CollectionPreviewFetcher = async (
address: string
): Promise<
[
{
name: string
bannerImageUrl?: string
}
]
> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/collectionPreview?address=${address}`
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 3000)
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
clearInterval(timeoutId)
const data = await r.json()
return [
{
name: data.data.collectionName,
bannerImageUrl: data.data.bannerImageUrl,
},
]
}

@ -0,0 +1,59 @@
import { isAddress } from '@ethersproject/address'
import { GenieCollection } from '../../types'
export const CollectionStatsFetcher = async (addressOrName: string, recursive = false): Promise<GenieCollection> => {
const isName = !isAddress(addressOrName.toLowerCase())
const url = `${process.env.REACT_APP_GENIE_API_URL}/collections`
if (!isName && !recursive) {
try {
return await CollectionStatsFetcher(addressOrName.toLowerCase(), true)
} catch {
// Handle Error
}
}
const filters = isName
? {
$or: [{ name: { $regex: addressOrName, $options: 'i' } }],
}
: { address: addressOrName }
const payload = {
filters,
limit: isName ? 6 : 1,
fields: isName
? {
name: 1,
imageUrl: 1,
address: 1,
stats: 1,
floorPrice: 1,
}
: {
traits: 1,
stats: 1,
'indexingStats.openSea': 1,
imageUrl: 1,
bannerImageUrl: 1,
twitter: 1,
externalUrl: 1,
instagram: 1,
discordUrl: 1,
marketplaceCount: 1,
floorPrice: 1,
},
offset: 0,
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await r.json()
return data?.data ? data.data[0] : {}
}

@ -0,0 +1,13 @@
import { LooksRareRewardsData } from '../../types'
const looksRareApiAddress = 'https://api.looksrare.org/api/v1'
export const fetchLooksRareRewards = async (address: string): Promise<LooksRareRewardsData> => {
const res = await fetch(`${looksRareApiAddress}/rewards?address=${address}`)
if (res.status !== 200) throw new Error(`LooksRare rewards API errored with status ${res.statusText}`)
const json = await res.json()
return json.data
}

@ -0,0 +1,31 @@
import { GenieCollection } from '../../types'
export const fetchMultipleCollectionStats = async ({
addresses,
}: {
addresses: string[]
}): Promise<GenieCollection[]> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/searchCollections`
const filters = {
address: { $in: addresses },
}
const payload = {
filters,
fields: {
stats: 1,
imageUrl: 1,
address: 1,
name: 1,
},
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await r.json()
return data.data
}

@ -0,0 +1,69 @@
import { GenieAsset, RouteResponse, TokenType } from '../../types'
export const fetchRoute = async ({
toSell,
toBuy,
senderAddress,
}: {
toSell: any
toBuy: any
senderAddress: string
}): Promise<RouteResponse> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/route`
const payload = {
sell: [...toSell].map((x) => buildRouteItem(x)),
buy: [...toBuy].filter((x) => x.tokenType !== 'Dust').map((x) => buildRouteItem(x)),
sender: senderAddress,
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await r.json()
return data
}
type ApiPriceInfo = {
basePrice: string
baseAsset: string
ETHPrice: string
}
type RouteItem = {
id?: string
symbol?: string
name: string
decimals: number
address: string
priceInfo: ApiPriceInfo
tokenType: TokenType
tokenId: string
amount: number
marketplace?: string
collectionName?: string
}
const buildRouteItem = (item: GenieAsset): RouteItem => {
return {
id: item.id,
symbol: item.priceInfo.baseAsset,
name: item.name,
decimals: item.decimals || 0, // 0 for fungible items
address: item.address,
tokenType: item.tokenType,
tokenId: item.tokenId,
marketplace: item.marketplace,
collectionName: item.collectionName,
amount: item.amount || 1, // default 1 for a single asset
priceInfo: {
basePrice: item.priceInfo.basePrice,
baseAsset: item.priceInfo.baseAsset,
ETHPrice: item.priceInfo.ETHPrice,
},
}
}

@ -0,0 +1,48 @@
import { isAddress } from '@ethersproject/address'
import { GenieCollection } from '../../types'
export const fetchSearchCollections = async (addressOrName: string, recursive = false): Promise<GenieCollection[]> => {
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/searchCollections`
const isName = !isAddress(addressOrName.toLowerCase())
if (!isName && !recursive) {
try {
return await fetchSearchCollections(addressOrName.toLowerCase(), true)
} catch {
return []
}
}
const filters = isName
? {
$or: [{ name: { $regex: addressOrName, $options: 'i' } }],
}
: { address: addressOrName }
const payload = {
filters,
limit: 6,
fields: {
name: 1,
imageUrl: 1,
address: 1,
floorPrice: 1,
},
offset: 0,
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (isName) {
const data = (await r.json()) as { data: GenieCollection[] }
return data?.data ? data.data.slice(0, 6) : []
}
const data = await r.json()
return data.data ? [data.data[0]] : []
}

@ -0,0 +1,20 @@
import { FungibleToken } from '../../types'
export const fetchSearchTokens = async (tokenQuery: string): Promise<FungibleToken[]> => {
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/searchTokens?tokenQuery=${tokenQuery}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await r.json()
// TODO Undo favoritism
return (
data.data &&
data.data.sort((a: FungibleToken, b: FungibleToken) => (b.name === 'Uniswap' ? 1 : b.volume24h - a.volume24h))
)
}

@ -0,0 +1,14 @@
import { CollectionInfoForAsset, GenieAsset } from '../../types'
export const fetchSingleAsset = async ({
contractAddress,
tokenId,
}: {
contractAddress: string
tokenId?: string
}): Promise<[GenieAsset, CollectionInfoForAsset]> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/assetDetails?address=${contractAddress}&tokenId=${tokenId}`
const r = await fetch(url)
const data = await r.json()
return [data.asset[0], data.collection]
}

@ -0,0 +1,55 @@
import { Trait } from '../../hooks/useCollectionFilters'
import { AssetPayload, GenieAsset } from '../../types'
import { formatTraits } from './AssetsFetcher'
export const fetchSweep = async ({
contractAddress,
markets,
traits = [],
}: {
contractAddress: string
markets?: string[]
traits?: Trait[]
}): Promise<GenieAsset[]> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/assets`
const payload: AssetPayload = {
filters: { address: contractAddress.toLowerCase(), traits: {}, notForSale: false },
fields: {
address: 1,
name: 1,
id: 1,
imageUrl: 1,
currentPrice: 1,
currentUsdPrice: 1,
paymentToken: 1,
animationUrl: 1,
notForSale: 1,
},
limit: 99,
offset: 0,
}
if (markets) {
payload.markets = markets
}
if (traits) {
payload.filters.traits = formatTraits(traits)
}
const numberOfTraits = traits.filter((trait) => trait.trait_type === 'Number of traits')
if (numberOfTraits) {
payload.filters.numTraits = numberOfTraits.map((el) => ({ traitCount: el.trait_value }))
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await r.json()
return data.data
}

@ -0,0 +1,19 @@
import { TransactionsResponse } from '../../types'
export const fetchTransactions = async (payload: { sweep?: boolean }): Promise<TransactionsResponse[]> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/transactions`
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = (await r.json()) as TransactionsResponse[]
return data.filter(
(x) => x.bannerImage && (payload.sweep ? x.nftCount >= 3 && Math.floor(x.ethValue / 10 ** 18) >= 1 : true)
)
}

@ -0,0 +1,20 @@
import { TimePeriod, TrendingCollection } from '../../types'
export const fetchTrendingCollections = async (payload: {
volumeType: 'eth' | 'nft'
timePeriod: TimePeriod
size: number
}): Promise<TrendingCollection[]> => {
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/collections/trending`
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
const data = await r.json()
return data
}

@ -0,0 +1,16 @@
import { FungibleToken } from '../../types'
export const fetchTrendingTokens = async (numTokens?: number): Promise<FungibleToken[]> => {
const url = `${process.env.REACT_APP_GENIE_V3_API_URL}/tokens/trending${numTokens ? `?numTokens=${numTokens}` : ''}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await r.json()
return data.data
}

@ -0,0 +1,64 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { WalletAsset } from '../../types'
const getEthPrice = (price: any) => {
if (price.toString().includes('e')) {
return BigNumber.from(10).pow(price.toString().split('e+')[1]).toString()
}
return Math.round(price).toString()
}
export const fetchWalletAssets = async ({
ownerAddress,
collectionAddresses,
pageParam,
}: {
ownerAddress: string
collectionAddresses?: string[]
pageParam: number
}): Promise<WalletAsset[]> => {
const collectionAddressesString = collectionAddresses
? collectionAddresses.reduce((str, collectionAddress) => str + `&assetContractAddresses=${collectionAddress}`, '')
: ''
const url = `${
process.env.REACT_APP_GENIE_API_URL
}/walletAssets?address=${ownerAddress}${collectionAddressesString}&limit=25&offset=${pageParam * 25}`
const r = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await r.json()
return data.data.assets.map((asset: any) => {
return {
...asset,
collectionIsVerified: asset.asset_contract.isVerified,
lastPrice: asset.last_sale && formatEther(asset.last_sale.total_price),
floorPrice: asset.collection?.floorPrice,
creatorPercentage: parseFloat(asset.asset_contract.dev_seller_fee_basis_points) / 10000,
date_acquired: asset.last_sale ? asset.last_sale.event_timestamp : asset.asset_contract.created_date,
listing_date: asset.sellOrders.length
? Math.max
.apply(
null,
asset.sellOrders.map(function (order: any) {
return new Date(order.orderCreatedDate)
})
)
.toString()
: null,
floor_sell_order_price: asset?.sellOrders?.length
? Math.min(
...asset.sellOrders.map((order: any) => {
return parseFloat(formatEther(getEthPrice(order.ethPrice)))
})
)
: null,
}
})
}

@ -0,0 +1,14 @@
export * from './AssetsFetcher'
export * from './CollectionPreviewFetcher'
export * from './CollectionStatsFetcher'
export * from './logListing'
export * from './LooksRareRewardsFetcher'
export * from './MultipleCollectionStatsFetcher'
export * from './RouteFetcher'
export * from './SearchCollectionsFetcher'
export * from './SingleAssetFetcher'
export * from './SweepFetcher'
export * from './TransactionsFetcher'
export * from './TrendingCollectionsFetcher'
export * from './triggerPriceUpdatesForCollection'
export * from './WalletAssetsFetcher'

@ -0,0 +1,36 @@
import { AssetRow, ListingMarket } from '../../types'
interface Listing extends AssetRow {
marketplaces: ListingMarket[]
}
export const logListing = async (listings: AssetRow[], userAddress: string): Promise<boolean> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/logGenieList`
const listingsConsolidated: Listing[] = listings.map((el) => ({ ...el, marketplaces: [] }))
const marketplacesById: Record<string, ListingMarket[]> = {}
const listingsWithMarketsConsolidated = listingsConsolidated.reduce((uniqueListings, curr) => {
const key = `${curr.asset.asset_contract.address}-${curr.asset.tokenId}`
if (marketplacesById[key]) {
marketplacesById[key].push(curr.marketplace)
} else {
marketplacesById[key] = [curr.marketplace]
}
if (!uniqueListings.some((listing) => `${listing.asset.asset_contract.address}-${listing.asset.tokenId}` === key)) {
curr.marketplaces = marketplacesById[key]
uniqueListings.push(curr)
}
return uniqueListings
}, [] as Listing[])
const payload = {
listings: listingsWithMarketsConsolidated,
userAddress,
}
const r = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
return r.status === 200
}

@ -0,0 +1,13 @@
export const triggerPriceUpdatesForCollection = async (address: string) => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/collections/refresh`
const r = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
address,
}),
})
return r.json()
}

4
src/nft/queries/index.ts Normal file

@ -0,0 +1,4 @@
export * from './genie'
export * from './looksRare'
export * from './openSea'
export * from './x2y2'

@ -0,0 +1,16 @@
export const createLooksRareOrder = async (payload: any): Promise<boolean> => {
const url = `${process.env.REACT_APP_GENIE_API_URL}/createLooksRareOrder`
const res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
try {
const data = await res.json()
return data.code === 200
} catch (e) {
return false
}
}

@ -0,0 +1,3 @@
export * from './createLooksRareOrder'
export * from './looksRareNonceFetcher'
export * from './looksRareRewardsFetcher'

@ -0,0 +1,14 @@
const looksRareApiAddress = 'https://api.looksrare.org/api/v1'
export const looksRareNonceFetcher = async (address: any): Promise<number | undefined> => {
const res = await fetch(`${looksRareApiAddress}/orders/nonce?address=${address}`)
if (res.status !== 200) {
console.log(`LooksRare nonce API errored with status ${res.statusText}`)
return
}
const json = await res.json()
return parseFloat(json.data)
}

@ -0,0 +1,13 @@
import { LooksRareRewardsData } from '../../types'
const looksRareApiAddress = 'https://api.looksrare.org/api/v1'
export const looksRareRewardsFetcher = async (address: any): Promise<LooksRareRewardsData | string> => {
const res = await fetch(`${looksRareApiAddress}/rewards?address=${address}`)
if (res.status !== 200) throw new Error(`LooksRare rewards API errored with status ${res.statusText}`)
const json = await res.json()
return json.data
}

@ -0,0 +1,30 @@
import { WalletCollection } from '../../types'
export const OSCollectionsFetcher = async ({ params }: any): Promise<WalletCollection[]> => {
let hasEmptyFields = false
for (const v of Object.values(params)) {
if (v === undefined) {
hasEmptyFields = true
}
}
if (hasEmptyFields) return []
const r = await fetch(`https://api.opensea.io/api/v1/collections?${new URLSearchParams(params).toString()}`)
const walletCollections = await r.json()
if (walletCollections) {
return walletCollections
.filter(
(collection: any) =>
collection.primary_asset_contracts.length && collection.primary_asset_contracts[0].schema_name === 'ERC721'
)
.map((collection: any) => ({
address: collection.primary_asset_contracts[0].address,
name: collection.name,
image: collection.image_url,
count: collection.owned_asset_count,
}))
} else {
return []
}
}

@ -0,0 +1,76 @@
import { OPENSEA_BASE_API_PATH } from 'nft/queries/openSea'
export async function PostOpenSeaSellOrder<T>(
apiPath: string,
body?: Record<string, unknown>,
opts: RequestInit = {}
): Promise<T> {
const fetchOpts = {
method: 'POST',
body: body ? JSON.stringify(body) : undefined,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
...opts,
}
const response = await _fetch(apiPath, fetchOpts)
return response.json()
}
async function _fetch(apiPath: string, opts: RequestInit = {}) {
const apiBase = OPENSEA_BASE_API_PATH
const finalUrl = apiBase + apiPath
const finalOpts = {
...opts,
headers: {
...(opts.headers || {}),
},
}
return fetch(finalUrl, finalOpts).then(async (res) => _handleApiResponse(res))
}
async function _handleApiResponse(response: Response) {
if (response.ok) {
return response
}
let result
let errorMessage
try {
result = await response.text()
result = JSON.parse(result)
} catch {
// Result will be undefined or text
}
switch (response.status) {
case 400:
errorMessage = result && result.errors ? result.errors.join(', ') : `Invalid request: ${JSON.stringify(result)}`
break
case 401:
case 403:
errorMessage = `Unauthorized. Full message was '${JSON.stringify(result)}'`
break
case 404:
errorMessage = `Not found. Full message was '${JSON.stringify(result)}'`
break
case 500:
errorMessage = `Internal server error. OpenSea has been alerted, but if the problem persists please contact us via Discord: https://discord.gg/ga8EJbv - full message was ${JSON.stringify(
result
)}`
break
case 503:
errorMessage = `Service unavailable. Please try again in a few minutes. If the problem persists please contact us via Discord: https://discord.gg/ga8EJbv - full message was ${JSON.stringify(
result
)}`
break
default:
errorMessage = `Message: ${JSON.stringify(result)}`
break
}
throw new Error(`API Error ${response.status}: ${errorMessage}`)
}

@ -0,0 +1,10 @@
export const OPENSEA_BASE_API_PATH = 'https://api.opensea.io'
export const OPENSEA_FEE_ADDRESS = '0x8de9c5a032463c561423387a9648c5c7bcc5bc90'
export const OPENSEA_DEFAULT_ZONE = '0x004c00500000ad104d7dbd00e3ae0a5c00560c00'
export const OPENSEA_LISTINGS_API_PATH = '/v2/orders/ethereum/seaport/listings'
export const OPENSEA_DEFAULT_CROSS_CHAIN_CONDUIT_KEY =
'0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000'
export const OPENSEA_CROSS_CHAIN_CONDUIT = '0x1e0049783f008a0085193e00003d00cd54003c71'
export const OPENSEA_KEY_TO_CONDUIT = { [OPENSEA_DEFAULT_CROSS_CHAIN_CONDUIT_KEY]: OPENSEA_CROSS_CHAIN_CONDUIT }
export const OPENSEA_DEFAULT_FEE = 0.025
export const INVERSE_BASIS_POINTS = 10000

@ -0,0 +1,3 @@
export * from './constants'
export * from './OSCollectionsFetcher'
export * from './PostOpenSeaSellOrder'

@ -0,0 +1,25 @@
import { OrderPayload } from '../../utils/x2y2'
export const newX2Y2Order = async (payload: OrderPayload): Promise<boolean> => {
const body = JSON.stringify(payload)
const url = `${process.env.REACT_APP_GENIE_API_URL}/postX2Y2SellOrderWithApiKey`
const ac = new AbortController()
const req = new Request(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
body,
signal: ac.signal,
})
const timeout = setTimeout(() => ac.abort(), 60 * 1000)
try {
const res = await fetch(req)
const data = await res.json()
return data.code === 200
} catch (e) {
return false
} finally {
clearTimeout(timeout)
}
}

@ -0,0 +1,39 @@
import { Theme, vars } from '../css/sprinkles.css'
export const darkTheme: Theme = {
colors: {
error: '#FF494A',
textDisconnect: '#FF494A',
modalBackdrop: 'linear-gradient(0deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7))',
backgroundSecondary: '#23262F',
modalClose: 'rgba(255, 255, 255, 0.08)',
text: '#fff',
modalTextSecondary: 'rgba(255, 255, 255, 0.6)',
// Bryan's colors from Figma that vary dark vs light
blackBlue: '#FFFFFF',
blackBlue20: '#FFFFFF20',
darkGray: vars.color.grey300,
medGray: `#99A1BD3D`,
lightGray: vars.color.grey900,
white: '#000000',
darkGray10: `#99A1BD1A`,
explicitWhite: '#FFFFFF',
magicGradient: vars.color.blue400,
placeholder: vars.color.grey400,
lightGrayButton: vars.color.grey700,
lightGrayContainer: `#99A1BD14`,
lightGrayOverlay: '#35373F',
// Opacities of black and white
white95: '#0E111AF2',
white90: '#000000E5',
white80: '#000000CC',
},
shadows: {
menu: '0px 10px 30px rgba(0, 0, 0, 0.1)',
genieBlue: '0 4px 16px 0 rgba(70, 115, 250, 0.4)',
elevation: '0px 4px 16px rgba(70, 115, 250, 0.4)',
tooltip: '0px 4px 16px rgba(255, 255, 255, 0.2)',
},
}

@ -0,0 +1,39 @@
import { Theme, vars } from '../css/sprinkles.css'
export const lightTheme: Theme = {
colors: {
error: '#FF494A',
textDisconnect: '#FF494A',
modalBackdrop: 'rgba(0, 0, 0, 0.3)',
backgroundSecondary: '#FCFCFD',
modalClose: 'rgba(60, 66, 82, 0.06)',
text: '#25292E',
modalTextSecondary: 'rgba(60, 66, 82, 0.6)',
// Bryan's colors from Figma that vary dark vs light
blackBlue: vars.color.grey900,
blackBlue20: `#0E111A33`,
darkGray: vars.color.grey500,
medGray: `#5E68873D`,
lightGray: vars.color.grey50,
white: '#FFFFFF',
darkGray10: `#5E68871A`,
explicitWhite: '#FFFFFF',
magicGradient: vars.color.pink400,
placeholder: vars.color.grey300,
lightGrayButton: vars.color.grey100,
lightGrayContainer: vars.color.grey100,
lightGrayOverlay: '#E6E8F0',
// Opacities of black and white
white95: '#EDEFF7F2',
white90: '#FFFFFFE5',
white80: '#FFFFFFCC',
},
shadows: {
menu: '0px 10px 30px rgba(0, 0, 0, 0.1)',
genieBlue: '0 4px 16px 0 rgba(251, 17, 142)',
elevation: '0px 4px 16px rgba(70, 115, 250, 0.4)',
tooltip: '0px 4px 16px rgba(10, 10, 59, 0.2)',
},
}

@ -0,0 +1,75 @@
import { ContractReceipt } from '@ethersproject/contracts'
import { GenieAsset, Markets, PriceInfo, TokenType } from '../common'
export interface UpdatedGenieAsset extends GenieAsset {
updatedPriceInfo?: PriceInfo
isUnavailable?: boolean
orderSource?: 'api' | 'stored' | string
}
export enum RoutingActions {
Buy = 'Buy',
Sell = 'Sell',
Swap = 'Swap',
}
export type SellItem = {
id?: string
symbol?: string
name: string
decimals: number
address: string
priceInfo: PriceInfo
tokenType: TokenType
tokenId: string
amount: string // convert to BigNumber
marketplace?: Markets
}
export type BuyItem = {
id?: string
symbol?: string
name: string
decimals: number
address: string
priceInfo: PriceInfo
tokenType: TokenType
tokenId: string
amount: string // convert to BigNumber
marketplace: Markets
collectionName?: string
orderSource?: 'api' | 'stored' | string
}
export type RoutingItem = {
action: RoutingActions
marketplace: string
amountIn: string
assetIn: SellItem | PriceInfo
amountOut: string
assetOut: BuyItem | PriceInfo
}
export interface RouteResponse {
valueToSend: string
route: RoutingItem[]
data: any
to: any
}
export interface TxResponse {
nftsPurchased: UpdatedGenieAsset[]
nftsNotPurchased: UpdatedGenieAsset[]
txReceipt: ContractReceipt
}
export enum TxStateType {
Success = 'Success',
Denied = 'Denied',
Invalid = 'Invalid',
Failed = 'Failed',
New = 'New',
Signing = 'Signing',
Confirming = 'Confirming',
}

@ -0,0 +1 @@
export * from './checkout'

@ -0,0 +1,101 @@
import { Markets, TokenType } from '../common'
export interface AssetPayload {
filters: {
traits?: Record<string, string[]>
address: string
currentEthPrice?: {
$gte?: number
$lte?: number
}
numTraits?: { traitCount: string }[]
name?: string
searchText?: string
notForSale?: boolean
tokenId?: string
}
fields?: Record<string, number>
limit: number
offset?: number
sort?: CollectionSort
markets?: string[]
}
export interface CollectionInfoForAsset {
collectionSymbol: string
collectionDescription: string | null
collectionImageUrl: string
collectionName: string
isVerified: boolean
totalSupply: number
}
export type CollectionSort = Record<
string,
'asc' | 'desc' | 1 | -1 | { $gte?: string | number; $lte?: string | number } | string | number
>
export enum UniformHeight {
unset,
notUniform,
}
export enum ActivityEventType {
Listing = 'LISTING',
Sale = 'SALE',
CancelListing = 'CANCEL_LISTING',
Transfer = 'TRANSFER',
}
export enum OrderStatus {
VALID = 'VALID',
EXECUTED = 'EXECUTED',
CANCELLED = 'CANCELLED',
EXPIRED = 'EXPIRED',
}
export interface ActivityFilter {
collectionAddress?: string
eventTypes?: ActivityEventType[]
marketplaces?: Markets[]
}
export interface ActivityEventResponse {
events: ActivityEvent[]
cursor?: string
}
export interface TokenRarity {
rank: number
score: number
source: string
}
export interface TokenMetadata {
name: string
imageUrl: string
smallImageUrl: string
metadataUrl: string
rarity: TokenRarity
suspiciousFlag: boolean
suspiciousFlaggedBy: string
standard: TokenType
}
export interface ActivityEvent {
collectionAddress: string
tokenId?: string
tokenMetadata?: TokenMetadata
eventType: ActivityEventType
marketplace?: Markets
fromAddress: string
toAddress?: string
transactionHash?: string
orderHash?: string
orderStatus?: OrderStatus
price?: string
symbol?: string
quantity?: number
auctionType?: string
url?: string
eventTimestamp?: number
}

@ -0,0 +1 @@
export * from './collection'

@ -0,0 +1,183 @@
import { SellOrder } from '../sell'
export interface OpenSeaCollection {
name: string
slug: string
image_url: string
description: string
external_url: string
featured: boolean
hidden: boolean
safelist_request_status: string
is_subject_to_whitelist: boolean
large_image_url: string
only_proxied_transfers: boolean
payout_address: string
}
export interface OpenSeaAsset {
id?: number
image_url?: string
image_preview_url?: string
name?: string
token_id?: string
last_sale?: {
total_price: string
}
asset_contract?: {
address: string
schema_name: 'ERC1155' | 'ERC721' | string
asset_contract_type: string
created_date: string
name: string
symbol: string
description: string
external_link: string
image_url: string
default_to_fiat: boolean
only_proxied_transfers: boolean
payout_address: string
}
collection?: OpenSeaCollection
}
interface OpenSeaUser {
user?: null
profile_img_url: string
address: string
config: string
}
export enum TokenType {
ERC20 = 'ERC20',
ERC721 = 'ERC721',
ERC1155 = 'ERC1155',
Dust = 'Dust',
Cryptopunk = 'Cryptopunk',
}
export interface PriceInfo {
ETHPrice: string
USDPrice: string
baseAsset: string
baseDecimals: string
basePrice: string
}
export interface AssetSellOrder {
ammFeePercent: number
ethReserves: number
tokenReserves: number
}
export interface Rarity {
primaryProvider: string
providers: { provider: string; rank: number; url: string; score: number }[]
}
export interface GenieAsset {
id?: string // This would be a random id created and assigned by front end
address: string
notForSale: boolean
collectionName: string
collectionSymbol: string
currentEthPrice: string
currentUsdPrice: string
imageUrl: string
animationUrl: string
marketplace: string
name: string
priceInfo: PriceInfo
openseaSusFlag: boolean
sellorders: SellOrder[]
smallImageUrl: string
tokenId: string
tokenType: TokenType
url: string
totalCount?: number // The totalCount from the query to /assets
amount?: number
decimals?: number
collectionIsVerified?: boolean
rarity?: Rarity
owner: OpenSeaUser
creator: OpenSeaUser
externalLink: string
traits?: {
trait_type: string
value: string
display_type?: any
max_value?: any
trait_count: number
order?: any
}[]
}
export interface GenieCollection {
collectionAddress: string
address: string
indexingStatus: string
isVerified: boolean
name: string
description: string
standard: string
bannerImageUrl?: string
floorPrice: number
stats: {
num_owners: number
floor_price: number
one_day_volume: number
one_day_change: number
one_day_floor_change: number
banner_image_url: string
total_supply: number
total_listings: number
total_volume: number
}
symbol: string
traits: {
trait_type: string
trait_value: string
trait_count: number
floorSellOrder: PriceInfo
floorPrice: number
}[]
numTraitsByAmount: { traitCount: number; numWithTrait: number }[]
indexingStats: { openSea: { successfulExecutionDate: string; lastRequestedAt: string } }
marketplaceCount?: { marketplace: string; count: number }[]
imageUrl: string
twitter?: string
instagram?: string
discordUrl?: string
externalUrl?: string
rarityVerified?: boolean
isFoundation?: boolean
}
export enum Markets {
NFT20 = 'nft20',
NFTX = 'nftx',
Opensea = 'opensea',
Rarible = 'rarible',
Uniswap = 'Uniswap',
Uniswap_V2 = 'Uniswap_V2',
SushiSwap = 'SushiSwap',
SuperRare = 'superrare',
KnownOrigin = 'knownorigin',
WETH = 'weth',
Cryptopunks = 'cryptopunks',
CryptoPhunks = 'cryptophunks',
}
export enum ToolTipType {
pool,
sus,
}
// index starts at 1 for boolean reasons
export interface DropDownOption {
displayText: string
icon?: JSX.Element
onClick: () => void
reverseIndex?: number
reverseOnClick?: () => void
}

@ -0,0 +1 @@
export * from './common'

@ -0,0 +1,77 @@
export enum TimePeriod {
OneDay = 'ONE_DAY',
SevenDays = 'SEVEN_DAYS',
ThirtyDays = 'THIRTY_DAYS',
AllTime = 'ALL_TIME',
}
export type VolumeType = 'nft' | 'eth'
export interface TransactionsResponse {
__v: number
_id: string
bannerImage: string
blockNumber: string
blockTimestamp: string
collections: [string]
createdAt: string
ethValue: number
from_address: string
gas: string
gasPrice: string
hash: string
isVerified: boolean
nftCount: number
profileImage: string
receiptContractAddress: string | null
receiptCumulatioveGasUsed: string
receiptGasUsed: string
receiptStatus: string
sweep: boolean
timestamp: string
to_address: string
updatedAt: string
usdValue: number
title: string
}
export interface TrendingCollection {
name: string
address: string
imageUrl: string
bannerImageUrl: string
isVerified: boolean
volume: number
volumeChange: number
floor: number
floorChange: number
marketCap: number
percentListed: number
owners: number
ownersChange: number
totalSupply: number
sales: number
}
export interface CollectionTableColumn {
collection: {
name: string
address: string
logo: string
isVerified: boolean
}
volume: {
value: number
change: number
type: VolumeType
}
floor: {
value: number
change: number
}
owners: {
value: number
change: number
}
sales: number
totalSupply: number
}

@ -0,0 +1 @@
export * from './discover'

@ -0,0 +1,21 @@
import { UseSortByColumnOptions, UseSortByColumnProps, UseSortByOptions, UseSortByState } from 'react-table'
/* https://github.com/TanStack/table/issues/2970 */
declare module 'react-table' {
export interface TableOptions<D extends Record<string, unknown>>
extends UseExpandedOptions<D>,
UseSortByOptions<D>,
Record<string, any> {}
export interface TableState<D extends Record<string, unknown> = Record<string, unknown>>
extends UseColumnOrderState<D>,
UseSortByState<D> {}
export interface ColumnInterface<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnOptions<D>,
UseSortByColumnOptions<D> {}
export interface ColumnInstance<D extends Record<string, unknown> = Record<string, unknown>>
extends UseFiltersColumnProps<D>,
UseSortByColumnProps<D> {}
}

6
src/nft/types/index.ts Normal file

@ -0,0 +1,6 @@
export * from './checkout'
export * from './collection'
export * from './common'
export * from './discover'
export * from './navbar'
export * from './sell'

@ -0,0 +1 @@
export * from './navbar'

@ -0,0 +1,20 @@
export interface LooksRareRewardsData {
address: string
cumulativeLooksAmount: string
cumulativeLooksProof: string[]
}
export interface FungibleToken {
name: string
address: string
symbol: string
decimals: number
chainId: number
logoURI: string
coinGeckoId: string
priceUsd: number
price24hChange: number
volume24h: number
onDefaultList?: boolean
marketCap: number
}

@ -0,0 +1 @@
export * from './sell'

117
src/nft/types/sell/sell.ts Normal file

@ -0,0 +1,117 @@
import { GenieCollection } from '../common'
export interface ListingMarket {
name: string
fee: number
icon: string
}
export interface ListingWarning {
marketplace: ListingMarket
message: string
}
export interface SellOrder {
assetId: string
ethPrice: number
basePrice: number
baseCurrency: string
baseCurrencyDecimal: number
orderCreatedDate: string
orderClosingDate: string
quantity: number
timestamp: string
marketplace: string
marketplaceUrl: string
orderHash: string
ammFeePercent?: number
ethReserves?: number
tokenReserves?: number
}
export interface WalletAsset {
id?: string
image_url: string
image_preview_url: string
name: string
tokenId: string
asset_contract: {
address: string
schema_name: 'ERC1155' | 'ERC721' | string
asset_contract_type: string
created_date: string
name: string
symbol: string
description: string
external_link: string
image_url: string
default_to_fiat: boolean
only_proxied_transfers: boolean
payout_address: string
}
collection: GenieCollection
collectionIsVerified: boolean
lastPrice: number
floorPrice: number
creatorPercentage: number
listing_date: string
date_acquired: string
sellOrders: SellOrder[]
floor_sell_order_price: number
// Used for creating new listings
expirationTime?: number
marketAgnosticPrice?: string
newListings?: {
price?: string
marketplace: ListingMarket
overrideFloorPrice?: boolean
}[]
marketplaces?: ListingMarket[]
listingWarnings?: ListingWarning[]
}
export interface WalletCollection {
address: string
name: string
image: string
floorPrice: number
count: number
}
export enum ListingStatus {
APPROVED = 'Approved',
CONTINUE = 'Continue',
DEFINED = 'Defined',
FAILED = 'Failed',
PAUSED = 'Paused',
PENDING = 'Pending',
REJECTED = 'Rejected',
SIGNING = 'Signing',
}
export interface ListingRow {
images: string[]
name: string
status: ListingStatus
callback?: () => Promise<void>
}
export interface AssetRow extends ListingRow {
asset: WalletAsset
marketplace: ListingMarket
}
export interface CollectionRow extends ListingRow {
collectionAddress: string
marketplace: ListingMarket
}
// Creating this as an enum and not boolean as we will likely have a success screen state to show
export enum SellPageStateType {
SELECTING,
LISTING,
}
export enum ListingResponse {
TRY_AGAIN,
SUCCESS,
}

19
src/nft/utils/address.ts Normal file

@ -0,0 +1,19 @@
import { isAddress } from '@ethersproject/address'
/**
* Shortens an Ethereum address by N characters
* @param address blockchain address
* @param charsStart amount of character to shorten (from both ends / in the beginning)
* @param charsEnd amount of characters to shorten in the end
* @returns formatted string
*/
export function shortenAddress(address: string, charsStart = 4, charsEnd?: number): string {
const parsed = isAddress(address)
if (!parsed) throw Error(`Invalid 'address' parameter '${address}'.`)
return `${address.substring(0, charsStart + 2)}...${address.substring(42 - (charsEnd || charsStart))}`
}
export function shortenEnsName(name?: string): string | undefined {
return !name || name.length <= 12 ? name : `${name.substring(0, 6)}...eth`
}

@ -0,0 +1,34 @@
import { BigNumber } from '@ethersproject/bignumber'
import { formatEther } from '@ethersproject/units'
import { ActivityEvent, GenieAsset } from 'nft/types'
export const buildActivityAsset = (event: ActivityEvent, collectionName: string, ethPriceInUSD: number): GenieAsset => {
const assetUsdPrice = event.price
? formatEther(
BigNumber.from(event.price)
.mul(BigNumber.from(Math.trunc(ethPriceInUSD * 100)))
.div(100)
)
: '0'
return {
address: event.collectionAddress,
collectionName,
currentEthPrice: event.price,
imageUrl: event.tokenMetadata?.imageUrl,
marketplace: event.marketplace,
name: event.tokenMetadata?.name,
tokenId: event.tokenId,
openseaSusFlag: event.tokenMetadata?.suspiciousFlag,
smallImageUrl: event.tokenMetadata?.smallImageUrl,
collectionSymbol: event.symbol,
currentUsdPrice: assetUsdPrice,
priceInfo: {
USDPrice: assetUsdPrice,
ETHPrice: event.price,
basePrice: event.price,
baseAsset: 'ETH',
},
tokenType: event.tokenMetadata?.standard,
} as GenieAsset
}

@ -0,0 +1,16 @@
export const buildSellObject = (amount: string) => {
return {
address: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
amount,
decimals: 18,
name: 'Ethereum',
priceInfo: {
baseAsset: 'ETH',
basePrice: amount,
ETHPrice: amount,
},
symbol: 'ETH',
tokenId: 'ETH',
tokenType: 'ERC20',
}
}

@ -0,0 +1,45 @@
import { BigNumber } from '@ethersproject/bignumber'
import { GenieAsset, Markets } from '../types'
export const calcPoolPrice = (asset: GenieAsset, position = 0) => {
let amountToBuy: BigNumber = BigNumber.from(0)
let marginalBuy: BigNumber = BigNumber.from(0)
const nft = asset.sellorders[0]
const decimals = BigNumber.from(1).mul(10).pow(18)
const ammFee = nft.ammFeePercent ? (100 + nft.ammFeePercent) * 100 : 110 * 100
if (asset.marketplace === Markets.NFTX) {
const sixteenmul = BigNumber.from(1).mul(10).pow(16)
amountToBuy = BigNumber.from(ammFee)
.div(100)
.mul(position + 1)
amountToBuy = amountToBuy.mul(sixteenmul)
marginalBuy = BigNumber.from(ammFee).div(100).mul(position)
marginalBuy = marginalBuy.mul(sixteenmul)
}
if (asset.marketplace === Markets.NFT20) {
amountToBuy = BigNumber.from(100).mul(position + 1)
amountToBuy = amountToBuy.mul(decimals)
marginalBuy = BigNumber.from(100).mul(position)
marginalBuy = marginalBuy.mul(decimals)
}
const ethReserves = BigNumber.from(nft.ethReserves?.toLocaleString('fullwide', { useGrouping: false }))
const tokenReserves = BigNumber.from(nft.tokenReserves?.toLocaleString('fullwide', { useGrouping: false }))
const numerator = ethReserves.mul(amountToBuy).mul(1000)
const denominator = tokenReserves.sub(amountToBuy).mul(997)
const marginalnumerator = ethReserves.mul(marginalBuy).mul(1000)
const marginaldenominator = tokenReserves.sub(marginalBuy).mul(997)
let price = numerator.div(denominator)
const marginalprice = marginalnumerator.div(marginaldenominator)
price = price.sub(marginalprice)
price = price.mul(101).div(100)
return price.toString()
}

@ -0,0 +1,48 @@
import { Signer } from '@ethersproject/abstract-signer'
import { Contract } from '@ethersproject/contracts'
import { BaseProvider } from '@ethersproject/providers'
const looksRareContract = new Contract('0xea37093ce161f090e443f304e1bf3a8f14d7bb40', [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: 'user', type: 'address' },
{ indexed: true, internalType: 'uint256', name: 'rewardRound', type: 'uint256' },
{ indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
],
name: 'RewardsClaim',
type: 'event',
},
{
inputs: [
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ internalType: 'bytes32[]', name: 'merkleProof', type: 'bytes32[]' },
],
name: 'claim',
outputs: [],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [{ internalType: 'address', name: '', type: 'address' }],
name: 'amountClaimedByUser',
outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
])
export const getClaimedAmount = async ({ address, provider }: { address: string; provider: BaseProvider }) =>
provider && (await looksRareContract.connect(provider).amountClaimedByUser(address))
export const claimLooks = async ({
signer,
looksTotal,
proof,
}: {
signer: Signer
looksTotal: string
proof: string[]
}) => {
await looksRareContract.connect(signer).functions.claim(looksTotal, proof)
}

20
src/nft/utils/colors.ts Normal file

@ -0,0 +1,20 @@
export const foregrounds = ['#001FAA', '#5D31FF', '#8EC3E4', '#F10B00', '#E843D3', '#C4B5FC', '#F88DD5']
export const backgrounds = ['#5DCCB9', '#9AFBCF', '#D1F8E7', '#73F54B', '#D3FB51', '#FCF958']
export function hashCode(text: string) {
let hash = 0
if (text.length === 0) return hash
for (let i = 0; i < text.length; i++) {
const chr = text.charCodeAt(i)
hash = (hash << 3) - hash + chr
hash |= 0
}
return hash
}
export function addressToHashedColor(colors: string[], address: string | null): string | undefined {
if (address == null) return undefined
return colors[Math.abs(hashCode(address.toLowerCase()) % colors.length)]
}

54
src/nft/utils/currency.ts Normal file

@ -0,0 +1,54 @@
import { formatEther, parseEther } from '@ethersproject/units'
export const formatUsdPrice = (price: number) => {
if (price > 1000000) {
return `$${(price / 1000000).toFixed(1)}M`
} else if (price > 1000) {
return `$${(price / 1000).toFixed(1)}K`
} else {
return `$${price.toFixed(2)}`
}
}
export const formatUSDPriceWithCommas = (price: number) => {
return `$${Math.round(price)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`
}
export const formatEthPrice = (price: string) => {
if (!price) return 0
const formattedPrice = parseFloat(formatEther(String(price)))
return (
Math.round(formattedPrice * (formattedPrice >= 1 ? 100 : 1000) + Number.EPSILON) /
(formattedPrice >= 1 ? 100 : 1000)
)
}
// Stringify the `price` anyway because the `price` is being passed as any in some places
export const numberToWei = (amount: number) => {
return parseEther(amount.toString())
}
export const ethNumberStandardFormatter = (amount: string | number | undefined, includeDollarSign = false): string => {
if (!amount) return '-'
const amountInDecimals = parseFloat(amount.toString())
const conditionalDollarSign = includeDollarSign ? '$' : ''
if (amountInDecimals < 0.0001) return `< ${conditionalDollarSign}0.00001`
if (amountInDecimals < 1) return `${conditionalDollarSign}${amountInDecimals.toFixed(3)}`
return (
conditionalDollarSign +
amountInDecimals
.toFixed(2)
.toString()
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
)
}
export const formatWeiToDecimal = (amount: string) => {
if (!amount) return '-'
return ethNumberStandardFormatter(formatEther(amount))
}

18
src/nft/utils/date.ts Normal file

@ -0,0 +1,18 @@
export const isValidDate = (date: number): boolean => {
const d = Date.parse(date.toString())
return isNaN(d) ? false : true
}
export const getTimeDifference = (eventTimestamp: string) => {
const date = new Date(eventTimestamp).getTime()
const diff = new Date().getTime() - date
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`
if (minutes > 1) return `${minutes} minutes ago`
return 'Just now'
}

@ -0,0 +1,72 @@
export function consentRequired(tz: string): boolean {
switch (tz) {
case 'Europe/Vienna':
return true
case 'Europe/Brussels':
return true
case 'Europe/Sofia':
return true
case 'Europe/Zagreb':
return true
case 'Asia/Famagusta':
return true
case 'Asia/Nicosia':
return true
case 'Europe/Prague':
return true
case 'Europe/Copenhagen':
return true
case 'Europe/Tallinn':
return true
case 'Europe/Helsinki':
return true
case 'Europe/Paris':
return true
case 'Europe/Berlin':
return true
case 'Europe/Busingen':
return true
case 'Europe/Athens':
return true
case 'Europe/Budapest':
return true
case 'Europe/Dublin':
return true
case 'Europe/Rome':
return true
case 'Europe/Riga':
return true
case 'Europe/Vilnius':
return true
case 'Europe/Luxembourg':
return true
case 'Europe/Malta':
return true
case 'Europe/Amsterdam':
return true
case 'Europe/Warsaw':
return true
case 'Atlantic/Azores':
return true
case 'Atlantic/Madeira':
return true
case 'Europe/Lisbon':
return true
case 'Europe/Bucharest':
return true
case 'Europe/Bratislava':
return true
case 'Europe/Ljubljana':
return true
case 'Africa/Ceuta':
return true
case 'Atlantic/Canary':
return true
case 'Europe/Madrid':
return true
case 'Europe/Stockholm':
return true
default:
return false
}
}

@ -0,0 +1,15 @@
export enum Currency {
ETH = 'ETH',
LOOKS = 'LOOKS',
MATIC = 'MATIC',
}
export const fetchPrice = async (currency: Currency = Currency.ETH): Promise<number | undefined> => {
try {
const response = await fetch(`https://api.coinbase.com/v2/exchange-rates?currency=${currency}`)
return response.json().then((j) => j.data.rates.USD)
} catch (e) {
console.error(e)
return
}
}

6
src/nft/utils/groupBy.ts Normal file

@ -0,0 +1,6 @@
export const groupBy = <T>(xs: T[], key: string) => {
return xs.reduce((rv: any, x: any) => {
;(rv[x[key]] = rv[x[key]] || []).push(x)
return rv
}, {})
}

@ -0,0 +1,25 @@
import { Provider } from '@ethersproject/abstract-provider'
import { Contract } from '@ethersproject/contracts'
import ERC721 from '../../abis/erc721.json'
import { TokenType } from '../types'
export const isAssetOwnedByUser = async ({
tokenId,
assetAddress,
userAddress,
tokenType,
provider,
}: {
tokenId: string
assetAddress: string
userAddress: string
tokenType: TokenType
provider: Provider
}) => {
if (tokenType === TokenType.ERC721) {
const c = new Contract(assetAddress, ERC721, provider)
return (await c.functions.ownerOf(tokenId)) === userAddress
} else return false
}

9
src/nft/utils/isAudio.ts Normal file

@ -0,0 +1,9 @@
const set = new Set<string>(['mp3', 'wav'])
export const isAudio = (file: string) => {
if (!file) return false
const fileType = file.substring(file.lastIndexOf('.') + 1)
return set.has(fileType)
}

@ -0,0 +1,11 @@
const iOSDevices = ['iPhone', 'iPad', 'iPod', 'iPhone Simulator', 'iPod Simulator', 'iPad Simulator']
export const isIPhoneOrSafari = () => {
const uA = navigator.userAgent
const vendor = navigator.vendor
const platform = navigator.platform
return (
iOSDevices.includes(platform) || (/Safari/i.test(uA) && /Apple Computer/.test(vendor) && !/Mobi|Android/i.test(uA))
)
}

3
src/nft/utils/isVideo.ts Normal file

@ -0,0 +1,3 @@
import extensions from 'video-extensions'
export const isVideo = (path: string | null) => extensions.find((ext) => path?.endsWith(`.${ext}`)) !== undefined

268
src/nft/utils/listNfts.ts Normal file

@ -0,0 +1,268 @@
import { Signer } from '@ethersproject/abstract-signer'
import { BigNumber } from '@ethersproject/bignumber'
import { Contract } from '@ethersproject/contracts'
import { JsonRpcSigner, Web3Provider } from '@ethersproject/providers'
import { parseEther } from '@ethersproject/units'
import { addressesByNetwork, MakerOrder, signMakerOrder, SupportedChainId } from '@looksrare/sdk'
import { Seaport } from '@opensea/seaport-js'
import { ItemType } from '@opensea/seaport-js/lib/constants'
import { ConsiderationInputItem } from '@opensea/seaport-js/lib/types'
import {
OPENSEA_DEFAULT_CROSS_CHAIN_CONDUIT_KEY,
OPENSEA_DEFAULT_ZONE,
OPENSEA_KEY_TO_CONDUIT,
OPENSEA_LISTINGS_API_PATH,
} from 'nft/queries/openSea'
import ERC721 from '../../abis/erc721.json'
import { PostOpenSeaSellOrder } from '../queries'
import { createLooksRareOrder } from '../queries'
import { newX2Y2Order } from '../queries'
import { INVERSE_BASIS_POINTS, OPENSEA_DEFAULT_FEE, OPENSEA_FEE_ADDRESS } from '../queries/openSea'
import { ListingMarket, WalletAsset } from '../types'
import { ListingStatus } from '../types'
import { createSellOrder, encodeOrder, OfferItem, OrderPayload, signOrderData } from './x2y2'
export const ListingMarkets: ListingMarket[] = [
{
name: 'LooksRare',
fee: 2.0,
icon: '/nft/svgs/marketplaces/looksrare.svg',
},
{
name: 'OpenSea',
fee: 2.5,
icon: '/nft/svgs/marketplaces/opensea.svg',
},
{
name: 'X2Y2',
fee: 0.5,
icon: '/nft/svgs/marketplaces/x2y2.svg',
},
]
const createConsiderationItem = (basisPoints: string, recipient: string): ConsiderationInputItem => {
return {
amount: basisPoints,
recipient,
}
}
const getConsiderationItems = (
asset: WalletAsset,
price: BigNumber,
signerAddress: string
): {
sellerFee: ConsiderationInputItem
openseaFee: ConsiderationInputItem
creatorFee?: ConsiderationInputItem
} => {
const openSeaBasisPoints = OPENSEA_DEFAULT_FEE * INVERSE_BASIS_POINTS
const creatorFeeBasisPoints = asset.creatorPercentage * INVERSE_BASIS_POINTS
const sellerBasisPoints = INVERSE_BASIS_POINTS - openSeaBasisPoints - creatorFeeBasisPoints
const openseaFee = price.mul(BigNumber.from(openSeaBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
const creatorFee = price
.mul(BigNumber.from(creatorFeeBasisPoints))
.div(BigNumber.from(INVERSE_BASIS_POINTS))
.toString()
const sellerFee = price.mul(BigNumber.from(sellerBasisPoints)).div(BigNumber.from(INVERSE_BASIS_POINTS)).toString()
return {
sellerFee: createConsiderationItem(sellerFee, signerAddress),
openseaFee: createConsiderationItem(openseaFee, OPENSEA_FEE_ADDRESS),
creatorFee:
creatorFeeBasisPoints > 0 ? createConsiderationItem(creatorFee, asset.asset_contract.payout_address) : undefined,
}
}
export async function approveCollection(
operator: string,
collectionAddress: string,
signer: Signer,
setStatus: (newStatus: ListingStatus) => void
): Promise<void> {
// This will work for both 721s & 1155s because they both have the
// setApprovalForAll() method
const ERC721Contract = new Contract(collectionAddress, ERC721, signer)
const signerAddress = await signer.getAddress()
setStatus(ListingStatus.PENDING)
try {
const approved = await ERC721Contract.isApprovedForAll(signerAddress, operator)
if (approved) {
setStatus(ListingStatus.APPROVED)
return
}
setStatus(ListingStatus.SIGNING)
const approvalTransaction = await ERC721Contract.setApprovalForAll(operator, true)
setStatus(ListingStatus.PENDING)
const tx = await approvalTransaction.wait()
tx.status === 1 ? setStatus(ListingStatus.APPROVED) : setStatus(ListingStatus.FAILED)
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
else setStatus(ListingStatus.FAILED)
}
}
export async function signListing(
marketplace: ListingMarket,
asset: WalletAsset,
signer: JsonRpcSigner,
provider: Web3Provider,
looksRareNonce = 0,
setStatus: (newStatus: ListingStatus) => void
): Promise<boolean> {
const seaport = new Seaport(provider, {
conduitKeyToConduit: OPENSEA_KEY_TO_CONDUIT,
overrides: {
defaultConduitKey: OPENSEA_DEFAULT_CROSS_CHAIN_CONDUIT_KEY,
},
})
const signerAddress = await signer.getAddress()
const listingPrice = asset.newListings?.find((listing) => listing.marketplace.name === marketplace.name)?.price
if (!listingPrice || !asset.expirationTime) return false
switch (marketplace.name) {
case 'OpenSea':
try {
const listingInWei = parseEther(listingPrice)
const { sellerFee, openseaFee, creatorFee } = getConsiderationItems(asset, listingInWei, signerAddress)
const considerationItems = [sellerFee, openseaFee, creatorFee].filter(
(item): item is ConsiderationInputItem => item !== undefined
)
const { executeAllActions } = await seaport.createOrder(
{
offer: [
{
itemType: ItemType.ERC721,
token: asset.asset_contract.address,
identifier: asset.tokenId,
amount: '1',
},
],
consideration: considerationItems,
endTime: asset.expirationTime.toString(),
zone: OPENSEA_DEFAULT_ZONE,
restrictedByZone: true,
allowPartialFills: true,
},
signerAddress
)
const order = await executeAllActions()
const res = await PostOpenSeaSellOrder(OPENSEA_LISTINGS_API_PATH, order)
if (res) setStatus(ListingStatus.APPROVED)
return true
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
else setStatus(ListingStatus.FAILED)
return false
}
case 'LooksRare':
const addresses = addressesByNetwork[SupportedChainId.MAINNET]
const currentTime = Math.round(Date.now() / 1000)
const makerOrder: MakerOrder = {
// true --> ask / false --> bid
isOrderAsk: true,
// signer address of the maker order
signer: signerAddress,
// collection address
collection: asset.asset_contract.address,
// Price in WEI
price: parseEther(listingPrice.toString()),
// Token ID
tokenId: BigNumber.from(asset.tokenId),
// amount of tokens to sell/purchase (must be 1 for ERC721, 1+ for ERC1155)
amount: BigNumber.from(1),
// strategy for trade execution (e.g., DutchAuction, StandardSaleForFixedPrice), see addresses in the SDK
strategy: addresses.STRATEGY_STANDARD_SALE,
// currency address
currency: addresses.WETH,
// order nonce (must be unique unless new maker order is meant to override existing one e.g., lower ask price)
nonce: BigNumber.from(looksRareNonce),
// startTime timestamp in seconds
startTime: BigNumber.from(currentTime),
// endTime timestamp in seconds
endTime: BigNumber.from(asset.expirationTime),
// minimum ratio to be received by the user (per 10000)
minPercentageToAsk: BigNumber.from(10000)
.sub(BigNumber.from(200).add(BigNumber.from(asset.creatorPercentage * 10000)))
.toNumber(),
// params (e.g., price, target account for private sale)
params: [],
}
try {
const signatureHash = await signMakerOrder(
signer,
SupportedChainId.MAINNET,
makerOrder,
process.env.REACT_APP_LOOKSRARE_MARKETPLACE_CONTRACT || ''
)
setStatus(ListingStatus.PENDING)
const payload = {
signature: signatureHash,
tokenId: asset.tokenId,
collection: asset.asset_contract.address,
strategy: addresses.STRATEGY_STANDARD_SALE,
currency: addresses.WETH,
signer: signerAddress,
isOrderAsk: true,
nonce: looksRareNonce,
amount: 1,
price: parseEther(listingPrice.toString()).toString(),
startTime: currentTime,
endTime: asset.expirationTime,
minPercentageToAsk: 10000 - (200 + asset.creatorPercentage * 10000),
params: [],
}
const res = await createLooksRareOrder(payload)
if (res) setStatus(ListingStatus.APPROVED)
return res
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
else setStatus(ListingStatus.FAILED)
return false
}
case 'X2Y2':
const orderItem: OfferItem = {
price: parseEther(listingPrice.toString()),
tokens: [
{
token: asset.asset_contract.address,
tokenId: BigNumber.from(parseFloat(asset.tokenId)),
},
],
}
const order = createSellOrder(signerAddress, asset.expirationTime, [orderItem])
try {
await signOrderData(provider, order)
const payload: OrderPayload = {
order: encodeOrder(order),
isBundle: false,
bundleName: '',
bundleDesc: '',
orderIds: [],
changePrice: false,
isCollection: false,
}
setStatus(ListingStatus.PENDING)
// call server api
const resp = await newX2Y2Order(payload)
if (resp) setStatus(ListingStatus.APPROVED)
return resp
} catch (error) {
if (error.code === 4001) setStatus(ListingStatus.REJECTED)
else setStatus(ListingStatus.FAILED)
return false
}
default:
return false
}
}

11
src/nft/utils/numbers.ts Normal file

@ -0,0 +1,11 @@
export const isNumber = (s: string): boolean => {
const reg = /^-?\d+\.?\d*$/
return reg.test(s) && !isNaN(parseFloat(s)) && isFinite(parseFloat(s))
}
export const formatPercentage = (percentage: string): string => {
if (!percentage) return '-'
return `${parseFloat(percentage)
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')}%`
}

@ -0,0 +1,8 @@
export const putCommas = (value: number) => {
try {
if (!value) return value
return value.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} catch (err) {
return value
}
}

17
src/nft/utils/rarity.ts Normal file

@ -0,0 +1,17 @@
// change this if we change the fallback provider
export const fallbackProvider = 'PopRank'
export const shouldLinkToFallbackProvider = false
export const fallbackProviderLogo = '/nft/logos/poprank.png'
/**
* Add provider mappings based on provider name returned from the backend here
*/
export const rarityProviderLogo: { [key: string]: string } = {
'Rarity Sniper': '/nft/svgs/gem.svg',
Genie: fallbackProviderLogo,
}
export const getRarityProviderLogo = (source?: string): string | null => {
if (!source) return null
return rarityProviderLogo[source] || fallbackProviderLogo
}

@ -0,0 +1,5 @@
export const roundAndPluralize = (i: number, word: string) => {
const rounded = Math.floor(i)
return `${rounded} ${word}${rounded === 1 ? '' : 's'}`
}

Some files were not shown because too many files have changed in this diff Show More