Commit 8dbc91ee authored by Charles Bachmeier's avatar Charles Bachmeier Committed by GitHub

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: default avatarCharles Bachmeier <charlie@genie.xyz>
Co-authored-by: default avatarZach Pomerantz <zzmp@uniswap.org>
parent 783f1974
/* 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 @@ ...@@ -14,11 +14,11 @@
"i18n:compile": "yarn i18n:extract && lingui compile", "i18n:compile": "yarn i18n:extract && lingui compile",
"i18n:pseudo": "lingui extract --locale pseudo && lingui compile", "i18n:pseudo": "lingui extract --locale pseudo && lingui compile",
"prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile", "prepare": "yarn contracts:compile && yarn graphql:generate && yarn i18n:compile",
"start": "react-scripts start", "start": "craco start",
"build": "react-scripts build", "build": "craco build",
"serve": "serve build -l 3000", "serve": "serve build -l 3000",
"lint": "yarn eslint .", "lint": "yarn eslint .",
"test": "react-scripts test --coverage", "test": "craco test --coverage",
"cypress:open": "cypress open --browser chrome --e2e", "cypress:open": "cypress open --browser chrome --e2e",
"cypress:run": "cypress run --browser chrome --e2e" "cypress:run": "cypress run --browser chrome --e2e"
}, },
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "6.4.3",
"@ethersproject/experimental": "^5.4.0", "@ethersproject/experimental": "^5.4.0",
"@graphql-codegen/cli": "1.21.5", "@graphql-codegen/cli": "1.21.5",
"@graphql-codegen/typescript": "1.22.3", "@graphql-codegen/typescript": "1.22.3",
...@@ -85,6 +86,7 @@ ...@@ -85,6 +86,7 @@
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.25",
"@types/testing-library__cypress": "^5.0.5", "@types/testing-library__cypress": "^5.0.5",
"@types/ua-parser-js": "^0.7.35", "@types/ua-parser-js": "^0.7.35",
"@types/uuid": "^8.3.4",
"@types/wcag-contrast": "^3.0.0", "@types/wcag-contrast": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^4", "@typescript-eslint/eslint-plugin": "^4",
"@typescript-eslint/parser": "^4", "@typescript-eslint/parser": "^4",
...@@ -104,7 +106,9 @@ ...@@ -104,7 +106,9 @@
"react-scripts": "^4.0.3", "react-scripts": "^4.0.3",
"serve": "^11.3.2", "serve": "^11.3.2",
"typechain": "^5.0.0", "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": { "dependencies": {
"@amplitude/analytics-browser": "^0.5.1", "@amplitude/analytics-browser": "^0.5.1",
...@@ -115,7 +119,9 @@ ...@@ -115,7 +119,9 @@
"@lingui/core": "^3.14.0", "@lingui/core": "^3.14.0",
"@lingui/macro": "^3.14.0", "@lingui/macro": "^3.14.0",
"@lingui/react": "^3.14.0", "@lingui/react": "^3.14.0",
"@looksrare/sdk": "^0.7.1",
"@metamask/jazzicon": "^2.0.0", "@metamask/jazzicon": "^2.0.0",
"@opensea/seaport-js": "^1.0.2",
"@popperjs/core": "^2.4.4", "@popperjs/core": "^2.4.4",
"@reach/dialog": "^0.10.3", "@reach/dialog": "^0.10.3",
"@reach/portal": "^0.10.3", "@reach/portal": "^0.10.3",
...@@ -135,6 +141,10 @@ ...@@ -135,6 +141,10 @@
"@uniswap/v3-core": "1.0.0", "@uniswap/v3-core": "1.0.0",
"@uniswap/v3-periphery": "^1.1.1", "@uniswap/v3-periphery": "^1.1.1",
"@uniswap/v3-sdk": "^3.9.0", "@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/axis": "^2.12.2",
"@visx/event": "^2.6.0", "@visx/event": "^2.6.0",
"@visx/glyph": "^2.10.0", "@visx/glyph": "^2.10.0",
...@@ -156,11 +166,13 @@ ...@@ -156,11 +166,13 @@
"array.prototype.flat": "^1.2.4", "array.prototype.flat": "^1.2.4",
"array.prototype.flatmap": "^1.2.4", "array.prototype.flatmap": "^1.2.4",
"cids": "^1.0.0", "cids": "^1.0.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.2.0", "copy-to-clipboard": "^3.2.0",
"d3": "^7.6.1", "d3": "^7.6.1",
"d3-curve-circlecorners": "^0.1.6", "d3-curve-circlecorners": "^0.1.6",
"ethers": "^5.1.4", "ethers": "^5.1.4",
"firebase": "^9.1.3", "firebase": "^9.1.3",
"focus-visible": "^5.2.0",
"fortmatic": "^2.4.0", "fortmatic": "^2.4.0",
"graphql": "^15.5.0", "graphql": "^15.5.0",
"graphql-request": "^3.4.0", "graphql-request": "^3.4.0",
...@@ -185,9 +197,11 @@ ...@@ -185,9 +197,11 @@
"react-is": "^17.0.2", "react-is": "^17.0.2",
"react-markdown": "^4.3.1", "react-markdown": "^4.3.1",
"react-popper": "^2.2.3", "react-popper": "^2.2.3",
"react-query": "^3.39.1",
"react-redux": "^8.0.2", "react-redux": "^8.0.2",
"react-router-dom": "^6.3.0", "react-router-dom": "^6.3.0",
"react-spring": "^8.0.27", "react-spring": "^8.0.27",
"react-table": "^7.8.0",
"react-use-gesture": "^6.0.14", "react-use-gesture": "^6.0.14",
"react-virtualized-auto-sizer": "^1.0.2", "react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.5", "react-window": "^1.8.5",
...@@ -200,11 +214,14 @@ ...@@ -200,11 +214,14 @@
"ua-parser-js": "^0.7.28", "ua-parser-js": "^0.7.28",
"use-count-up": "^2.2.5", "use-count-up": "^2.2.5",
"use-resize-observer": "^9.0.2", "use-resize-observer": "^9.0.2",
"uuid": "^8.3.2",
"video-extensions": "^1.2.0",
"wcag-contrast": "^3.0.0", "wcag-contrast": "^3.0.0",
"web-vitals": "^2.1.0", "web-vitals": "^2.1.0",
"workbox-core": "^6.1.0", "workbox-core": "^6.1.0",
"workbox-navigation-preload": "^6.1.0", "workbox-navigation-preload": "^6.1.0",
"workbox-precaching": "^6.1.0", "workbox-precaching": "^6.1.0",
"workbox-routing": "^6.1.0" "workbox-routing": "^6.1.0",
"zustand": "^4.0.0-rc.1"
} }
} }
[ [
{ {
"constant": true,
"inputs": [ "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", "internalType": "address",
"name": "_owner", "name": "to",
"type": "address" "type": "address"
}, },
{ {
"indexed": false,
"internalType": "uint256",
"name": "id",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256", "internalType": "uint256",
"name": "_id", "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": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "id",
"type": "uint256" "type": "uint256"
} }
], ],
...@@ -21,16 +149,165 @@ ...@@ -21,16 +149,165 @@
"type": "uint256" "type": "uint256"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "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": [ "inputs": [
{ {
"internalType": "uint256", "internalType": "uint256",
"name": "_id", "name": "",
"type": "uint256" "type": "uint256"
} }
], ],
...@@ -42,7 +319,6 @@ ...@@ -42,7 +319,6 @@
"type": "string" "type": "string"
} }
], ],
"payable": false,
"stateMutability": "view", "stateMutability": "view",
"type": "function" "type": "function"
} }
......
This diff is collapsed.
import { useEffect, useState } from 'react' import { useEffect, useRef, useState } from 'react'
/** /**
* Debounces updates to a value. * Debounces updates to a value.
...@@ -24,3 +24,34 @@ export default function useDebounce<T>(value: T, delay: number): T { ...@@ -24,3 +24,34 @@ export default function useDebounce<T>(value: T, delay: number): T {
return debouncedValue 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)
}
}
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])
}
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])
}
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' ...@@ -8,6 +8,7 @@ import { BlockNumberProvider } from 'lib/hooks/useBlockNumber'
import { MulticallUpdater } from 'lib/state/multicall' import { MulticallUpdater } from 'lib/state/multicall'
import { StrictMode } from 'react' import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
...@@ -25,6 +26,8 @@ import UserUpdater from './state/user/updater' ...@@ -25,6 +26,8 @@ import UserUpdater from './state/user/updater'
import ThemeProvider, { ThemedGlobalStyle } from './theme' import ThemeProvider, { ThemedGlobalStyle } from './theme'
import RadialGradientByChainUpdater from './theme/RadialGradientByChainUpdater' import RadialGradientByChainUpdater from './theme/RadialGradientByChainUpdater'
const queryClient = new QueryClient()
if (!!window.ethereum) { if (!!window.ethereum) {
window.ethereum.autoRefreshOnNetworkChange = false window.ethereum.autoRefreshOnNetworkChange = false
} }
...@@ -49,21 +52,23 @@ createRoot(container).render( ...@@ -49,21 +52,23 @@ createRoot(container).render(
<StrictMode> <StrictMode>
<Provider store={store}> <Provider store={store}>
<FeatureFlagsProvider> <FeatureFlagsProvider>
<HashRouter> <QueryClientProvider client={queryClient}>
<LanguageProvider> <HashRouter>
<Web3Provider> <LanguageProvider>
<Blocklist> <Web3Provider>
<BlockNumberProvider> <Blocklist>
<Updaters /> <BlockNumberProvider>
<ThemeProvider> <Updaters />
<ThemedGlobalStyle /> <ThemeProvider>
<App /> <ThemedGlobalStyle />
</ThemeProvider> <App />
</BlockNumberProvider> </ThemeProvider>
</Blocklist> </BlockNumberProvider>
</Web3Provider> </Blocklist>
</LanguageProvider> </Web3Provider>
</HashRouter> </LanguageProvider>
</HashRouter>
</QueryClientProvider>
</FeatureFlagsProvider> </FeatureFlagsProvider>
</Provider> </Provider>
</StrictMode> </StrictMode>
......
...@@ -141,3 +141,7 @@ export default function useCurrencyBalance( ...@@ -141,3 +141,7 @@ export default function useCurrencyBalance(
useMemo(() => [currency], [currency]) useMemo(() => [currency], [currency])
)[0] )[0]
} }
export function useCurrencyBalanceString(account: string): string {
return useNativeCurrencyBalances(account ? [account] : [])?.[account ?? '']?.toSignificant(3) ?? ''
}
This diff is collapsed.
This diff is collapsed.
[
{
"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"
}
]
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)
}
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)',
})
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
}
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('')
}
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',
},
])
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,
}
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]
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'
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' }
)
)
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' }
)
)
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]
}
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
},
}))
)
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' }
)
)
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' }
)
)
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' }
)
)
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' }
)
)
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: [] })),
}))
)
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' }
)
)
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' }
)
)
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
}
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' }
)
)
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,
}
}
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
}
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
}
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
}
}
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,
},
]
}
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] : {}
}
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
}
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
}
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,
},
}
}
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]] : []
}
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))
)
}
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]
}
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
}
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)
)
}
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
}
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
}
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,
}
})
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
export * from './genie'
export * from './looksRare'
export * from './openSea'
export * from './x2y2'
This diff is collapsed.
export * from './createLooksRareOrder'
export * from './looksRareNonceFetcher'
export * from './looksRareRewardsFetcher'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment