(Feature) erc721 feature implementation (#570)
* Add Assets sections * (add) collectibles tab * (add) criptokitty items * (add) collectible items, definitive edition * (fix) collectibles were overlapping with bottom banner * (fix) wording * (fix) responsive issues * Install `async-sema` dependency * Create collectible source classes - source from mocked data and opensea, it's extendable to import information from other sources * Update `Collectible` implementation to use new data source * Create constants file to better handle env variables and default values * Add description to item's cards - also added a mocked class with real data * Fix `saveTxToHistory`, remove hardcoded `CALL` * Fix after merge development * Set background color for collectible based on data info - Changed `withStyles` in favor of a hook-like approach with `makeStyles` * Enhance collectible card info and group title * Use current safeAddress to query for collectibles information - also migrated from `withStyles` to `makeStyles` * Use proper key values for lists and set more significant names * update yarn.lock after merge * Fix linting error * Move ethAsToken verification outside loop * Use absolute route for `SendModal` import * Move Collectibles into redux store * Update yarn.lock file * Selectable NFTs * Divide the `collectible` store into `nftAssets` and `nftTokens` - Also updated components to retain functionality - Created a `textShortener` function for better presentation * Update `yarn.lock` * Update `yarn.lock` * Fix item background color * Clears the tokenID select field when the collectible selected changes * Open Send modal from the assets section * Use token name for the token selection dropdown * Add openZeppelin contracts dependency * Create ERC721 getter * Fix types, default values and clean code * Fix: properly refresh list of collectibles when switching safes * Add ReviewCollectible step in send NFT * Change items shadow * Give option to choose what to send by clicking 'Send' button in AddressBook * Disable [Send] button for Collectibles if not owner * Set Coins as default option in assets tab - also fixed styles for `Coins` option * Use collectible icon in send modal * Set default message when no assets available - removed pagination feature * Create SafeVersionProvider to better handle version-related tasks Provides: - current and latest versions, - a boolean indicating a need for update, - an upgradeSafe callback to trigger upgrade from any place, - a list of enabled features, depending on the current version - the latter needs a refactor like extract features outside the provider and define constants for the features. * Force build * Update `yarn.lock` * Disable Manage list for NFTs * Fix container shadow - Also fixes tables shadow, thanks to @gabitoesmiapodo * Enable nested routes for balances (assets) tab * Default to `/balance` if invalid nested path * Disable [Send Collectible] button, if not supported by safe * Change sub-menu buttons to clickable text * Replace Paragraph with Link * Fix invalid props errors for Link component * Fallback to `transferFrom` if `safeTransferFrom` is not implemented * Use `transfer` as fallback to ERC-721's `safeTransferFrom` - need to identify ERC721 token using `transfer` and `name` methods * Display failed transactions * Use react.lazy for collectibles' modals * Identify ERC-721 token transaction * Fix Send Collectibles modal layout/behavior - disable dropdown list if there's no item to pick - fix placeholder for tokens list - fix dropdown list styles * Set default `isSuccessful` flag to `true` * Save version related values into store - each safe has its `currentVersion`, `needsUpdate` and `featuresEnabled` - and safes store has the `latestMasterContractVersion` * Migrate Balance to use store-provided values * Migrate Settings to use store-provided values * Migrate ChooseTxType to use store-provided values * Remove SafeVersionProvider Co-authored-by: Gabriel Rodriguez Alsina <gabriel.rodriguez@altoros.com> Co-authored-by: apane <agustin.pane@gmail.com>
This commit is contained in:
parent
497b672633
commit
5359794e21
|
@ -0,0 +1,35 @@
|
|||
// flow-typed signature: 8bfc0eb0795000bed1976434e91400ae
|
||||
// flow-typed version: <<STUB>>/async-sema_v3.1.0/flow_v0.114.0
|
||||
|
||||
/**
|
||||
* This is an autogenerated libdef stub for:
|
||||
*
|
||||
* 'async-sema'
|
||||
*
|
||||
* Fill this stub out by replacing all the `any` types.
|
||||
*
|
||||
* Once filled out, we encourage you to share your work with the
|
||||
* community by sending a pull request to:
|
||||
* https://github.com/flowtype/flow-typed
|
||||
*/
|
||||
|
||||
declare module 'async-sema' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* We include stubs for each file inside this npm package in case you need to
|
||||
* require those files directly. Feel free to delete any files that aren't
|
||||
* needed.
|
||||
*/
|
||||
declare module 'async-sema/lib' {
|
||||
declare module.exports: any;
|
||||
}
|
||||
|
||||
// Filename aliases
|
||||
declare module 'async-sema/lib/index' {
|
||||
declare module.exports: $Exports<'async-sema/lib'>;
|
||||
}
|
||||
declare module 'async-sema/lib/index.js' {
|
||||
declare module.exports: $Exports<'async-sema/lib'>;
|
||||
}
|
|
@ -46,8 +46,10 @@
|
|||
"@material-ui/core": "4.9.5",
|
||||
"@material-ui/icons": "4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.39",
|
||||
"@openzeppelin/contracts": "^2.5.0",
|
||||
"@testing-library/jest-dom": "5.1.1",
|
||||
"@welldone-software/why-did-you-render": "4.0.5",
|
||||
"async-sema": "^3.1.0",
|
||||
"axios": "0.19.2",
|
||||
"bignumber.js": "9.0.0",
|
||||
"bnc-onboard": "1.3.5",
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React from 'react'
|
||||
import React, { type Node } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Modal from '~/components/Modal'
|
||||
|
@ -40,8 +40,8 @@ const useStyles = makeStyles(() =>
|
|||
|
||||
type Props = {
|
||||
title: string,
|
||||
body: React.Node,
|
||||
footer: React.Node,
|
||||
body: Node,
|
||||
footer: Node,
|
||||
onClose: () => void,
|
||||
}
|
||||
|
||||
|
|
|
@ -30,6 +30,7 @@ const useStyles = makeStyles({
|
|||
padding: '27px 15px',
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
zIndex: '5',
|
||||
},
|
||||
content: {
|
||||
maxWidth: '100%',
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
import React from 'react'
|
||||
import { OnChange } from 'react-final-form-listeners'
|
||||
|
||||
import GnoField from '~/components/forms/Field'
|
||||
type Props = {
|
||||
field: string,
|
||||
set: string,
|
||||
to: string | number | null,
|
||||
}
|
||||
|
||||
const WhenFieldChanges = ({ field, set, to }: Props) => (
|
||||
<GnoField name={set} subscription={{}}>
|
||||
{(
|
||||
// No subscription. We only use Field to get to the change function
|
||||
{ input: { onChange } },
|
||||
) => (
|
||||
<OnChange name={field}>
|
||||
{() => {
|
||||
onChange(to)
|
||||
}}
|
||||
</OnChange>
|
||||
)}
|
||||
</GnoField>
|
||||
)
|
||||
export default WhenFieldChanges
|
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import OpenSea from '~/logic/collectibles/sources/OpenSea'
|
||||
import mockedOpenSea from '~/logic/collectibles/sources/mocked_opensea'
|
||||
|
||||
class MockedOpenSea extends OpenSea {
|
||||
_fetch = async () => {
|
||||
await this._rateLimit()
|
||||
return { json: () => mockedOpenSea }
|
||||
}
|
||||
}
|
||||
|
||||
export default MockedOpenSea
|
|
@ -0,0 +1,108 @@
|
|||
// @flow
|
||||
import { RateLimit } from 'async-sema'
|
||||
|
||||
import type {
|
||||
CollectibleMetadataSource,
|
||||
CollectiblesInfo,
|
||||
NFTAsset,
|
||||
NFTAssets,
|
||||
NFTToken,
|
||||
OpenSeaAsset,
|
||||
} from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import NFTIcon from '~/routes/safe/components/Balances/assets/nft_icon.png'
|
||||
import { OPENSEA_API_KEY } from '~/utils/constants'
|
||||
|
||||
class OpenSea implements CollectibleMetadataSource {
|
||||
_rateLimit = async () => {}
|
||||
|
||||
_endpointsUrls: { [key: number]: string } = {
|
||||
// $FlowFixMe
|
||||
1: 'https://api.opensea.io/api/v1',
|
||||
// $FlowFixMe
|
||||
4: 'https://rinkeby-api.opensea.io/api/v1',
|
||||
}
|
||||
|
||||
_fetch = async (url: string) => {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
return fetch(url, {
|
||||
headers: { 'X-API-KEY': OPENSEA_API_KEY || '' },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenSea class constructor
|
||||
* @param {object} options
|
||||
* @param {number} options.rps - requests per second
|
||||
*/
|
||||
constructor(options: { rps: number }) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
this._rateLimit = RateLimit(options.rps, { timeUnit: 60 * 1000, uniformDistribution: true })
|
||||
}
|
||||
|
||||
static extractAssets(assets: OpenSeaAsset[]): NFTAssets {
|
||||
const extractNFTAsset = (asset): NFTAsset => ({
|
||||
address: asset.asset_contract.address,
|
||||
assetContract: asset.asset_contract,
|
||||
collection: asset.collection,
|
||||
description: asset.asset_contract.name,
|
||||
image: asset.asset_contract.image_url || NFTIcon,
|
||||
name: asset.collection.name,
|
||||
numberOfTokens: 1,
|
||||
slug: asset.collection.slug,
|
||||
symbol: asset.asset_contract.symbol,
|
||||
})
|
||||
|
||||
return assets.reduce((acc: NFTAssets, asset: OpenSeaAsset) => {
|
||||
const address = asset.asset_contract.address
|
||||
|
||||
if (acc[address] === undefined) {
|
||||
acc[address] = extractNFTAsset(asset)
|
||||
} else {
|
||||
// By default, extractNFTAsset sets `numberOfTokens` to 1,
|
||||
// counting the asset recently processed.
|
||||
// If it happens to already exist the asset in the map,
|
||||
// then we just increment the `numberOfTokens` value by 1.
|
||||
acc[address].numberOfTokens = acc[address].numberOfTokens + 1
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
static extractTokens(assets: OpenSeaAsset[]): NFTToken[] {
|
||||
return assets.map((asset: OpenSeaAsset): NFTToken => ({
|
||||
assetAddress: asset.asset_contract.address,
|
||||
color: asset.background_color,
|
||||
description: asset.description,
|
||||
image: asset.image_thumbnail_url || NFTIcon,
|
||||
name: asset.name || `${asset.asset_contract.name} - #${asset.token_id}`,
|
||||
tokenId: asset.token_id,
|
||||
}))
|
||||
}
|
||||
|
||||
static extractCollectiblesInfo(assetResponseJson: { assets: OpenSeaAsset[] }): CollectiblesInfo {
|
||||
return {
|
||||
nftAssets: OpenSea.extractAssets(assetResponseJson.assets),
|
||||
nftTokens: OpenSea.extractTokens(assetResponseJson.assets),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches from OpenSea the list of collectibles, grouped by category,
|
||||
* for the provided Safe Address in the specified Network
|
||||
* @param {string} safeAddress
|
||||
* @param {number} networkId
|
||||
* @returns {Promise<{ nftAssets: Map<string, NFTAsset>, nftTokens: Array<NFTToken> }>}
|
||||
*/
|
||||
async fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, networkId: number) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const metadataSourceUrl = this._endpointsUrls[networkId]
|
||||
const url = `${metadataSourceUrl}/assets/?owner=${safeAddress}`
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const assetsResponse = await this._fetch(url)
|
||||
const assetsResponseJson = await assetsResponse.json()
|
||||
return OpenSea.extractCollectiblesInfo(assetsResponseJson)
|
||||
}
|
||||
}
|
||||
|
||||
export default OpenSea
|
|
@ -0,0 +1,12 @@
|
|||
// @flow
|
||||
import MockedOpenSea from '~/logic/collectibles/sources/MockedOpenSea'
|
||||
import OpenSea from '~/logic/collectibles/sources/OpenSea'
|
||||
import type { CollectibleMetadataSource } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import { COLLECTIBLES_SOURCE } from '~/utils/constants'
|
||||
|
||||
const sources: { [key: string]: CollectibleMetadataSource } = {
|
||||
opensea: new OpenSea({ rps: 4 }),
|
||||
mockedopensea: new MockedOpenSea({ rps: 4 }),
|
||||
}
|
||||
|
||||
export const getConfiguredSource = () => sources[COLLECTIBLES_SOURCE.toLowerCase()]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
import type { NFTAssets, NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
|
||||
export const ADD_NFT_ASSETS = 'ADD_NFT_ASSETS'
|
||||
export const ADD_NFT_TOKENS = 'ADD_NFT_TOKENS'
|
||||
|
||||
export const addNftAssets = createAction<string, *, *>(ADD_NFT_ASSETS, (nftAssets: NFTAssets) => ({
|
||||
nftAssets,
|
||||
}))
|
||||
|
||||
export const addNftTokens = createAction<string, *, *>(ADD_NFT_TOKENS, (nftTokens: NFTToken[]) => ({
|
||||
nftTokens,
|
||||
}))
|
|
@ -0,0 +1,21 @@
|
|||
// @flow
|
||||
import type { Dispatch } from 'redux'
|
||||
|
||||
import { getConfiguredSource } from '~/logic/collectibles/sources'
|
||||
import { addNftAssets, addNftTokens } from '~/logic/collectibles/store/actions/addCollectibles'
|
||||
import { PROVIDER_REDUCER_ID } from '~/logic/wallets/store/reducer/provider'
|
||||
import { safeParamAddressFromStateSelector } from '~/routes/safe/store/selectors'
|
||||
import type { GlobalState } from '~/store'
|
||||
|
||||
const fetchCollectibles = () => async (dispatch: Dispatch<GlobalState>, getState) => {
|
||||
const state = getState()
|
||||
const { network } = state[PROVIDER_REDUCER_ID]
|
||||
const safeAddress = safeParamAddressFromStateSelector(state)
|
||||
const source = getConfiguredSource()
|
||||
const collectibles = await source.fetchAllUserCollectiblesByCategoryAsync(safeAddress, network)
|
||||
|
||||
dispatch(addNftAssets(collectibles.nftAssets))
|
||||
dispatch(addNftTokens(collectibles.nftTokens))
|
||||
}
|
||||
|
||||
export default fetchCollectibles
|
|
@ -0,0 +1,33 @@
|
|||
// @flow
|
||||
import { type ActionType, handleActions } from 'redux-actions'
|
||||
|
||||
import { ADD_NFT_ASSETS, ADD_NFT_TOKENS } from '~/logic/collectibles/store/actions/addCollectibles'
|
||||
import type { NFTAssets, NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
|
||||
export const NFT_ASSETS_REDUCER_ID = 'nftAssets'
|
||||
export const NFT_TOKENS_REDUCER_ID = 'nftTokens'
|
||||
|
||||
export type NFTAssetsState = NFTAssets | {}
|
||||
export type NFTTokensState = NFTToken[]
|
||||
|
||||
export const nftAssetReducer = handleActions<NFTAssetsState, *>(
|
||||
{
|
||||
[ADD_NFT_ASSETS]: (state: NFTAssetsState, action: ActionType<Function>): NFTAssetsState => {
|
||||
const { nftAssets } = action.payload
|
||||
|
||||
return nftAssets
|
||||
},
|
||||
},
|
||||
{},
|
||||
)
|
||||
|
||||
export const nftTokensReducer = handleActions<NFTTokensState, *>(
|
||||
{
|
||||
[ADD_NFT_TOKENS]: (state: NFTTokensState, action: ActionType<Function>): NFTTokensState => {
|
||||
const { nftTokens } = action.payload
|
||||
|
||||
return nftTokens
|
||||
},
|
||||
},
|
||||
[],
|
||||
)
|
|
@ -0,0 +1,6 @@
|
|||
// @flow
|
||||
import { NFT_ASSETS_REDUCER_ID, NFT_TOKENS_REDUCER_ID } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import type { GlobalState } from '~/store'
|
||||
|
||||
export const nftAssetsSelector = (state: GlobalState) => state[NFT_ASSETS_REDUCER_ID]
|
||||
export const nftTokensSelector = (state: GlobalState) => state[NFT_TOKENS_REDUCER_ID]
|
|
@ -1,22 +1,50 @@
|
|||
// @flow
|
||||
import semverLessThan from 'semver/functions/lt'
|
||||
import semverSatisfies from 'semver/functions/satisfies'
|
||||
import semverValid from 'semver/functions/valid'
|
||||
|
||||
import { getSafeLastVersion } from '~/config'
|
||||
import { getGnosisSafeInstanceAt, getSafeMasterContract } from '~/logic/contracts/safeContracts'
|
||||
|
||||
export const FEATURES = [
|
||||
{ name: 'ERC721', validVersion: '>=1.1.1' },
|
||||
{ name: 'ERC1155', validVersion: '>=1.1.1' },
|
||||
]
|
||||
|
||||
export const safeNeedsUpdate = (currentVersion: string, latestVersion: string) => {
|
||||
if (!currentVersion || !latestVersion) {
|
||||
return false
|
||||
}
|
||||
|
||||
const current = semverValid(currentVersion)
|
||||
const latest = semverValid(latestVersion)
|
||||
|
||||
return latest ? semverLessThan(current, latest) : false
|
||||
}
|
||||
|
||||
export const getCurrentSafeVersion = gnosisSafeInstance => gnosisSafeInstance.VERSION()
|
||||
|
||||
export const enabledFeatures = (version: string) =>
|
||||
FEATURES.reduce((acc, feature) => {
|
||||
if (semverSatisfies(version, feature.validVersion)) {
|
||||
acc.push(feature.name)
|
||||
}
|
||||
return acc
|
||||
}, [])
|
||||
|
||||
export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion) => {
|
||||
if (!gnosisSafeInstance || !lastSafeVersion) {
|
||||
return null
|
||||
}
|
||||
const safeMasterVersion = await gnosisSafeInstance.VERSION()
|
||||
const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance)
|
||||
const current = semverValid(safeMasterVersion)
|
||||
const latest = semverValid(lastSafeVersion)
|
||||
const needUpdate = latest ? semverLessThan(current, latest) : false
|
||||
const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion)
|
||||
|
||||
return { current, latest, needUpdate }
|
||||
}
|
||||
|
||||
const getCurrentMasterContractLastVersion = async () => {
|
||||
export const getCurrentMasterContractLastVersion = async () => {
|
||||
const safeMaster = await getSafeMasterContract()
|
||||
let safeMasterVersion
|
||||
try {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
|
||||
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.json'
|
||||
import ERC721 from '@openzeppelin/contracts/build/contracts/ERC721'
|
||||
import { List } from 'immutable'
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
import contract from 'truffle-contract'
|
||||
|
@ -28,10 +29,27 @@ const createHumanFriendlyTokenContract = async () => {
|
|||
return humanErc20Token
|
||||
}
|
||||
|
||||
const createERC721TokenContract = async () => {
|
||||
const web3 = getWeb3()
|
||||
const erc721Token = await contract(ERC721)
|
||||
erc721Token.setProvider(web3.currentProvider)
|
||||
|
||||
return erc721Token
|
||||
}
|
||||
|
||||
export const getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract)
|
||||
|
||||
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
|
||||
|
||||
export const getERC721TokenContract = ensureOnce(createERC721TokenContract)
|
||||
|
||||
export const containsMethodByHash = async (contractAddress: string, methodHash: string) => {
|
||||
const web3 = getWeb3()
|
||||
const byteCode = await web3.eth.getCode(contractAddress)
|
||||
|
||||
return byteCode.indexOf(methodHash.replace('0x', '')) !== -1
|
||||
}
|
||||
|
||||
export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
try {
|
||||
const {
|
||||
|
|
|
@ -222,7 +222,7 @@ const AddressBookTable = ({ classes }: Props) => {
|
|||
onClose={() => setDeleteEntryModalOpen(false)}
|
||||
/>
|
||||
<SendModal
|
||||
activeScreenType="sendFunds"
|
||||
activeScreenType="chooseTxType"
|
||||
ethBalance={ethBalance}
|
||||
isOpen={sendFundsModalOpen}
|
||||
onClose={() => setSendFundsModalOpen(false)}
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import CallMade from '@material-ui/icons/CallMade'
|
||||
import cn from 'classnames'
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Button from '~/components/layout/Button'
|
||||
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import { grantedSelector } from '~/routes/safe/container/selector'
|
||||
import { fontColor, sm, xs } from '~/theme/variables'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
item: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 0 10px 0 rgba(33, 48, 77, 0.10)',
|
||||
boxSizing: 'border-box',
|
||||
cursor: props => (props.granted ? 'pointer' : 'default'),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: '1',
|
||||
minHeight: '250px',
|
||||
minWidth: '0',
|
||||
position: 'relative',
|
||||
|
||||
'&:hover .showOnHover': {
|
||||
opacity: '1',
|
||||
},
|
||||
'&:active .showOnHover': {
|
||||
opacity: '1',
|
||||
},
|
||||
},
|
||||
mainContent: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: '1',
|
||||
position: 'relative',
|
||||
zIndex: '1',
|
||||
},
|
||||
extraContent: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(255, 243, 226, 0.6)',
|
||||
bottom: '0',
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
left: '0',
|
||||
opacity: '0',
|
||||
position: 'absolute',
|
||||
right: '0',
|
||||
top: '0',
|
||||
transition: 'opacity 0.15s ease-out',
|
||||
zIndex: '5',
|
||||
},
|
||||
image: {
|
||||
backgroundColor: props => `#${props.backgroundColor}` || '#f0efee',
|
||||
backgroundPosition: '50% 50%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'contain',
|
||||
borderRadius: '8px',
|
||||
height: '178px',
|
||||
flexGrow: '1',
|
||||
width: '100%',
|
||||
},
|
||||
textContainer: {
|
||||
boxSizing: 'border-box',
|
||||
color: fontColor,
|
||||
flexShrink: '0',
|
||||
fontSize: '12px',
|
||||
lineHeight: '1.4',
|
||||
padding: '15px 22px 20px',
|
||||
},
|
||||
title: {
|
||||
fontWeight: 'bold',
|
||||
margin: '0',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
text: {
|
||||
margin: '0',
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
buttonIcon: {
|
||||
fontSize: 16,
|
||||
marginRight: sm,
|
||||
},
|
||||
sendButton: {
|
||||
borderRadius: xs,
|
||||
minWidth: '85px',
|
||||
|
||||
'& > span': {
|
||||
fontSize: '14px',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type Props = {
|
||||
data: NFTToken,
|
||||
onSend: Function,
|
||||
}
|
||||
|
||||
const Item = ({ data, onSend }: Props) => {
|
||||
const granted = useSelector(grantedSelector)
|
||||
const classes = useStyles({ backgroundColor: data.color, granted })
|
||||
|
||||
return (
|
||||
<div className={classes.item}>
|
||||
<div className={classes.mainContent}>
|
||||
<div className={classes.image} style={{ backgroundImage: `url(${data.image})` }} />
|
||||
<div className={classes.textContainer}>
|
||||
{data.name && (
|
||||
<h3 className={classes.title} title={data.name}>
|
||||
{data.name}
|
||||
</h3>
|
||||
)}
|
||||
{data.description && (
|
||||
<p className={classes.text} title={data.description}>
|
||||
{data.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{granted && (
|
||||
<div className={cn(classes.extraContent, 'showOnHover')}>
|
||||
<Button className={classes.sendButton} color="primary" onClick={onSend} size="small" variant="contained">
|
||||
<CallMade alt="Send" className={classes.buttonIcon} /> Send
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Item
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
After Width: | Height: | Size: 30 KiB |
|
@ -0,0 +1,138 @@
|
|||
// @flow
|
||||
import Card from '@material-ui/core/Card'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Item from './components/Item'
|
||||
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { fontColor, lg, screenSm, screenXs } from '~/theme/variables'
|
||||
|
||||
const useStyles = makeStyles({
|
||||
cardInner: {
|
||||
boxSizing: 'border-box',
|
||||
maxWidth: '100%',
|
||||
padding: '52px 54px',
|
||||
},
|
||||
cardOuter: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
},
|
||||
gridRow: {
|
||||
boxSizing: 'border-box',
|
||||
columnGap: '30px',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
marginBottom: '45px',
|
||||
maxWidth: '100%',
|
||||
rowGap: '45px',
|
||||
|
||||
'&:last-child': {
|
||||
marginBottom: '0',
|
||||
},
|
||||
|
||||
[`@media (min-width: ${screenXs}px)`]: {
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
},
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
gridTemplateColumns: '1fr 1fr 1fr 1fr',
|
||||
},
|
||||
},
|
||||
title: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
margin: '0 0 18px',
|
||||
},
|
||||
titleImg: {
|
||||
backgroundPosition: '50% 50%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'contain',
|
||||
borderRadius: '50%',
|
||||
height: '45px',
|
||||
margin: '0 10px 0 0',
|
||||
width: '45px',
|
||||
},
|
||||
titleText: {
|
||||
color: fontColor,
|
||||
fontSize: '18px',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1.2',
|
||||
margin: '0',
|
||||
},
|
||||
titleFiller: {
|
||||
backgroundColor: '#e8e7e6',
|
||||
flexGrow: '1',
|
||||
height: '2px',
|
||||
marginLeft: '40px',
|
||||
},
|
||||
noData: {
|
||||
fontSize: lg,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
const Collectibles = () => {
|
||||
const classes = useStyles()
|
||||
const [selectedToken, setSelectedToken] = React.useState({})
|
||||
const [sendNFTsModalOpen, setSendNFTsModalOpen] = React.useState(false)
|
||||
const { address, ethBalance, name } = useSelector(safeSelector)
|
||||
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const nftAssetsKeys = Object.keys(nftAssets)
|
||||
|
||||
const handleItemSend = nftToken => {
|
||||
setSelectedToken(nftToken)
|
||||
setSendNFTsModalOpen(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={classes.cardOuter}>
|
||||
<div className={classes.cardInner}>
|
||||
{nftAssetsKeys.length ? (
|
||||
nftAssetsKeys.map(assetAddress => {
|
||||
const nftAsset = nftAssets[assetAddress]
|
||||
|
||||
return (
|
||||
<React.Fragment key={nftAsset.slug}>
|
||||
<div className={classes.title}>
|
||||
<div className={classes.titleImg} style={{ backgroundImage: `url(${nftAsset.image || ''})` }} />
|
||||
<h2 className={classes.titleText}>{nftAsset.name}</h2>
|
||||
<div className={classes.titleFiller} />
|
||||
</div>
|
||||
<div className={classes.gridRow}>
|
||||
{nftTokens
|
||||
.filter(({ assetAddress }) => nftAsset.address === assetAddress)
|
||||
.map(nftToken => (
|
||||
<Item
|
||||
data={nftToken}
|
||||
key={`${nftAsset.slug}_${nftToken.tokenId}`}
|
||||
onSend={() => handleItemSend(nftToken)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<Paragraph className={classes.noData}>No collectibles available</Paragraph>
|
||||
)}
|
||||
</div>
|
||||
<SendModal
|
||||
activeScreenType="sendCollectible"
|
||||
ethBalance={ethBalance}
|
||||
isOpen={sendNFTsModalOpen}
|
||||
onClose={() => setSendNFTsModalOpen(false)}
|
||||
safeAddress={address}
|
||||
safeName={name}
|
||||
selectedToken={selectedToken}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default Collectibles
|
|
@ -0,0 +1,209 @@
|
|||
// @flow
|
||||
export type AssetContractType = 'fungible' | 'non-fungible' | 'semi-fungible' | 'unknown'
|
||||
|
||||
export type CollectibleContract = {
|
||||
address: string,
|
||||
asset_contract_type: AssetContractType,
|
||||
created_date: string,
|
||||
name: string,
|
||||
nft_version: string,
|
||||
opensea_version: ?string,
|
||||
owner: ?number,
|
||||
schema_name: string,
|
||||
symbol: string,
|
||||
total_supply: ?string,
|
||||
description: ?string,
|
||||
external_link: ?string,
|
||||
image_url: ?string,
|
||||
default_to_fiat: boolean,
|
||||
dev_buyer_fee_basis_points: number,
|
||||
dev_seller_fee_basis_points: number,
|
||||
only_proxied_transfers: boolean,
|
||||
opensea_buyer_fee_basis_points: number,
|
||||
opensea_seller_fee_basis_points: number,
|
||||
buyer_fee_basis_points: number,
|
||||
seller_fee_basis_points: number,
|
||||
payout_address: ?string,
|
||||
}
|
||||
|
||||
export type Range = {
|
||||
min: number,
|
||||
max: number,
|
||||
}
|
||||
|
||||
export type Traits = {
|
||||
cooldown_index: Range,
|
||||
generation: Range,
|
||||
fancy_ranking: Range,
|
||||
}
|
||||
|
||||
export type Stats = {
|
||||
seven_day_volume: number,
|
||||
seven_day_change: number,
|
||||
total_volume: number,
|
||||
count: number,
|
||||
num_owners: number,
|
||||
market_cap: number,
|
||||
average_price: number,
|
||||
items_sold: number,
|
||||
}
|
||||
|
||||
export type DisplayData = {
|
||||
card_display_style?: ?string,
|
||||
images: ?(string[]),
|
||||
}
|
||||
|
||||
export type OpenSeaCollection = {
|
||||
primary_asset_contracts?: CollectibleContract[],
|
||||
traits?: Traits | {},
|
||||
stats?: Stats,
|
||||
banner_image_url: ?string,
|
||||
chat_url: ?string,
|
||||
created_date: string,
|
||||
default_to_fiat: boolean,
|
||||
description: ?string,
|
||||
dev_buyer_fee_basis_points: string,
|
||||
dev_seller_fee_basis_points: string,
|
||||
display_data: DisplayData,
|
||||
external_url: ?string,
|
||||
featured: boolean,
|
||||
featured_image_url: ?string,
|
||||
hidden: boolean,
|
||||
image_url: ?string,
|
||||
is_subject_to_whitelist: boolean,
|
||||
large_image_url: ?string,
|
||||
name: string,
|
||||
only_proxied_transfers: boolean,
|
||||
opensea_buyer_fee_basis_points: string,
|
||||
opensea_seller_fee_basis_points: string,
|
||||
payout_address: ?string,
|
||||
require_email: boolean,
|
||||
short_description: ?string,
|
||||
slug: string,
|
||||
wiki_url: ?string,
|
||||
owned_asset_count?: number,
|
||||
}
|
||||
|
||||
export type OpenSeaAccount = {
|
||||
user: ?number | string,
|
||||
profile_img_url: string,
|
||||
address: string,
|
||||
config: string,
|
||||
discord_id: string,
|
||||
}
|
||||
|
||||
export type OpenSeaTransaction = {
|
||||
id: number,
|
||||
from_account: OpenSeaAccount,
|
||||
to_account: OpenSeaAccount,
|
||||
created_date: string,
|
||||
modified_date: string,
|
||||
transaction_hash: string,
|
||||
transaction_index: string,
|
||||
block_number: string,
|
||||
block_hash: string,
|
||||
timestamp: string,
|
||||
}
|
||||
|
||||
export type OpenSeaToken = {
|
||||
symbol: string,
|
||||
address: string,
|
||||
image_url: ?string,
|
||||
name: string,
|
||||
decimals: number,
|
||||
eth_price: string,
|
||||
usd_price: string,
|
||||
}
|
||||
|
||||
export type OpenSeaSale = {
|
||||
event_type: string,
|
||||
auction_type: ?string,
|
||||
total_price: string,
|
||||
transaction: OpenSeaTransaction,
|
||||
payment_token: OpenSeaToken,
|
||||
}
|
||||
|
||||
export type OpenSeaAsset = {
|
||||
token_id: string,
|
||||
num_sales: number,
|
||||
background_color: string,
|
||||
image_url: string,
|
||||
image_preview_url: string,
|
||||
image_thumbnail_url: string,
|
||||
image_original_url: string,
|
||||
animation_url: ?string,
|
||||
animation_original_url: ?string,
|
||||
name: string,
|
||||
description: string,
|
||||
external_link: string,
|
||||
asset_contract: CollectibleContract,
|
||||
owner: OpenSeaAccount,
|
||||
permalink: string,
|
||||
collection: OpenSeaCollection,
|
||||
decimals: ?(number | string),
|
||||
auctions: ?(number | string),
|
||||
sell_orders: ?(string[]),
|
||||
traits: {}[],
|
||||
last_sale: OpenSeaSale,
|
||||
top_bid: ?(number | string),
|
||||
current_price: ?(number | string),
|
||||
current_escrow_price: ?(number | string),
|
||||
listing_date: ?string,
|
||||
is_presale: boolean,
|
||||
transfer_fee_payment_token: ?(number | string),
|
||||
transfer_fee: ?(number | string),
|
||||
}
|
||||
|
||||
export type AssetCollectible = {
|
||||
tokenId: string,
|
||||
title: string,
|
||||
text: ?string,
|
||||
color: string,
|
||||
image: string,
|
||||
assetUrl: string,
|
||||
description: string,
|
||||
order?: ?string,
|
||||
assetAddress: string,
|
||||
asset: CollectibleContract,
|
||||
collection: OpenSeaCollection,
|
||||
}
|
||||
|
||||
export type CollectibleData = {
|
||||
image: ?string,
|
||||
slug: string,
|
||||
title: string,
|
||||
data: AssetCollectible[],
|
||||
}
|
||||
|
||||
export type NFTAsset = {
|
||||
address: string,
|
||||
assetContract: CollectibleContract,
|
||||
collection: OpenSeaCollection,
|
||||
description: ?string,
|
||||
image: ?string,
|
||||
name: string,
|
||||
numberOfTokens: number,
|
||||
slug: string,
|
||||
symbol: string,
|
||||
}
|
||||
|
||||
export type NFTToken = {
|
||||
assetAddress: string,
|
||||
color: ?string,
|
||||
description: string,
|
||||
image: string,
|
||||
name: string,
|
||||
tokenId: string,
|
||||
}
|
||||
|
||||
export type NFTAssets = { [key: string]: NFTAsset }
|
||||
|
||||
export type CollectiblesInfo = {
|
||||
nftAssets: NFTAssets,
|
||||
nftTokens: NFTToken[],
|
||||
}
|
||||
|
||||
export interface CollectibleMetadataSource {
|
||||
constructor(options: { rps: number }): void;
|
||||
fetchAllUserCollectiblesByCategoryAsync(safeAddress: string, networkId: number): Promise<CollectiblesInfo>;
|
||||
}
|
|
@ -1,35 +1,46 @@
|
|||
// @flow
|
||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import cn from 'classnames'
|
||||
import { List } from 'immutable'
|
||||
import React, { Suspense, useEffect, useState } from 'react'
|
||||
|
||||
import Modal from '~/components/Modal'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
|
||||
const ChooseTxType = React.lazy(() => import('./screens/ChooseTxType'))
|
||||
|
||||
const SendFunds = React.lazy(() => import('./screens/SendFunds'))
|
||||
|
||||
const SendCollectible = React.lazy(() => import('./screens/SendCollectible'))
|
||||
|
||||
const ReviewCollectible = React.lazy(() => import('./screens/ReviewCollectible'))
|
||||
|
||||
const ReviewTx = React.lazy(() => import('./screens/ReviewTx'))
|
||||
|
||||
const SendCustomTx = React.lazy(() => import('./screens/SendCustomTx'))
|
||||
|
||||
const ReviewCustomTx = React.lazy(() => import('./screens/ReviewCustomTx'))
|
||||
|
||||
type ActiveScreen = 'chooseTxType' | 'sendFunds' | 'reviewTx' | 'sendCustomTx' | 'reviewCustomTx'
|
||||
type ActiveScreen =
|
||||
| 'chooseTxType'
|
||||
| 'sendFunds'
|
||||
| 'reviewTx'
|
||||
| 'sendCustomTx'
|
||||
| 'reviewCustomTx'
|
||||
| 'sendCollectible'
|
||||
| 'reviewCollectible'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
isOpen: boolean,
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
ethBalance: string,
|
||||
tokens: List<Token>,
|
||||
selectedToken: string,
|
||||
createTransaction: Function,
|
||||
selectedToken?: string | NFTToken | {},
|
||||
createTransaction?: Function,
|
||||
activeScreenType: ActiveScreen,
|
||||
recipientAddress?: string,
|
||||
}
|
||||
|
@ -43,7 +54,7 @@ type TxStateType =
|
|||
}
|
||||
| Object
|
||||
|
||||
const styles = () => ({
|
||||
const useStyles = makeStyles({
|
||||
scalableModalWindow: {
|
||||
height: 'auto',
|
||||
},
|
||||
|
@ -51,19 +62,17 @@ const styles = () => ({
|
|||
height: 'auto',
|
||||
position: 'static',
|
||||
},
|
||||
loaderStyle: {
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
const loaderStyle = {
|
||||
height: '500px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}
|
||||
|
||||
const Send = ({
|
||||
const SendModal = ({
|
||||
activeScreenType,
|
||||
classes,
|
||||
createTransaction,
|
||||
ethBalance,
|
||||
isOpen,
|
||||
|
@ -74,6 +83,7 @@ const Send = ({
|
|||
selectedToken,
|
||||
tokens,
|
||||
}: Props) => {
|
||||
const classes = useStyles()
|
||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType')
|
||||
const [tx, setTx] = useState<TxStateType>({})
|
||||
|
||||
|
@ -94,6 +104,11 @@ const Send = ({
|
|||
setTx(customTxInfo)
|
||||
}
|
||||
|
||||
const handleSendCollectible = txInfo => {
|
||||
setActiveScreen('reviewCollectible')
|
||||
setTx(txInfo)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
description="Send Tokens Form"
|
||||
|
@ -104,12 +119,14 @@ const Send = ({
|
|||
>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div style={loaderStyle}>
|
||||
<div className={classes.loaderStyle}>
|
||||
<CircularProgress size={40} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
|
||||
{activeScreen === 'chooseTxType' && (
|
||||
<ChooseTxType onClose={onClose} recipientAddress={recipientAddress} setActiveScreen={setActiveScreen} />
|
||||
)}
|
||||
{activeScreen === 'sendFunds' && (
|
||||
<SendFunds
|
||||
ethBalance={ethBalance}
|
||||
|
@ -141,6 +158,7 @@ const Send = ({
|
|||
initialValues={tx}
|
||||
onClose={onClose}
|
||||
onSubmit={handleCustomTxCreation}
|
||||
recipientAddress={recipientAddress}
|
||||
safeAddress={safeAddress}
|
||||
safeName={safeName}
|
||||
/>
|
||||
|
@ -156,9 +174,21 @@ const Send = ({
|
|||
tx={tx}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'sendCollectible' && (
|
||||
<SendCollectible
|
||||
initialValues={tx}
|
||||
onClose={onClose}
|
||||
onNext={handleSendCollectible}
|
||||
recipientAddress={recipientAddress}
|
||||
selectedToken={selectedToken}
|
||||
/>
|
||||
)}
|
||||
{activeScreen === 'reviewCollectible' && (
|
||||
<ReviewCollectible onClose={onClose} onPrev={() => setActiveScreen('sendCollectible')} tx={tx} />
|
||||
)}
|
||||
</Suspense>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Send)
|
||||
export default SendModal
|
||||
|
|
|
@ -1,22 +1,26 @@
|
|||
// @flow
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { withStyles } from '@material-ui/core/styles'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import classNames from 'classnames/bind'
|
||||
import * as React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import Code from '../assets/code.svg'
|
||||
import Collectible from '../assets/collectibles.svg'
|
||||
import Token from '../assets/token.svg'
|
||||
|
||||
import { mustBeEthereumContractAddress } from '~/components/forms/validator'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { lg, md, sm } from '~/theme/variables'
|
||||
|
||||
const styles = () => ({
|
||||
const useStyles = makeStyles({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'space-between',
|
||||
|
@ -26,6 +30,14 @@ const styles = () => ({
|
|||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
disclaimer: {
|
||||
marginBottom: `-${md}`,
|
||||
paddingTop: md,
|
||||
textAlign: 'center',
|
||||
},
|
||||
disclaimerText: {
|
||||
fontSize: md,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
|
@ -51,47 +63,96 @@ const styles = () => ({
|
|||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
recipientAddress?: string,
|
||||
setActiveScreen: Function,
|
||||
}
|
||||
|
||||
const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Send
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Row align="center">
|
||||
<Col className={classes.buttonColumn} layout="column" middle="xs">
|
||||
<Button
|
||||
className={classes.firstButton}
|
||||
color="primary"
|
||||
minHeight={52}
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendFunds')}
|
||||
variant="contained"
|
||||
>
|
||||
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
||||
Send funds
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
minHeight={52}
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendCustomTx')}
|
||||
variant="outlined"
|
||||
>
|
||||
<Img alt="Send custom transaction" className={classNames(classes.leftIcon, classes.iconSmall)} src={Code} />
|
||||
Send custom transaction
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: Props) => {
|
||||
const classes = useStyles()
|
||||
const { featuresEnabled } = useSelector(safeSelector)
|
||||
const erc721Enabled = featuresEnabled.includes('ERC721')
|
||||
const [disableCustomTx, setDisableCustomTx] = React.useState(!!recipientAddress)
|
||||
|
||||
export default withStyles(styles)(ChooseTxType)
|
||||
React.useEffect(() => {
|
||||
let isCurrent = true
|
||||
const isContract = async () => {
|
||||
if (recipientAddress && isCurrent) {
|
||||
setDisableCustomTx(!!(await mustBeEthereumContractAddress(recipientAddress)))
|
||||
}
|
||||
}
|
||||
|
||||
isContract()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [recipientAddress])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Send
|
||||
</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
{!!recipientAddress && (
|
||||
<Row align="center">
|
||||
<Col className={classes.disclaimer} layout="column" middle="xs">
|
||||
<Paragraph className={classes.disclaimerText} noMargin>
|
||||
Please select what you will send to {recipientAddress}
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<Row align="center">
|
||||
<Col className={classes.buttonColumn} layout="column" middle="xs">
|
||||
<Button
|
||||
className={classes.firstButton}
|
||||
color="primary"
|
||||
minHeight={52}
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendFunds')}
|
||||
variant="contained"
|
||||
>
|
||||
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
||||
Send funds
|
||||
</Button>
|
||||
{erc721Enabled && (
|
||||
<Button
|
||||
className={classes.firstButton}
|
||||
color="primary"
|
||||
minHeight={52}
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendCollectible')}
|
||||
variant="contained"
|
||||
>
|
||||
<Img
|
||||
alt="Send collectible"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
src={Collectible}
|
||||
/>
|
||||
Send collectible
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={disableCustomTx}
|
||||
minHeight={52}
|
||||
minWidth={260}
|
||||
onClick={() => setActiveScreen('sendCustomTx')}
|
||||
variant="outlined"
|
||||
>
|
||||
<Img alt="Send custom transaction" className={classNames(classes.leftIcon, classes.iconSmall)} src={Code} />
|
||||
Send custom transaction
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChooseTxType
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
// @flow
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import { List } from 'immutable'
|
||||
import { withSnackbar } from 'notistack'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import CopyBtn from '~/components/CopyBtn'
|
||||
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
||||
import { estimateTxGasCosts } from '~/logic/safe/transactions/gasNew'
|
||||
import {
|
||||
containsMethodByHash,
|
||||
getERC721TokenContract,
|
||||
getHumanFriendlyToken,
|
||||
} from '~/logic/tokens/store/actions/fetchTokens'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
|
||||
import { getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { sm } from '~/theme/variables'
|
||||
import { textShortener } from '~/utils/strings'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '0x42842e0e'
|
||||
|
||||
type Props = {
|
||||
onClose: () => void,
|
||||
onPrev: () => void,
|
||||
classes: Object,
|
||||
tx: Object,
|
||||
tokens: List<Token>,
|
||||
createTransaction: Function,
|
||||
enqueueSnackbar: Function,
|
||||
closeSnackbar: Function,
|
||||
}
|
||||
|
||||
const ReviewCollectible = ({ closeSnackbar, enqueueSnackbar, onClose, onPrev, tx }: Props) => {
|
||||
const classes = useStyles()
|
||||
const shortener = textShortener()
|
||||
const dispatch = useDispatch()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const [gasCosts, setGasCosts] = useState<string>('< 0.001')
|
||||
const txToken = nftTokens.find(
|
||||
({ assetAddress, tokenId }) => assetAddress === tx.assetAddress && tokenId === tx.nftTokenId,
|
||||
)
|
||||
const [data, setData] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let isCurrent = true
|
||||
|
||||
const estimateGas = async () => {
|
||||
const { fromWei, toBN } = getWeb3().utils
|
||||
|
||||
const supportsSafeTransfer = await containsMethodByHash(tx.assetAddress, SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)
|
||||
const methodToCall = supportsSafeTransfer ? SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH : 'transfer'
|
||||
const transferParams = [tx.recipientAddress, tx.nftTokenId]
|
||||
const params = methodToCall === 'transfer' ? transferParams : [safeAddress, ...transferParams]
|
||||
|
||||
const ERC721Token = methodToCall === 'transfer' ? await getHumanFriendlyToken() : await getERC721TokenContract()
|
||||
const tokenInstance = await ERC721Token.at(tx.assetAddress)
|
||||
const txData = tokenInstance.contract.methods[methodToCall](...params).encodeABI()
|
||||
|
||||
const estimatedGasCosts = await estimateTxGasCosts(safeAddress, tx.recipientAddress, txData)
|
||||
const gasCostsAsEth = fromWei(toBN(estimatedGasCosts), 'ether')
|
||||
const formattedGasCosts = formatAmount(gasCostsAsEth)
|
||||
|
||||
if (isCurrent) {
|
||||
setGasCosts(formattedGasCosts)
|
||||
setData(txData)
|
||||
}
|
||||
}
|
||||
|
||||
estimateGas()
|
||||
|
||||
return () => {
|
||||
isCurrent = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const submitTx = async () => {
|
||||
dispatch(
|
||||
createTransaction({
|
||||
safeAddress,
|
||||
to: tx.assetAddress,
|
||||
valueInWei: '0',
|
||||
txData: data,
|
||||
notifiedTransaction: TX_NOTIFICATION_TYPES.STANDARD_TX,
|
||||
enqueueSnackbar,
|
||||
closeSnackbar,
|
||||
}),
|
||||
)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.headingText} noMargin weight="bolder">
|
||||
Send Funds
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>2 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<Block className={classes.container}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={tx.recipientAddress} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Paragraph className={classes.address} noMargin weight="bolder">
|
||||
{tx.recipientAddress}
|
||||
</Paragraph>
|
||||
<CopyBtn content={tx.recipientAddress} />
|
||||
<EtherscanBtn type="address" value={tx.recipientAddress} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
{textShortener({ charsStart: 40, charsEnd: 0 })(tx.assetName)}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{txToken && (
|
||||
<Row align="center" margin="md">
|
||||
<Img alt={txToken.name} height={28} onError={setImageToPlaceholder} src={txToken.image} />
|
||||
<Paragraph className={classes.amount} noMargin size="md">
|
||||
{shortener(txToken.name)} (Token ID: {shortener(txToken.tokenId)})
|
||||
</Paragraph>
|
||||
</Row>
|
||||
)}
|
||||
<Row>
|
||||
<Paragraph>
|
||||
{`You're about to create a transaction and will have to confirm it with your currently connected wallet. Make sure you have ${gasCosts} (fee price) ETH in this wallet to fund this confirmation.`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline style={{ position: 'absolute', bottom: 85 }} />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onPrev}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="submit-tx-btn"
|
||||
disabled={!data}
|
||||
minWidth={140}
|
||||
onClick={submitTx}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default withSnackbar(ReviewCollectible)
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
import { lg, md, secondaryText, sm } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
color: secondaryText,
|
||||
marginRight: 'auto',
|
||||
marginLeft: '20px',
|
||||
},
|
||||
headingText: {
|
||||
fontSize: lg,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
container: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
amount: {
|
||||
marginLeft: sm,
|
||||
},
|
||||
address: {
|
||||
marginRight: sm,
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
},
|
||||
},
|
||||
submitButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginLeft: '15px',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,85 @@
|
|||
// @flow
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { selectStyles, selectedTokenStyles } from './style'
|
||||
|
||||
import Field from '~/components/forms/Field'
|
||||
import SelectField from '~/components/forms/SelectField'
|
||||
import { required } from '~/components/forms/validator'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import type { NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import { textShortener } from '~/utils/strings'
|
||||
|
||||
type SelectedCollectibleProps = {
|
||||
tokens: NFTTokensState,
|
||||
tokenId: string | number,
|
||||
}
|
||||
|
||||
const useSelectedCollectibleStyles = makeStyles(selectedTokenStyles)
|
||||
|
||||
const SelectedCollectible = ({ tokenId, tokens }: SelectedCollectibleProps) => {
|
||||
const classes = useSelectedCollectibleStyles()
|
||||
const token = tokenId && tokens ? tokens.find(({ tokenId: id }) => tokenId === id) : null
|
||||
const shortener = textShortener({ charsStart: 40, charsEnd: 0 })
|
||||
|
||||
return (
|
||||
<MenuItem className={classes.container}>
|
||||
{token ? (
|
||||
<>
|
||||
<ListItemIcon className={classes.tokenImage}>
|
||||
<Img alt={token.description} height={28} onError={setImageToPlaceholder} src={token.image} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
className={classes.tokenData}
|
||||
primary={shortener(token.name)}
|
||||
secondary={`token ID: ${shortener(token.tokenId)}`}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph color="disabled" size="md" style={{ opacity: 0.5 }} weight="light">
|
||||
Select a token*
|
||||
</Paragraph>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectFieldProps = {
|
||||
initialValue: string,
|
||||
tokens: NFTTokensState,
|
||||
}
|
||||
|
||||
const useCollectibleSelectFieldStyles = makeStyles(selectStyles)
|
||||
|
||||
const CollectibleSelectField = ({ initialValue, tokens }: SelectFieldProps) => {
|
||||
const classes = useCollectibleSelectFieldStyles()
|
||||
|
||||
return (
|
||||
<Field
|
||||
className={classes.selectMenu}
|
||||
component={SelectField}
|
||||
disabled={!tokens.length}
|
||||
initialValue={initialValue}
|
||||
name="nftTokenId"
|
||||
renderValue={nftTokenId => <SelectedCollectible tokenId={nftTokenId} tokens={tokens} />}
|
||||
validate={required}
|
||||
>
|
||||
{tokens.map(token => (
|
||||
<MenuItem key={`${token.assetAddress}-${token.tokenId}`} value={token.tokenId}>
|
||||
<ListItemIcon className={classes.tokenImage}>
|
||||
<Img alt={token.name} height={28} onError={setImageToPlaceholder} src={token.image} />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={token.name} secondary={`token ID: ${token.tokenId}`} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
export default CollectibleSelectField
|
|
@ -0,0 +1,27 @@
|
|||
// @flow
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
export const selectedTokenStyles = () => ({
|
||||
container: {
|
||||
minHeight: '55px',
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
},
|
||||
tokenData: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
lineHeight: '14px',
|
||||
},
|
||||
tokenImage: {
|
||||
marginRight: sm,
|
||||
},
|
||||
})
|
||||
|
||||
export const selectStyles = () => ({
|
||||
selectMenu: {
|
||||
paddingRight: 0,
|
||||
},
|
||||
tokenImage: {
|
||||
marginRight: sm,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,94 @@
|
|||
// @flow
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon'
|
||||
import ListItemText from '@material-ui/core/ListItemText'
|
||||
import MenuItem from '@material-ui/core/MenuItem'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import React from 'react'
|
||||
|
||||
import { selectStyles, selectedTokenStyles } from './style'
|
||||
|
||||
import Field from '~/components/forms/Field'
|
||||
import SelectField from '~/components/forms/SelectField'
|
||||
import { required } from '~/components/forms/validator'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import type { NFTAssetsState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { formatAmount } from '~/logic/tokens/utils/formatAmount'
|
||||
import { setImageToPlaceholder } from '~/routes/safe/components/Balances/utils'
|
||||
import { textShortener } from '~/utils/strings'
|
||||
|
||||
type SelectedTokenProps = {
|
||||
assetAddress?: string,
|
||||
assets: NFTAssetsState,
|
||||
}
|
||||
|
||||
const useSelectedTokenStyles = makeStyles(selectedTokenStyles)
|
||||
|
||||
const SelectedToken = ({ assetAddress, assets }: SelectedTokenProps) => {
|
||||
const classes = useSelectedTokenStyles()
|
||||
const asset = assetAddress ? assets[assetAddress] : null
|
||||
const shortener = textShortener({ charsStart: 40, charsEnd: 0 })
|
||||
|
||||
return (
|
||||
<MenuItem className={classes.container}>
|
||||
{asset && asset.numberOfTokens ? (
|
||||
<>
|
||||
<ListItemIcon className={classes.tokenImage}>
|
||||
<Img alt={asset.name} height={28} onError={setImageToPlaceholder} src={asset.image} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
className={classes.tokenData}
|
||||
primary={shortener(asset.name)}
|
||||
secondary={`${formatAmount(asset.numberOfTokens)} ${asset.symbol}`}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Paragraph color="disabled" size="md" style={{ opacity: 0.5 }} weight="light">
|
||||
Select an asset*
|
||||
</Paragraph>
|
||||
)}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
|
||||
type SelectFieldProps = {
|
||||
assets: NFTAssetsState,
|
||||
initialValue: ?string,
|
||||
}
|
||||
|
||||
const useTokenSelectFieldStyles = makeStyles(selectStyles)
|
||||
|
||||
const TokenSelectField = ({ assets, initialValue }: SelectFieldProps) => {
|
||||
const classes = useTokenSelectFieldStyles()
|
||||
const assetsAddresses = Object.keys(assets)
|
||||
|
||||
return (
|
||||
<Field
|
||||
className={classes.selectMenu}
|
||||
component={SelectField}
|
||||
disabled={!assetsAddresses.length}
|
||||
initialValue={initialValue}
|
||||
name="assetAddress"
|
||||
renderValue={assetAddress => <SelectedToken assetAddress={assetAddress} assets={assets} />}
|
||||
validate={required}
|
||||
>
|
||||
{assetsAddresses.map(assetAddress => {
|
||||
const asset = assets[assetAddress]
|
||||
|
||||
return (
|
||||
<MenuItem key={asset.slug} value={assetAddress}>
|
||||
<ListItemIcon className={classes.tokenImage}>
|
||||
<Img alt={asset.name} height={28} onError={setImageToPlaceholder} src={asset.image} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={asset.name}
|
||||
secondary={`Count: ${formatAmount(asset.numberOfTokens)} ${asset.symbol}`}
|
||||
/>
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
export default TokenSelectField
|
|
@ -0,0 +1,27 @@
|
|||
// @flow
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
export const selectedTokenStyles = () => ({
|
||||
container: {
|
||||
minHeight: '55px',
|
||||
padding: 0,
|
||||
width: '100%',
|
||||
},
|
||||
tokenData: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
lineHeight: '14px',
|
||||
},
|
||||
tokenImage: {
|
||||
marginRight: sm,
|
||||
},
|
||||
})
|
||||
|
||||
export const selectStyles = () => ({
|
||||
selectMenu: {
|
||||
paddingRight: 0,
|
||||
},
|
||||
tokenImage: {
|
||||
marginRight: sm,
|
||||
},
|
||||
})
|
|
@ -0,0 +1,268 @@
|
|||
// @flow
|
||||
import IconButton from '@material-ui/core/IconButton'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import Close from '@material-ui/icons/Close'
|
||||
import React, { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import ArrowDown from '../assets/arrow-down.svg'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
import QRIcon from '~/assets/icons/qrcode.svg'
|
||||
import CopyBtn from '~/components/CopyBtn'
|
||||
import EtherscanBtn from '~/components/EtherscanBtn'
|
||||
import Identicon from '~/components/Identicon'
|
||||
import ScanQRModal from '~/components/ScanQRModal'
|
||||
import WhenFieldChanges from '~/components/WhenFieldChanges'
|
||||
import GnoForm from '~/components/forms/GnoForm'
|
||||
import Block from '~/components/layout/Block'
|
||||
import Button from '~/components/layout/Button'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Hairline from '~/components/layout/Hairline'
|
||||
import Img from '~/components/layout/Img'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { NFTAssetsState, NFTTokensState } from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import { nftAssetsSelector, nftTokensSelector } from '~/logic/collectibles/store/selectors'
|
||||
import type { NFTToken } from '~/routes/safe/components/Balances/Collectibles/types'
|
||||
import SafeInfo from '~/routes/safe/components/Balances/SendModal/SafeInfo'
|
||||
import AddressBookInput from '~/routes/safe/components/Balances/SendModal/screens/AddressBookInput'
|
||||
import CollectibleSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendCollectible/CollectibleSelectField'
|
||||
import TokenSelectField from '~/routes/safe/components/Balances/SendModal/screens/SendCollectible/TokenSelectField'
|
||||
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||
import { sm } from '~/theme/variables'
|
||||
|
||||
type Props = {
|
||||
initialValues: Object,
|
||||
onClose: () => void,
|
||||
onNext: any => void,
|
||||
recipientAddress?: string,
|
||||
selectedToken?: NFTToken | {},
|
||||
}
|
||||
|
||||
const formMutators = {
|
||||
setMax: (args, state, utils) => {
|
||||
utils.changeValue(state, 'amount', () => args[0])
|
||||
},
|
||||
onTokenChange: (args, state, utils) => {
|
||||
utils.changeValue(state, 'amount', () => '')
|
||||
},
|
||||
setRecipient: (args, state, utils) => {
|
||||
utils.changeValue(state, 'recipientAddress', () => args[0])
|
||||
},
|
||||
}
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SendCollectible = ({ initialValues, onClose, onNext, recipientAddress, selectedToken = {} }: Props) => {
|
||||
const classes = useStyles()
|
||||
const { address: safeAddress, ethBalance, name: safeName } = useSelector(safeSelector)
|
||||
const nftAssets: NFTAssetsState = useSelector(nftAssetsSelector)
|
||||
const nftTokens: NFTTokensState = useSelector(nftTokensSelector)
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
})
|
||||
const [pristine, setPristine] = useState<boolean>(true)
|
||||
const [isValidAddress, setIsValidAddress] = useState<boolean>(true)
|
||||
|
||||
React.useMemo(() => {
|
||||
if (selectedEntry === null && pristine) {
|
||||
setPristine(false)
|
||||
}
|
||||
}, [selectedEntry, pristine])
|
||||
|
||||
const handleSubmit = values => {
|
||||
// If the input wasn't modified, there was no mutation of the recipientAddress
|
||||
if (!values.recipientAddress) {
|
||||
values.recipientAddress = selectedEntry.address
|
||||
}
|
||||
|
||||
values.assetName = nftAssets[values.assetAddress].name
|
||||
|
||||
onNext(values)
|
||||
}
|
||||
|
||||
const openQrModal = () => {
|
||||
setQrModalOpen(true)
|
||||
}
|
||||
|
||||
const closeQrModal = () => {
|
||||
setQrModalOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.heading} grow>
|
||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||
Send Collectible
|
||||
</Paragraph>
|
||||
<Paragraph className={classes.annotation}>1 of 2</Paragraph>
|
||||
<IconButton disableRipple onClick={onClose}>
|
||||
<Close className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</Row>
|
||||
<Hairline />
|
||||
<GnoForm formMutators={formMutators} initialValues={initialValues} onSubmit={handleSubmit}>
|
||||
{(...args) => {
|
||||
const formState = args[2]
|
||||
const mutators = args[3]
|
||||
const { assetAddress } = formState.values
|
||||
const selectedNFTTokens = nftTokens.filter(nftToken => nftToken.assetAddress === assetAddress)
|
||||
|
||||
const handleScan = value => {
|
||||
let scannedAddress = value
|
||||
|
||||
if (scannedAddress.startsWith('ethereum:')) {
|
||||
scannedAddress = scannedAddress.replace('ethereum:', '')
|
||||
}
|
||||
|
||||
mutators.setRecipient(scannedAddress)
|
||||
closeQrModal()
|
||||
}
|
||||
|
||||
let shouldDisableSubmitButton = !isValidAddress
|
||||
|
||||
if (selectedEntry) {
|
||||
shouldDisableSubmitButton = !selectedEntry.address
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WhenFieldChanges field="assetAddress" set="nftTokenId" to={''} />
|
||||
<Block className={classes.formContainer}>
|
||||
<SafeInfo ethBalance={ethBalance} safeAddress={safeAddress} safeName={safeName} />
|
||||
<Row margin="md">
|
||||
<Col xs={1}>
|
||||
<img alt="Arrow Down" src={ArrowDown} style={{ marginLeft: sm }} />
|
||||
</Col>
|
||||
<Col center="xs" layout="column" xs={11}>
|
||||
<Hairline />
|
||||
</Col>
|
||||
</Row>
|
||||
{selectedEntry && selectedEntry.address ? (
|
||||
<div
|
||||
onKeyDown={e => {
|
||||
if (e.keyCode !== 9) {
|
||||
setSelectedEntry(null)
|
||||
}
|
||||
}}
|
||||
role="listbox"
|
||||
tabIndex="0"
|
||||
>
|
||||
<Row margin="xs">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Recipient
|
||||
</Paragraph>
|
||||
</Row>
|
||||
<Row align="center" margin="md">
|
||||
<Col xs={1}>
|
||||
<Identicon address={selectedEntry.address} diameter={32} />
|
||||
</Col>
|
||||
<Col layout="column" xs={11}>
|
||||
<Block justify="left">
|
||||
<Block>
|
||||
<Paragraph
|
||||
className={classes.selectAddress}
|
||||
noMargin
|
||||
onClick={() => setSelectedEntry(null)}
|
||||
weight="bolder"
|
||||
>
|
||||
{selectedEntry.name}
|
||||
</Paragraph>
|
||||
<Paragraph
|
||||
className={classes.selectAddress}
|
||||
noMargin
|
||||
onClick={() => setSelectedEntry(null)}
|
||||
weight="bolder"
|
||||
>
|
||||
{selectedEntry.address}
|
||||
</Paragraph>
|
||||
</Block>
|
||||
<CopyBtn content={selectedEntry.address} />
|
||||
<EtherscanBtn type="address" value={selectedEntry.address} />
|
||||
</Block>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Row margin="md">
|
||||
<Col xs={11}>
|
||||
<AddressBookInput
|
||||
fieldMutator={mutators.setRecipient}
|
||||
pristine={pristine}
|
||||
recipientAddress={recipientAddress}
|
||||
setIsValidAddress={setIsValidAddress}
|
||||
setSelectedEntry={setSelectedEntry}
|
||||
/>
|
||||
</Col>
|
||||
<Col center="xs" className={classes} middle="xs" xs={1}>
|
||||
<Img
|
||||
alt="Scan QR"
|
||||
className={classes.qrCodeBtn}
|
||||
height={20}
|
||||
onClick={() => {
|
||||
openQrModal()
|
||||
}}
|
||||
role="button"
|
||||
src={QRIcon}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
<Row margin="xs">
|
||||
<Col between="lg">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Collectible
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="sm">
|
||||
<Col>
|
||||
<TokenSelectField assets={nftAssets} initialValue={selectedToken.assetAddress} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="xs">
|
||||
<Col between="lg">
|
||||
<Paragraph color="disabled" noMargin size="md" style={{ letterSpacing: '-0.5px' }}>
|
||||
Token ID
|
||||
</Paragraph>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row margin="md">
|
||||
<Col>
|
||||
<CollectibleSelectField initialValue={selectedToken.tokenId} tokens={selectedNFTTokens} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Block>
|
||||
<Hairline />
|
||||
<Row align="center" className={classes.buttonRow}>
|
||||
<Button minWidth={140} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className={classes.submitButton}
|
||||
color="primary"
|
||||
data-testid="review-tx-btn"
|
||||
disabled={shouldDisableSubmitButton}
|
||||
minWidth={140}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Review
|
||||
</Button>
|
||||
</Row>
|
||||
{qrModalOpen && <ScanQRModal isOpen={qrModalOpen} onClose={closeQrModal} onScan={handleScan} />}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</GnoForm>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendCollectible
|
|
@ -0,0 +1,45 @@
|
|||
// @flow
|
||||
import { lg, md, secondaryText } from '~/theme/variables'
|
||||
|
||||
export const styles = () => ({
|
||||
heading: {
|
||||
padding: `${md} ${lg}`,
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
maxHeight: '75px',
|
||||
},
|
||||
annotation: {
|
||||
letterSpacing: '-1px',
|
||||
color: secondaryText,
|
||||
marginRight: 'auto',
|
||||
marginLeft: '20px',
|
||||
},
|
||||
manage: {
|
||||
fontSize: lg,
|
||||
},
|
||||
closeIcon: {
|
||||
height: '35px',
|
||||
width: '35px',
|
||||
},
|
||||
qrCodeBtn: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
formContainer: {
|
||||
padding: `${md} ${lg}`,
|
||||
},
|
||||
buttonRow: {
|
||||
height: '84px',
|
||||
justifyContent: 'center',
|
||||
'& > button': {
|
||||
fontFamily: 'Averta',
|
||||
fontSize: md,
|
||||
},
|
||||
},
|
||||
submitButton: {
|
||||
boxShadow: '1px 2px 10px 0 rgba(212, 212, 211, 0.59)',
|
||||
marginLeft: '15px',
|
||||
},
|
||||
selectAddress: {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
})
|
|
@ -34,6 +34,7 @@ import { sm } from '~/theme/variables'
|
|||
type Props = {
|
||||
onClose: () => void,
|
||||
classes: Object,
|
||||
recipientAddress: string,
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
ethBalance: string,
|
||||
|
@ -41,10 +42,19 @@ type Props = {
|
|||
initialValues: Object,
|
||||
}
|
||||
|
||||
const SendCustomTx = ({ classes, ethBalance, initialValues, onClose, onSubmit, safeAddress, safeName }: Props) => {
|
||||
const SendCustomTx = ({
|
||||
classes,
|
||||
ethBalance,
|
||||
initialValues,
|
||||
onClose,
|
||||
onSubmit,
|
||||
recipientAddress,
|
||||
safeAddress,
|
||||
safeName,
|
||||
}: Props) => {
|
||||
const [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
||||
address: '',
|
||||
address: recipientAddress || initialValues.recipientAddress,
|
||||
name: '',
|
||||
})
|
||||
const [pristine, setPristine] = useState<boolean>(true)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||
<defs>
|
||||
<path id="prefix__a" d="M0 0L23.984 0 23.984 22.002 0 22.002z"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d="M0 0H24V24H0z"/>
|
||||
<g transform="translate(0 1)">
|
||||
<mask id="prefix__b" fill="#fff">
|
||||
<use xlink:href="#prefix__a"/>
|
||||
</mask>
|
||||
<path fill="#FFF" d="M11.992 19.384L3.05 8.003h17.884l-8.942 11.38zM5.999 2h12.4l2.709 4.003H2.993L5.999 2zm17.974 5.16c.007-.042.009-.085.01-.128.002-.047.002-.093-.004-.141-.005-.043-.013-.084-.024-.127-.011-.046-.025-.092-.044-.137-.008-.02-.01-.042-.02-.061-.01-.022-.027-.038-.04-.06-.011-.02-.017-.043-.031-.063L19.759.44c-.186-.275-.496-.44-.83-.44H5.5c-.315 0-.611.149-.8.4L.192 6.403c-.02.026-.03.056-.047.085-.016.027-.04.048-.052.078-.006.013-.008.028-.014.042-.02.045-.031.09-.045.138-.011.042-.023.084-.028.127-.007.047-.005.094-.005.142 0 .044 0 .088.006.13.007.048.02.092.035.137.012.044.023.087.043.128.005.014.006.028.012.04.017.033.04.058.06.088.018.028.028.058.049.083l11 13.999c.189.242.479.382.786.382.307 0 .597-.14.786-.382l11-14c.019-.022.027-.05.044-.075.017-.025.038-.046.053-.073.009-.016.01-.033.018-.05.019-.041.034-.083.046-.126.015-.045.027-.09.034-.136z" mask="url(#prefix__b)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
|
@ -11,7 +11,6 @@ import * as React from 'react'
|
|||
|
||||
import AssetTableCell from './AssetTableCell'
|
||||
import Receive from './Receive'
|
||||
import SendModal from './SendModal'
|
||||
import Tokens from './Tokens'
|
||||
import { BALANCE_TABLE_ASSET_ID, type BalanceRow, generateColumns, getBalanceData } from './dataFetcher'
|
||||
import { styles } from './style'
|
||||
|
@ -22,36 +21,47 @@ import { type Column, cellWidth } from '~/components/Table/TableHead'
|
|||
import Button from '~/components/layout/Button'
|
||||
import ButtonLink from '~/components/layout/ButtonLink'
|
||||
import Col from '~/components/layout/Col'
|
||||
import Divider from '~/components/layout/Divider'
|
||||
import Link from '~/components/layout/Link'
|
||||
import Row from '~/components/layout/Row'
|
||||
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
|
||||
import { type Token } from '~/logic/tokens/store/model/token'
|
||||
import { SAFELIST_ADDRESS } from '~/routes/routes'
|
||||
import Collectibles from '~/routes/safe/components/Balances/Collectibles'
|
||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||
import { BALANCE_TABLE_BALANCE_ID, BALANCE_TABLE_VALUE_ID } from '~/routes/safe/components/Balances/dataFetcher'
|
||||
import DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
|
||||
import { history } from '~/store'
|
||||
|
||||
export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn'
|
||||
export const BALANCE_ROW_TEST_ID = 'balance-row'
|
||||
|
||||
type State = {
|
||||
showToken: boolean,
|
||||
showReceive: boolean,
|
||||
erc721Enabled: boolean,
|
||||
subMenuOptions: string[],
|
||||
sendFunds: Object,
|
||||
showCoins: boolean,
|
||||
showCollectibles: boolean,
|
||||
showReceive: boolean,
|
||||
showToken: boolean,
|
||||
}
|
||||
|
||||
type Props = {
|
||||
classes: Object,
|
||||
granted: boolean,
|
||||
tokens: List<Token>,
|
||||
activateTokensByBalance: Function,
|
||||
activeTokens: List<Token>,
|
||||
blacklistedTokens: List<Token>,
|
||||
activateTokensByBalance: Function,
|
||||
fetchTokens: Function,
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
ethBalance: string,
|
||||
classes: Object,
|
||||
createTransaction: Function,
|
||||
currencySelected: string,
|
||||
fetchCurrencyValues: Function,
|
||||
currencyValues: BalanceCurrencyType[],
|
||||
ethBalance: string,
|
||||
featuresEnabled: string[],
|
||||
fetchCurrencyValues: Function,
|
||||
fetchTokens: Function,
|
||||
granted: boolean,
|
||||
safeAddress: string,
|
||||
safeName: string,
|
||||
tokens: List<Token>,
|
||||
}
|
||||
|
||||
type Action = 'Token' | 'Send' | 'Receive'
|
||||
|
@ -60,20 +70,58 @@ class Balances extends React.Component<Props, State> {
|
|||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
erc721Enabled: false,
|
||||
subMenuOptions: [],
|
||||
showToken: false,
|
||||
sendFunds: {
|
||||
isOpen: false,
|
||||
selectedToken: undefined,
|
||||
},
|
||||
showCoins: true,
|
||||
showCollectibles: false,
|
||||
showReceive: false,
|
||||
}
|
||||
props.fetchTokens()
|
||||
}
|
||||
|
||||
static isCoinsLocation = /\/balances\/?$/
|
||||
static isCollectiblesLocation = /\/balances\/collectibles$/
|
||||
|
||||
componentDidMount(): void {
|
||||
const { activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
||||
fetchCurrencyValues(safeAddress)
|
||||
activateTokensByBalance(safeAddress)
|
||||
|
||||
const showCollectibles = Balances.isCollectiblesLocation.test(history.location.pathname)
|
||||
const showCoins = Balances.isCoinsLocation.test(history.location.pathname)
|
||||
|
||||
if (!showCollectibles && !showCoins) {
|
||||
history.replace(`${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances`)
|
||||
}
|
||||
|
||||
const subMenuOptions = [
|
||||
{ enabled: showCoins, legend: 'Coins', url: `${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances` },
|
||||
]
|
||||
const erc721Enabled = this.props.featuresEnabled.includes('ERC721')
|
||||
|
||||
if (erc721Enabled) {
|
||||
subMenuOptions.push({
|
||||
enabled: showCollectibles,
|
||||
legend: 'Collectibles',
|
||||
url: `${SAFELIST_ADDRESS}/${this.props.safeAddress}/balances/collectibles`,
|
||||
})
|
||||
} else {
|
||||
if (showCollectibles) {
|
||||
history.replace(subMenuOptions[0].url)
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showCoins,
|
||||
showCollectibles,
|
||||
erc721Enabled,
|
||||
subMenuOptions,
|
||||
})
|
||||
}
|
||||
|
||||
onShow = (action: Action) => () => {
|
||||
|
@ -103,7 +151,7 @@ class Balances extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { sendFunds, showReceive, showToken } = this.state
|
||||
const { erc721Enabled, sendFunds, showCoins, showCollectibles, showReceive, showToken, subMenuOptions } = this.state
|
||||
const {
|
||||
activeTokens,
|
||||
blacklistedTokens,
|
||||
|
@ -125,106 +173,135 @@ class Balances extends React.Component<Props, State> {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Row align="center" className={classes.message}>
|
||||
<Col end="sm" xs={12}>
|
||||
<DropdownCurrency />
|
||||
<ButtonLink onClick={this.onShow('Token')} size="lg" testId="manage-tokens-btn">
|
||||
Manage List
|
||||
</ButtonLink>
|
||||
<Modal
|
||||
description="Enable and disable tokens to be listed"
|
||||
handleClose={this.onHide('Token')}
|
||||
open={showToken}
|
||||
title="Manage List"
|
||||
>
|
||||
<Tokens
|
||||
activeTokens={activeTokens}
|
||||
blacklistedTokens={blacklistedTokens}
|
||||
onClose={this.onHide('Token')}
|
||||
safeAddress={safeAddress}
|
||||
tokens={tokens}
|
||||
/>
|
||||
</Modal>
|
||||
<Row align="center" className={classes.controls}>
|
||||
<Col className={classes.assetTabs} sm={6} start="sm" xs={12}>
|
||||
{subMenuOptions.length > 1 &&
|
||||
subMenuOptions.map(({ enabled, legend, url }, index) => (
|
||||
<React.Fragment key={`legend-${index}`}>
|
||||
{index > 0 && <Divider className={classes.assetDivider} />}
|
||||
<Link
|
||||
className={enabled ? classes.assetTabActive : classes.assetTab}
|
||||
data-testid={`${legend.toLowerCase()}'-assets-btn'`}
|
||||
size="md"
|
||||
to={url}
|
||||
weight={enabled ? 'bold' : 'regular'}
|
||||
>
|
||||
{legend}
|
||||
</Link>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Col>
|
||||
<Col className={classes.tokenControls} end="sm" sm={6} xs={12}>
|
||||
{showCoins && (
|
||||
<>
|
||||
<DropdownCurrency />
|
||||
<ButtonLink
|
||||
className={classes.manageTokensButton}
|
||||
onClick={this.onShow('Token')}
|
||||
size="lg"
|
||||
testId="manage-tokens-btn"
|
||||
>
|
||||
Manage List
|
||||
</ButtonLink>
|
||||
<Modal
|
||||
description="Enable and disable tokens to be listed"
|
||||
handleClose={this.onHide('Token')}
|
||||
open={showToken}
|
||||
title="Manage List"
|
||||
>
|
||||
<Tokens
|
||||
activeTokens={activeTokens}
|
||||
blacklistedTokens={blacklistedTokens}
|
||||
onClose={this.onHide('Token')}
|
||||
safeAddress={safeAddress}
|
||||
tokens={tokens}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
defaultFixed
|
||||
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
|
||||
defaultRowsPerPage={10}
|
||||
label="Balances"
|
||||
size={filteredData.size}
|
||||
>
|
||||
{(sortedData: Array<BalanceRow>) =>
|
||||
sortedData.map((row: any, index: number) => (
|
||||
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
|
||||
{autoColumns.map((column: Column) => {
|
||||
const { align, id, width } = column
|
||||
let cellItem
|
||||
switch (id) {
|
||||
case BALANCE_TABLE_ASSET_ID: {
|
||||
cellItem = <AssetTableCell asset={row[id]} />
|
||||
break
|
||||
{showCoins && (
|
||||
<TableContainer>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={filteredData}
|
||||
defaultFixed
|
||||
defaultOrderBy={BALANCE_TABLE_ASSET_ID}
|
||||
defaultRowsPerPage={10}
|
||||
label="Balances"
|
||||
size={filteredData.size}
|
||||
>
|
||||
{(sortedData: Array<BalanceRow>) =>
|
||||
sortedData.map((row: any, index: number) => (
|
||||
<TableRow className={classes.hide} data-testid={BALANCE_ROW_TEST_ID} key={index} tabIndex={-1}>
|
||||
{autoColumns.map((column: Column) => {
|
||||
const { align, id, width } = column
|
||||
let cellItem
|
||||
switch (id) {
|
||||
case BALANCE_TABLE_ASSET_ID: {
|
||||
cellItem = <AssetTableCell asset={row[id]} />
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_BALANCE_ID: {
|
||||
cellItem = <div>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_VALUE_ID: {
|
||||
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
default: {
|
||||
cellItem = null
|
||||
break
|
||||
}
|
||||
}
|
||||
case BALANCE_TABLE_BALANCE_ID: {
|
||||
cellItem = <div>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
case BALANCE_TABLE_VALUE_ID: {
|
||||
cellItem = <div className={classes.currencyValueRow}>{row[id]}</div>
|
||||
break
|
||||
}
|
||||
default: {
|
||||
cellItem = null
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<TableCell align={align} component="td" key={id} style={cellWidth(width)}>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{granted && (
|
||||
return (
|
||||
<TableCell align={align} component="td" key={id} style={cellWidth(width)}>
|
||||
{cellItem}
|
||||
</TableCell>
|
||||
)
|
||||
})}
|
||||
<TableCell component="td">
|
||||
<Row align="end" className={classes.actions}>
|
||||
{granted && (
|
||||
<Button
|
||||
className={classes.send}
|
||||
color="primary"
|
||||
onClick={() => this.showSendFunds(row.asset.address)}
|
||||
size="small"
|
||||
testId="balance-send-btn"
|
||||
variant="contained"
|
||||
>
|
||||
<CallMade
|
||||
alt="Send Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/>
|
||||
Send
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={classes.send}
|
||||
className={classes.receive}
|
||||
color="primary"
|
||||
onClick={() => this.showSendFunds(row.asset.address)}
|
||||
onClick={this.onShow('Receive')}
|
||||
size="small"
|
||||
testId="balance-send-btn"
|
||||
variant="contained"
|
||||
>
|
||||
<CallMade
|
||||
alt="Send Transaction"
|
||||
<CallReceived
|
||||
alt="Receive Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/>
|
||||
Send
|
||||
Receive
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
className={classes.receive}
|
||||
color="primary"
|
||||
onClick={this.onShow('Receive')}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
<CallReceived
|
||||
alt="Receive Transaction"
|
||||
className={classNames(classes.leftIcon, classes.iconSmall)}
|
||||
/>
|
||||
Receive
|
||||
</Button>
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Row>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
}
|
||||
</Table>
|
||||
</TableContainer>
|
||||
)}
|
||||
{erc721Enabled && showCollectibles && <Collectibles />}
|
||||
<SendModal
|
||||
activeScreenType="sendFunds"
|
||||
createTransaction={createTransaction}
|
||||
|
|
|
@ -1,17 +1,66 @@
|
|||
// @flow
|
||||
import { md, sm, xs } from '~/theme/variables'
|
||||
import { md, screenSm, secondary, sm, xs } from '~/theme/variables'
|
||||
|
||||
export const styles = (theme: Object) => ({
|
||||
root: {
|
||||
width: '20px',
|
||||
marginRight: sm,
|
||||
width: '20px',
|
||||
},
|
||||
message: {
|
||||
margin: `${sm} 0`,
|
||||
padding: `${md} 0`,
|
||||
maxHeight: '54px',
|
||||
controls: {
|
||||
alignItems: 'center',
|
||||
boxSizing: 'border-box',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
padding: `${md} 0`,
|
||||
},
|
||||
assetTabs: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
order: '2',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
order: '1',
|
||||
},
|
||||
},
|
||||
assetDivider: {
|
||||
borderRightColor: `${secondary} !important`,
|
||||
height: '18px !important',
|
||||
},
|
||||
assetTab: {
|
||||
color: '#686868',
|
||||
margin: '2px 0',
|
||||
padding: '0 10px',
|
||||
textDecoration: 'underline',
|
||||
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
assetTabActive: {
|
||||
color: secondary,
|
||||
fontWeight: 'bold',
|
||||
margin: '2px 0',
|
||||
padding: '0 10px',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
tokenControls: {
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
order: '1',
|
||||
padding: '0 0 10px',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
justifyContent: 'flex-end',
|
||||
order: '2',
|
||||
padding: '0',
|
||||
},
|
||||
},
|
||||
manageTokensButton: {
|
||||
marginLeft: 'auto',
|
||||
|
||||
[`@media (min-width: ${screenSm}px)`]: {
|
||||
marginLeft: '0',
|
||||
},
|
||||
},
|
||||
actionIcon: {
|
||||
marginRight: theme.spacing(1),
|
||||
|
|
|
@ -13,7 +13,6 @@ import { type Actions } from '../container/actions'
|
|||
|
||||
import Balances from './Balances'
|
||||
import Receive from './Balances/Receive'
|
||||
import SendModal from './Balances/SendModal'
|
||||
import Settings from './Settings'
|
||||
import Transactions from './Transactions'
|
||||
import { AddressBookIcon } from './assets/AddressBookIcon'
|
||||
|
@ -35,9 +34,9 @@ import Hairline from '~/components/layout/Hairline'
|
|||
import Heading from '~/components/layout/Heading'
|
||||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import AddressBookTable from '~/routes/safe/components/AddressBook'
|
||||
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||
import { type SelectorProps } from '~/routes/safe/container/selector'
|
||||
import { border } from '~/theme/variables'
|
||||
|
||||
|
@ -107,23 +106,6 @@ const Layout = (props: Props) => {
|
|||
onClose: null,
|
||||
})
|
||||
|
||||
const [needUpdate, setNeedUpdate] = useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkUpdateRequirement = async () => {
|
||||
let safeVersion = {}
|
||||
|
||||
try {
|
||||
safeVersion = await getSafeVersion(safe.address)
|
||||
} catch (e) {
|
||||
console.error('failed to check version', e)
|
||||
}
|
||||
setNeedUpdate(safeVersion.needUpdate)
|
||||
}
|
||||
|
||||
checkUpdateRequirement()
|
||||
}, [safe && safe.address])
|
||||
|
||||
const handleCallToRouter = (_, value) => {
|
||||
const { history } = props
|
||||
|
||||
|
@ -134,7 +116,7 @@ const Layout = (props: Props) => {
|
|||
return <NoSafe provider={provider} text="Safe not found" />
|
||||
}
|
||||
|
||||
const { address, ethBalance, name } = safe
|
||||
const { address, ethBalance, featuresEnabled, name } = safe
|
||||
const etherScanLink = getEtherScanLink('address', address)
|
||||
const web3Instance = getWeb3()
|
||||
|
||||
|
@ -176,7 +158,7 @@ const Layout = (props: Props) => {
|
|||
<Badge
|
||||
badgeContent=""
|
||||
color="error"
|
||||
invisible={!needUpdate || !granted}
|
||||
invisible={!safe.needsUpdate || !granted}
|
||||
style={{ paddingRight: '10px' }}
|
||||
variant="dot"
|
||||
>
|
||||
|
@ -187,7 +169,7 @@ const Layout = (props: Props) => {
|
|||
const labelBalances = (
|
||||
<>
|
||||
<BalancesIcon />
|
||||
Balances
|
||||
Assets
|
||||
</>
|
||||
)
|
||||
const labelTransactions = (
|
||||
|
@ -212,6 +194,18 @@ const Layout = (props: Props) => {
|
|||
</React.Suspense>
|
||||
)
|
||||
|
||||
const tabsValue = () => {
|
||||
const balanceLocation = `${match.url}/balances`
|
||||
const isInBalance = new RegExp(`^${balanceLocation}.*$`)
|
||||
const { pathname } = location
|
||||
|
||||
if (isInBalance.test(pathname)) {
|
||||
return balanceLocation
|
||||
}
|
||||
|
||||
return pathname
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Block className={classes.container} margin="xl">
|
||||
|
@ -261,7 +255,7 @@ const Layout = (props: Props) => {
|
|||
indicatorColor="secondary"
|
||||
onChange={handleCallToRouter}
|
||||
textColor="secondary"
|
||||
value={location.pathname}
|
||||
value={tabsValue()}
|
||||
variant="scrollable"
|
||||
>
|
||||
<Tab
|
||||
|
@ -316,7 +310,7 @@ const Layout = (props: Props) => {
|
|||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={`${match.path}/balances`}
|
||||
path={`${match.path}/balances/:assetType?`}
|
||||
render={() => (
|
||||
<Balances
|
||||
activateTokensByBalance={activateTokensByBalance}
|
||||
|
@ -326,6 +320,7 @@ const Layout = (props: Props) => {
|
|||
currencySelected={currencySelected}
|
||||
currencyValues={currencyValues}
|
||||
ethBalance={ethBalance}
|
||||
featuresEnabled={featuresEnabled}
|
||||
fetchCurrencyValues={fetchCurrencyValues}
|
||||
fetchTokens={fetchTokens}
|
||||
granted={granted}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import { withSnackbar } from 'notistack'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { styles } from './style'
|
||||
|
@ -19,9 +19,9 @@ import Paragraph from '~/components/layout/Paragraph'
|
|||
import Row from '~/components/layout/Row'
|
||||
import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
||||
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal'
|
||||
import { grantedSelector } from '~/routes/safe/container/selector'
|
||||
import { latestMasterContractVersionSelector } from '~/routes/safe/store/selectors'
|
||||
|
||||
export const SAFE_NAME_INPUT_TEST_ID = 'safe-name-input'
|
||||
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
||||
|
@ -29,7 +29,9 @@ export const SAFE_NAME_UPDATE_SAFE_BTN_TEST_ID = 'update-safe-name-btn'
|
|||
|
||||
type Props = {
|
||||
safeAddress: string,
|
||||
safeCurrentVersion: string,
|
||||
safeName: string,
|
||||
safeNeedsUpdate: boolean,
|
||||
updateSafe: Function,
|
||||
enqueueSnackbar: Function,
|
||||
createTransaction: Function,
|
||||
|
@ -40,11 +42,19 @@ const useStyles = makeStyles(styles)
|
|||
|
||||
const SafeDetails = (props: Props) => {
|
||||
const classes = useStyles()
|
||||
const [safeVersions, setSafeVersions] = React.useState({ current: null, latest: null, needUpdate: false })
|
||||
const isUserOwner = useSelector(grantedSelector)
|
||||
const { closeSnackbar, createTransaction, enqueueSnackbar, safeAddress, safeName, updateSafe } = props
|
||||
const latestMasterContractVersion = useSelector(latestMasterContractVersionSelector)
|
||||
const {
|
||||
closeSnackbar,
|
||||
enqueueSnackbar,
|
||||
safeAddress,
|
||||
safeCurrentVersion,
|
||||
safeName,
|
||||
safeNeedsUpdate,
|
||||
updateSafe,
|
||||
} = props
|
||||
|
||||
const [isModalOpen, setModalOpen] = useState(false)
|
||||
const [isModalOpen, setModalOpen] = React.useState(false)
|
||||
|
||||
const toggleModal = () => {
|
||||
setModalOpen(prevOpen => !prevOpen)
|
||||
|
@ -61,19 +71,6 @@ const SafeDetails = (props: Props) => {
|
|||
setModalOpen(true)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const getVersion = async () => {
|
||||
try {
|
||||
const { current, latest, needUpdate } = await getSafeVersion(safeAddress)
|
||||
setSafeVersions({ current, latest, needUpdate })
|
||||
} catch (err) {
|
||||
setSafeVersions({ current: 'Version not defined' })
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
getVersion()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<GnoForm onSubmit={handleSubmit}>
|
||||
|
@ -83,11 +80,11 @@ const SafeDetails = (props: Props) => {
|
|||
<Heading tag="h2">Safe Version</Heading>
|
||||
<Row align="end" grow>
|
||||
<Paragraph className={classes.versionNumber}>
|
||||
{safeVersions.current}
|
||||
{safeVersions.needUpdate && ` (there's a newer version: ${safeVersions.latest})`}
|
||||
{safeCurrentVersion}
|
||||
{safeNeedsUpdate && ` (there's a newer version: ${latestMasterContractVersion})`}
|
||||
</Paragraph>
|
||||
</Row>
|
||||
{safeVersions.needUpdate && isUserOwner ? (
|
||||
{safeNeedsUpdate && isUserOwner ? (
|
||||
<Row align="end" grow>
|
||||
<Paragraph>
|
||||
<Button
|
||||
|
@ -138,7 +135,7 @@ const SafeDetails = (props: Props) => {
|
|||
</Col>
|
||||
</Row>
|
||||
<Modal description="Update Safe" handleClose={toggleModal} open={isModalOpen} title="Update Safe">
|
||||
<UpdateSafeModal createTransaction={createTransaction} onClose={toggleModal} safeAddress={safeAddress} />
|
||||
<UpdateSafeModal onClose={toggleModal} safeAddress={safeAddress} />
|
||||
</Modal>
|
||||
</>
|
||||
)}
|
||||
|
|
|
@ -3,6 +3,8 @@ import IconButton from '@material-ui/core/IconButton'
|
|||
import Close from '@material-ui/icons/Close'
|
||||
import { withStyles } from '@material-ui/styles'
|
||||
import React from 'react'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import { styles } from './style'
|
||||
|
||||
|
@ -13,18 +15,19 @@ import Hairline from '~/components/layout/Hairline'
|
|||
import Paragraph from '~/components/layout/Paragraph'
|
||||
import Row from '~/components/layout/Row'
|
||||
import { upgradeSafeToLatestVersion } from '~/logic/safe/utils/upgradeSafe'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
|
||||
type Props = {
|
||||
onClose: Function,
|
||||
createTransaction: Function,
|
||||
classes: Object,
|
||||
safeAddress: string,
|
||||
}
|
||||
|
||||
const UpdateSafeModal = ({ classes, createTransaction, onClose, safeAddress }: Props) => {
|
||||
const UpdateSafeModal = ({ classes, onClose, safeAddress }: Props) => {
|
||||
const dispatch = useDispatch()
|
||||
const handleSubmit = async () => {
|
||||
// Call the update safe method
|
||||
await upgradeSafeToLatestVersion(safeAddress, createTransaction)
|
||||
await upgradeSafeToLatestVersion(safeAddress, bindActionCreators(createTransaction, dispatch))
|
||||
onClose()
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import Paragraph from '~/components/layout/Paragraph'
|
|||
import Row from '~/components/layout/Row'
|
||||
import Span from '~/components/layout/Span'
|
||||
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import { type Owner } from '~/routes/safe/store/models/owner'
|
||||
import type { Safe } from '~/routes/safe/store/models/safe'
|
||||
|
||||
|
@ -35,7 +34,6 @@ export const OWNERS_SETTINGS_TAB_TEST_ID = 'owner-settings-tab'
|
|||
type State = {
|
||||
showRemoveSafe: boolean,
|
||||
menuOptionIndex: number,
|
||||
needUpdate: boolean,
|
||||
}
|
||||
|
||||
type Props = Actions & {
|
||||
|
@ -68,25 +66,9 @@ class Settings extends React.Component<Props, State> {
|
|||
this.state = {
|
||||
showRemoveSafe: false,
|
||||
menuOptionIndex: 1,
|
||||
needUpdate: false,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const checkUpdateRequirement = async () => {
|
||||
let safeVersion = {}
|
||||
|
||||
try {
|
||||
safeVersion = await getSafeVersion(this.props.safe.address)
|
||||
} catch (e) {
|
||||
console.error('failed to check version', e)
|
||||
}
|
||||
this.setState({ needUpdate: safeVersion.needUpdate })
|
||||
}
|
||||
|
||||
checkUpdateRequirement()
|
||||
}
|
||||
|
||||
handleChange = menuOptionIndex => () => {
|
||||
this.setState({ menuOptionIndex })
|
||||
}
|
||||
|
@ -148,7 +130,7 @@ class Settings extends React.Component<Props, State> {
|
|||
<Badge
|
||||
badgeContent=" "
|
||||
color="error"
|
||||
invisible={!this.state.needUpdate || !granted}
|
||||
invisible={!safe.needsUpdate || !granted}
|
||||
style={{ paddingRight: '10px' }}
|
||||
variant="dot"
|
||||
>
|
||||
|
@ -184,7 +166,9 @@ class Settings extends React.Component<Props, State> {
|
|||
<SafeDetails
|
||||
createTransaction={createTransaction}
|
||||
safeAddress={safeAddress}
|
||||
safeCurrentVersion={safe.currentVersion}
|
||||
safeName={safeName}
|
||||
safeNeedsUpdate={safe.needsUpdate}
|
||||
updateSafe={updateSafe}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -21,6 +21,7 @@ type Props = {
|
|||
const statusToIcon = {
|
||||
success: OkIcon,
|
||||
cancelled: ErrorIcon,
|
||||
failed: ErrorIcon,
|
||||
awaiting_your_confirmation: AwaitingIcon,
|
||||
awaiting_confirmations: AwaitingIcon,
|
||||
awaiting_execution: AwaitingIcon,
|
||||
|
@ -30,6 +31,7 @@ const statusToIcon = {
|
|||
const statusToLabel = {
|
||||
success: 'Success',
|
||||
cancelled: 'Cancelled',
|
||||
failed: 'Failed',
|
||||
awaiting_your_confirmation: 'Awaiting your confirmation',
|
||||
awaiting_confirmations: 'Awaiting confirmations',
|
||||
awaiting_execution: 'Awaiting execution',
|
||||
|
|
|
@ -23,6 +23,11 @@ export const styles = () => ({
|
|||
color: error,
|
||||
border: `1px solid ${error}`,
|
||||
},
|
||||
failed: {
|
||||
backgroundColor: 'transparent',
|
||||
color: error,
|
||||
border: `1px solid ${error}`,
|
||||
},
|
||||
awaiting_your_confirmation: {
|
||||
backgroundColor: '#d4d5d3',
|
||||
color: disabled,
|
||||
|
|
|
@ -125,7 +125,7 @@ const TxsTable = ({
|
|||
{autoColumns.map((column: Column) => (
|
||||
<TableCell
|
||||
align={column.align}
|
||||
className={cn(classes.cell, row.status === 'cancelled' && classes.cancelledRow)}
|
||||
className={cn(classes.cell, ['cancelled', 'failed'].includes(row.status) && classes.cancelledRow)}
|
||||
component="td"
|
||||
key={column.id}
|
||||
style={cellWidth(column.width)}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// @flow
|
||||
import loadAddressBookFromStorage from '~/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||
import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
|
||||
import fetchCollectibles from '~/logic/collectibles/store/actions/fetchCollectibles'
|
||||
import fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||
import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe'
|
||||
import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
|
||||
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||
import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance'
|
||||
import fetchLatestMasterContractVersion from '~/routes/safe/store/actions/fetchLatestMasterContractVersion'
|
||||
import fetchSafe, { checkAndUpdateSafe } from '~/routes/safe/store/actions/fetchSafe'
|
||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||
|
@ -19,9 +21,11 @@ export type Actions = {
|
|||
createTransaction: typeof createTransaction,
|
||||
fetchTransactions: typeof fetchTransactions,
|
||||
updateSafe: typeof updateSafe,
|
||||
fetchCollectibles: typeof fetchCollectibles,
|
||||
fetchTokens: typeof fetchTokens,
|
||||
processTransaction: typeof processTransaction,
|
||||
fetchEtherBalance: typeof fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
||||
activateTokensByBalance: typeof activateTokensByBalance,
|
||||
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
|
||||
fetchCurrencyValues: typeof fetchCurrencyValues,
|
||||
|
@ -35,11 +39,13 @@ export default {
|
|||
fetchTokenBalances,
|
||||
createTransaction,
|
||||
processTransaction,
|
||||
fetchCollectibles,
|
||||
fetchTokens,
|
||||
fetchTransactions,
|
||||
activateTokensByBalance,
|
||||
updateSafe,
|
||||
fetchEtherBalance,
|
||||
fetchLatestMasterContractVersion,
|
||||
fetchCurrencyValues,
|
||||
checkAndUpdateSafeOwners: checkAndUpdateSafe,
|
||||
loadAddressBook: loadAddressBookFromStorage,
|
||||
|
|
|
@ -34,11 +34,15 @@ class SafeView extends React.Component<Props, State> {
|
|||
|
||||
intervalId: IntervalID
|
||||
|
||||
longIntervalId: IntervalID
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
activeTokens,
|
||||
addViewedSafe,
|
||||
fetchCollectibles,
|
||||
fetchCurrencyValues,
|
||||
fetchLatestMasterContractVersion,
|
||||
fetchSafe,
|
||||
fetchTokenBalances,
|
||||
fetchTokens,
|
||||
|
@ -47,11 +51,14 @@ class SafeView extends React.Component<Props, State> {
|
|||
safeUrl,
|
||||
} = this.props
|
||||
|
||||
fetchSafe(safeUrl).then(() => {
|
||||
// The safe needs to be loaded before fetching the transactions
|
||||
fetchTransactions(safeUrl)
|
||||
addViewedSafe(safeUrl)
|
||||
})
|
||||
fetchLatestMasterContractVersion()
|
||||
.then(() => fetchSafe(safeUrl))
|
||||
.then(() => {
|
||||
// The safe needs to be loaded before fetching the transactions
|
||||
fetchTransactions(safeUrl)
|
||||
addViewedSafe(safeUrl)
|
||||
fetchCollectibles()
|
||||
})
|
||||
fetchTokenBalances(safeUrl, activeTokens)
|
||||
// fetch tokens there to get symbols for tokens in TXs list
|
||||
fetchTokens()
|
||||
|
@ -61,6 +68,10 @@ class SafeView extends React.Component<Props, State> {
|
|||
this.intervalId = setInterval(() => {
|
||||
this.checkForUpdates()
|
||||
}, TIMEOUT)
|
||||
|
||||
this.longIntervalId = setInterval(() => {
|
||||
fetchCollectibles()
|
||||
}, TIMEOUT * 3)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -78,6 +89,7 @@ class SafeView extends React.Component<Props, State> {
|
|||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.intervalId)
|
||||
clearInterval(this.longIntervalId)
|
||||
}
|
||||
|
||||
onShow = (action: Action) => () => {
|
||||
|
|
|
@ -63,6 +63,10 @@ const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): Transact
|
|||
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
|
||||
}
|
||||
|
||||
if (tx.isSuccessful === false) {
|
||||
txStatus = 'failed'
|
||||
}
|
||||
|
||||
return txStatus
|
||||
}
|
||||
|
||||
|
@ -98,12 +102,12 @@ export const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List
|
|||
map.set(tokenAddress, baseToken.set('balance', tokenBalance || '0'))
|
||||
}
|
||||
})
|
||||
|
||||
if (ethAsToken) {
|
||||
map.set(ethAsToken.address, ethAsToken)
|
||||
}
|
||||
})
|
||||
|
||||
if (ethAsToken) {
|
||||
return extendedTokens.set(ethAsToken.address, ethAsToken).toList()
|
||||
}
|
||||
|
||||
return extendedTokens.toList()
|
||||
},
|
||||
)
|
||||
|
|
|
@ -28,7 +28,7 @@ type CreateTransactionArgs = {
|
|||
to: string,
|
||||
valueInWei: string,
|
||||
txData: string,
|
||||
notifiedTransaction: NotifiedTransaction,
|
||||
notifiedTransaction: $Values<NotifiedTransaction>,
|
||||
enqueueSnackbar: Function,
|
||||
closeSnackbar: Function,
|
||||
txNonce?: number,
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
// @flow
|
||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||
|
||||
import { getCurrentMasterContractLastVersion } from '~/logic/safe/utils/safeVersion'
|
||||
import setLatestMasterContractVersion from '~/routes/safe/store/actions/setLatestMasterContractVersion'
|
||||
import { type GlobalState } from '~/store'
|
||||
|
||||
const fetchLatestMasterContractVersion = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
const latestVersion = await getCurrentMasterContractLastVersion()
|
||||
|
||||
dispatch(setLatestMasterContractVersion(latestVersion))
|
||||
}
|
||||
|
||||
export default fetchLatestMasterContractVersion
|
|
@ -4,6 +4,7 @@ import type { Dispatch as ReduxDispatch } from 'redux'
|
|||
|
||||
import { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||
import { getLocalSafe, getSafeName } from '~/logic/safe/utils'
|
||||
import { enabledFeatures, safeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
|
||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||
import { getBalanceInEtherOf, getWeb3 } from '~/logic/wallets/getWeb3'
|
||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
||||
|
@ -34,7 +35,7 @@ const buildOwnersFrom = (
|
|||
})
|
||||
})
|
||||
|
||||
export const buildSafe = async (safeAdd: string, safeName: string) => {
|
||||
export const buildSafe = async (safeAdd: string, safeName: string, latestMasterContractVersion: string) => {
|
||||
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||
const ethBalance = await getBalanceInEtherOf(safeAddress)
|
||||
|
@ -42,6 +43,9 @@ export const buildSafe = async (safeAdd: string, safeName: string) => {
|
|||
const threshold = Number(await gnosisSafe.getThreshold())
|
||||
const nonce = Number(await gnosisSafe.nonce())
|
||||
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), await getLocalSafe(safeAddress)))
|
||||
const currentVersion = await gnosisSafe.VERSION()
|
||||
const needsUpdate = await safeNeedsUpdate(currentVersion, latestMasterContractVersion)
|
||||
const featuresEnabled = enabledFeatures(currentVersion)
|
||||
|
||||
const safe: SafeProps = {
|
||||
address: safeAddress,
|
||||
|
@ -50,6 +54,9 @@ export const buildSafe = async (safeAdd: string, safeName: string) => {
|
|||
owners,
|
||||
ethBalance,
|
||||
nonce,
|
||||
currentVersion,
|
||||
needsUpdate,
|
||||
featuresEnabled,
|
||||
}
|
||||
|
||||
return safe
|
||||
|
@ -93,11 +100,12 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDis
|
|||
}
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
export default (safeAdd: string) => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
export default (safeAdd: string) => async (dispatch: ReduxDispatch<GlobalState>, getState: () => GlobalState) => {
|
||||
try {
|
||||
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
||||
const safeProps: SafeProps = await buildSafe(safeAddress, safeName)
|
||||
const latestMasterContractVersion = getState().safes.get('latestMasterContractVersion')
|
||||
const safeProps: SafeProps = await buildSafe(safeAddress, safeName, latestMasterContractVersion)
|
||||
|
||||
dispatch(addSafe(safeProps))
|
||||
} catch (err) {
|
||||
|
|
|
@ -52,6 +52,7 @@ type TxServiceModel = {
|
|||
executionDate: ?string,
|
||||
confirmations: ConfirmationServiceModel[],
|
||||
isExecuted: boolean,
|
||||
isSuccessful: boolean,
|
||||
transactionHash: ?string,
|
||||
creationTx?: boolean,
|
||||
}
|
||||
|
@ -90,10 +91,13 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
|
|||
)
|
||||
const modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
|
||||
const cancellationTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !tx.data
|
||||
const isSendTokenTx = isTokenTransfer(tx.data, Number(tx.value))
|
||||
const code = tx.to ? await web3.eth.getCode(tx.to) : ''
|
||||
const isERC721Token =
|
||||
code.includes('42842e0e') || (isTokenTransfer(tx.data, Number(tx.value)) && code.includes('06fdde03'))
|
||||
const isSendTokenTx = !isERC721Token && isTokenTransfer(tx.data, Number(tx.value))
|
||||
const isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value))
|
||||
const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data)
|
||||
const customTx = !sameAddress(tx.to, safeAddress) && !!tx.data && !isSendTokenTx && !isUpgradeTx
|
||||
const customTx = !sameAddress(tx.to, safeAddress) && !!tx.data && !isSendTokenTx && !isUpgradeTx && !isERC721Token
|
||||
|
||||
let refundParams = null
|
||||
if (tx.gasPrice > 0) {
|
||||
|
@ -163,6 +167,7 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
|
|||
refundReceiver: tx.refundReceiver,
|
||||
refundParams,
|
||||
isExecuted: tx.isExecuted,
|
||||
isSuccessful: tx.isSuccessful,
|
||||
submissionDate: tx.submissionDate,
|
||||
executor: tx.executor,
|
||||
executionDate: tx.executionDate,
|
||||
|
|
|
@ -11,10 +11,10 @@ import { loadFromStorage } from '~/utils/storage'
|
|||
|
||||
const loadSafesFromStorage = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||
try {
|
||||
const safes: ?{ [string]: SafeProps } = await loadFromStorage(SAFES_KEY)
|
||||
const safes: ?{ [key: string]: SafeProps } = await loadFromStorage(SAFES_KEY)
|
||||
|
||||
if (safes) {
|
||||
Object.values(safes).forEach((safeProps: SafeProps) => {
|
||||
Object.values(safes).forEach(safeProps => {
|
||||
dispatch(addSafe(buildSafe(safeProps)))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// @flow
|
||||
import { createAction } from 'redux-actions'
|
||||
|
||||
export const SET_LATEST_MASTER_CONTRACT_VERSION = 'SET_LATEST_MASTER_CONTRACT_VERSION'
|
||||
|
||||
const setLatestMasterContractVersion = createAction<string, *>(SET_LATEST_MASTER_CONTRACT_VERSION)
|
||||
|
||||
export default setLatestMasterContractVersion
|
|
@ -16,6 +16,9 @@ export type SafeProps = {
|
|||
nonce: number,
|
||||
latestIncomingTxBlock?: number,
|
||||
recurringUser?: boolean,
|
||||
currentVersion: string,
|
||||
needsUpdate: boolean,
|
||||
featuresEnabled: string[],
|
||||
}
|
||||
|
||||
const SafeRecord: RecordFactory<SafeProps> = Record({
|
||||
|
@ -30,6 +33,9 @@ const SafeRecord: RecordFactory<SafeProps> = Record({
|
|||
nonce: 0,
|
||||
latestIncomingTxBlock: 0,
|
||||
recurringUser: undefined,
|
||||
currentVersion: '',
|
||||
needsUpdate: false,
|
||||
featuresEnabled: [],
|
||||
})
|
||||
|
||||
export type Safe = RecordOf<SafeProps>
|
||||
|
|
|
@ -20,6 +20,7 @@ export type TransactionStatus =
|
|||
| 'awaiting_your_confirmation'
|
||||
| 'awaiting_confirmations'
|
||||
| 'success'
|
||||
| 'failed'
|
||||
| 'cancelled'
|
||||
| 'awaiting_execution'
|
||||
| 'pending'
|
||||
|
@ -39,6 +40,7 @@ export type TransactionProps = {
|
|||
gasToken: string,
|
||||
refundReceiver: string,
|
||||
isExecuted: boolean,
|
||||
isSuccessful: boolean,
|
||||
submissionDate: ?string,
|
||||
executionDate: ?string,
|
||||
symbol: string,
|
||||
|
@ -75,6 +77,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
|||
gasToken: ZERO_ADDRESS,
|
||||
refundReceiver: ZERO_ADDRESS,
|
||||
isExecuted: false,
|
||||
isSuccessful: true,
|
||||
submissionDate: '',
|
||||
executor: '',
|
||||
executionDate: '',
|
||||
|
|
|
@ -11,9 +11,10 @@ import { REMOVE_SAFE } from '~/routes/safe/store/actions/removeSafe'
|
|||
import { REMOVE_SAFE_OWNER } from '~/routes/safe/store/actions/removeSafeOwner'
|
||||
import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner'
|
||||
import { SET_DEFAULT_SAFE } from '~/routes/safe/store/actions/setDefaultSafe'
|
||||
import { SET_LATEST_MASTER_CONTRACT_VERSION } from '~/routes/safe/store/actions/setLatestMasterContractVersion'
|
||||
import { UPDATE_SAFE } from '~/routes/safe/store/actions/updateSafe'
|
||||
import { UPDATE_SAFE_THRESHOLD } from '~/routes/safe/store/actions/updateSafeThreshold'
|
||||
import { type OwnerProps, makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import { makeOwner } from '~/routes/safe/store/models/owner'
|
||||
import SafeRecord, { type SafeProps } from '~/routes/safe/store/models/safe'
|
||||
|
||||
export const SAFE_REDUCER_ID = 'safes'
|
||||
|
@ -21,11 +22,8 @@ export const SAFE_REDUCER_ID = 'safes'
|
|||
export type SafeReducerState = Map<string, *>
|
||||
|
||||
export const buildSafe = (storedSafe: SafeProps) => {
|
||||
const names = storedSafe.owners.map((owner: OwnerProps) => owner.name)
|
||||
const addresses = storedSafe.owners.map((owner: OwnerProps) => {
|
||||
const checksumed = getWeb3().utils.toChecksumAddress(owner.address)
|
||||
return checksumed
|
||||
})
|
||||
const names = storedSafe.owners.map(owner => owner.name)
|
||||
const addresses = storedSafe.owners.map(owner => getWeb3().utils.toChecksumAddress(owner.address))
|
||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||
const activeTokens = Set(storedSafe.activeTokens)
|
||||
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
||||
|
@ -53,7 +51,7 @@ export default handleActions<SafeReducerState, *>(
|
|||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const tokenAddress = action.payload
|
||||
|
||||
const newState = state.withMutations(map => {
|
||||
return state.withMutations(map => {
|
||||
map
|
||||
.get('safes')
|
||||
.keySeq()
|
||||
|
@ -64,8 +62,6 @@ export default handleActions<SafeReducerState, *>(
|
|||
map.updateIn(['safes', safeAddress], prevSafe => prevSafe.merge({ activeTokens }))
|
||||
})
|
||||
})
|
||||
|
||||
return newState
|
||||
},
|
||||
[ADD_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||
const { safe }: { safe: SafeProps } = action.payload
|
||||
|
@ -132,10 +128,14 @@ export default handleActions<SafeReducerState, *>(
|
|||
},
|
||||
[SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState =>
|
||||
state.set('defaultSafe', action.payload),
|
||||
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState =>
|
||||
state.set('latestMasterContractVersion', action.payload),
|
||||
},
|
||||
Map({
|
||||
// $FlowFixMe
|
||||
defaultSafe: undefined,
|
||||
safes: Map(),
|
||||
// $FlowFixMe
|
||||
latestMasterContractVersion: '',
|
||||
}),
|
||||
)
|
||||
|
|
|
@ -52,6 +52,14 @@ export const defaultSafeSelector: OutputSelector<GlobalState, {}, string> = crea
|
|||
(safeState: Map<string, *>): string => safeState.get('defaultSafe'),
|
||||
)
|
||||
|
||||
export const latestMasterContractVersionSelector: OutputSelector<
|
||||
GlobalState,
|
||||
{},
|
||||
string,
|
||||
> = createSelector(safesStateSelector, (safeState: Map<string, *>): string =>
|
||||
safeState.get('latestMasterContractVersion'),
|
||||
)
|
||||
|
||||
const transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||
|
||||
const cancellationTransactionsSelector = (state: GlobalState): CancelTransactionsState =>
|
||||
|
|
|
@ -6,6 +6,14 @@ import thunk from 'redux-thunk'
|
|||
|
||||
import addressBookMiddleware from '~/logic/addressBook/store/middleware/addressBookMiddleware'
|
||||
import addressBook, { ADDRESS_BOOK_REDUCER_ID } from '~/logic/addressBook/store/reducer/addressBook'
|
||||
import {
|
||||
type NFTAssetsState,
|
||||
type NFTTokensState,
|
||||
NFT_ASSETS_REDUCER_ID,
|
||||
NFT_TOKENS_REDUCER_ID,
|
||||
nftAssetReducer,
|
||||
nftTokensReducer,
|
||||
} from '~/logic/collectibles/store/reducer/collectibles'
|
||||
import cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
|
||||
import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
|
||||
import currentSession, {
|
||||
|
@ -54,6 +62,8 @@ export type GlobalState = {
|
|||
providers: ProviderState,
|
||||
router: RouterHistory,
|
||||
safes: SafeState,
|
||||
nftAssets: NFTAssetsState,
|
||||
nftTokens: NFTTokensState,
|
||||
tokens: TokensState,
|
||||
transactions: TransactionsState,
|
||||
cancellationTransactions: CancelTransactionsState,
|
||||
|
@ -68,6 +78,8 @@ const reducers: CombinedReducer<GlobalState, *> = combineReducers({
|
|||
router: connectRouter(history),
|
||||
[PROVIDER_REDUCER_ID]: provider,
|
||||
[SAFE_REDUCER_ID]: safe,
|
||||
[NFT_ASSETS_REDUCER_ID]: nftAssetReducer,
|
||||
[NFT_TOKENS_REDUCER_ID]: nftTokensReducer,
|
||||
[TOKEN_REDUCER_ID]: tokens,
|
||||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: cancellationTransactions,
|
||||
|
|
|
@ -309,6 +309,16 @@ const theme = createMuiTheme({
|
|||
},
|
||||
},
|
||||
},
|
||||
MuiTableContainer: {
|
||||
root: {
|
||||
marginLeft: '-10px',
|
||||
marginRight: '-10px',
|
||||
marginTop: '-10px',
|
||||
paddingLeft: '10px',
|
||||
paddingRight: '10px',
|
||||
paddingTop: '10px',
|
||||
},
|
||||
},
|
||||
MuiTablePagination: {
|
||||
toolbar: {
|
||||
paddingRight: '15px',
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { textShortener } from '~/utils/strings'
|
||||
|
||||
describe('Utils > strings > textShortener', () => {
|
||||
it(`should return the original string if there's no room to shorten`, () => {
|
||||
// Given
|
||||
const text = "I'm a short string"
|
||||
const shortener = textShortener() // default values
|
||||
|
||||
// When
|
||||
const shortenText = shortener(text)
|
||||
|
||||
// Then
|
||||
expect(shortenText).toEqual(text)
|
||||
})
|
||||
|
||||
it(`should return a shorter version with the default ellipsis`, () => {
|
||||
// Given
|
||||
const text = "I'm a short string" // 18 chars long
|
||||
const shortener = textShortener({ charsStart: 2, charsEnd: 2 })
|
||||
|
||||
// When
|
||||
const shortenText = shortener(text)
|
||||
|
||||
// Then
|
||||
expect(shortenText).toEqual("I'...ng")
|
||||
})
|
||||
|
||||
it(`should return a shorter version with the '---' ellipsis`, () => {
|
||||
// Given
|
||||
const text = "I'm a short string" // 18 chars long
|
||||
const shortener = textShortener({ charsStart: 2, charsEnd: 2, ellipsis: '---' })
|
||||
|
||||
// When
|
||||
const shortenText = shortener(text)
|
||||
|
||||
// Then
|
||||
expect(shortenText).toEqual("I'---ng")
|
||||
})
|
||||
|
||||
it(`should return a shorter version with only the start of the string`, () => {
|
||||
// Given
|
||||
const text = "I'm a short string" // 18 chars long
|
||||
const shortener = textShortener({ charsStart: 2, charsEnd: 0 })
|
||||
|
||||
// When
|
||||
const shortenText = shortener(text)
|
||||
|
||||
// Then
|
||||
expect(shortenText).toEqual("I'...")
|
||||
})
|
||||
|
||||
it(`should return a shorter version with only the end of the string`, () => {
|
||||
// Given
|
||||
const text = "I'm a short string" // 18 chars long
|
||||
const shortener = textShortener({ charsStart: 0, charsEnd: 2 })
|
||||
|
||||
// When
|
||||
const shortenText = shortener(text)
|
||||
|
||||
// Then
|
||||
expect(shortenText).toEqual('...ng')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
export const NETWORK = process.env.REACT_APP_NETWORK
|
||||
export const GOOGLE_ANALYTICS_ID_RINKEBY = process.env.REACT_APP_GOOGLE_ANALYTICS_ID_RINKEBY
|
||||
export const GOOGLE_ANALYTICS_ID_MAINNET = process.env.REACT_APP_GOOGLE_ANALYTICS_ID_MAINNET
|
||||
export const INTERCOM_ID = process.env.REACT_APP_INTERCOM_ID
|
||||
export const PORTIS_ID = process.env.REACT_APP_PORTIS_ID
|
||||
export const SQUARELINK_ID = process.env.REACT_APP_SQUARELINK_ID
|
||||
export const FORTMATIC_KEY = process.env.REACT_APP_FORTMATIC_KEY
|
||||
export const INFURA_TOKEN = process.env.REACT_APP_INFURA_TOKEN || ''
|
||||
export const LATEST_SAFE_VERSION = process.env.REACT_APP_LATEST_SAFE_VERSION || 'not-defined'
|
||||
export const APP_VERSION = process.env.REACT_APP_APP_VERSION || 'not-defined'
|
||||
export const OPENSEA_API_KEY = process.env.REACT_APP_OPENSEA_API_KEY || ''
|
||||
export const COLLECTIBLES_SOURCE = process.env.REACT_APP_COLLECTIBLES_SOURCE || 'OpenSea'
|
|
@ -0,0 +1,54 @@
|
|||
// @flow
|
||||
type ShortenTextOptionsProps = {
|
||||
charsStart?: number,
|
||||
charsEnd?: number,
|
||||
ellipsis?: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Setups `shortenText` options
|
||||
* @param {object} opts
|
||||
* @param {number} opts.charsStart=10 - characters to preserve from the beginning
|
||||
* @param {number} opts.charsEnd=10 - characters to preserve at the end
|
||||
* @param {string} opts.ellipsis='...' - ellipsis characters
|
||||
* @returns {function} shortener
|
||||
*/
|
||||
export const textShortener = ({ charsEnd = 10, charsStart = 10, ellipsis = '...' }: ShortenTextOptionsProps = {}) =>
|
||||
/**
|
||||
* @function
|
||||
* @name shortener
|
||||
*
|
||||
* Shortens a text string based on options
|
||||
* @param {string} text=null - String to shorten
|
||||
* @param text
|
||||
* @returns {string|?string}
|
||||
*/
|
||||
(text: ?string = null) => {
|
||||
if (typeof text !== 'string') {
|
||||
throw new TypeError(` A string is required. ${typeof text} was provided instead.`)
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const amountOfCharsToKeep = charsEnd + charsStart
|
||||
const finalStringLength = amountOfCharsToKeep + ellipsis.length
|
||||
|
||||
if (finalStringLength >= text.length || !amountOfCharsToKeep) {
|
||||
// no need to shorten
|
||||
return text
|
||||
}
|
||||
|
||||
const r = new RegExp(`^(.{${charsStart}}).+(.{${charsEnd}})$`)
|
||||
const matchResult = r.exec(text)
|
||||
|
||||
if (!matchResult) {
|
||||
// if for any reason the exec returns null, the text remains untouched
|
||||
return text
|
||||
}
|
||||
|
||||
const [, textStart, textEnd] = matchResult
|
||||
|
||||
return `${textStart}${ellipsis}${textEnd}`
|
||||
}
|
186
yarn.lock
186
yarn.lock
|
@ -1499,6 +1499,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
||||
|
||||
"@openzeppelin/contracts@^2.5.0":
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@openzeppelin/contracts/-/contracts-2.5.0.tgz#e327a98ba1d26b7756ff62885a0aa0967a375449"
|
||||
integrity sha512-t3jm8FrhL9tkkJTofkznTqo/XXdHi21w5yXwalEnaMOp22ZwZ0f/mmKdlgMMLPFa6bSVHbY88mKESwJT/7m5Lg==
|
||||
|
||||
"@portis/eth-json-rpc-middleware@^4.1.2":
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@portis/eth-json-rpc-middleware/-/eth-json-rpc-middleware-4.1.2.tgz#391e392da03dea348c8111a8111ce4550aa24a02"
|
||||
|
@ -2410,11 +2415,6 @@ abab@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
|
||||
integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
|
||||
|
||||
abi-decoder@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/abi-decoder/-/abi-decoder-1.2.0.tgz#c42882dbb91b444805f0cd203a87a5cc3c22f4a8"
|
||||
|
@ -2659,19 +2659,11 @@ app-module-path@^2.2.0:
|
|||
resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"
|
||||
integrity sha1-ZBqlXft9am8KgUHEucCqULbCTdU=
|
||||
|
||||
aproba@^1.0.3, aproba@^1.1.1:
|
||||
aproba@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
|
||||
|
||||
are-we-there-yet@~1.1.2:
|
||||
version "1.1.5"
|
||||
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
|
||||
integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==
|
||||
dependencies:
|
||||
delegates "^1.0.0"
|
||||
readable-stream "^2.0.6"
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
|
@ -2857,6 +2849,11 @@ async-limiter@^1.0.0, async-limiter@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
||||
|
||||
async-sema@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.0.tgz#3a813beb261e4cc58b19213916a48e931e21d21e"
|
||||
integrity sha512-+JpRq3r0zjpRLDruS6q/nC4V5tzsaiu07521677Mdi5i+AkaU/aNJH38rYHJVQ4zvz+SSkjgc8FUI7qIZrR+3g==
|
||||
|
||||
async@2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
|
||||
|
@ -4821,11 +4818,6 @@ console-browserify@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
|
||||
|
||||
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
|
||||
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
|
||||
|
||||
constants-browserify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||
|
@ -5348,7 +5340,7 @@ debug@3.1.0, debug@=3.1.0:
|
|||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6:
|
||||
debug@3.2.6, debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5:
|
||||
version "3.2.6"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||
|
@ -5464,11 +5456,6 @@ deep-equal@^1.0.1, deep-equal@~1.1.1:
|
|||
object-keys "^1.1.1"
|
||||
regexp.prototype.flags "^1.2.0"
|
||||
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
||||
deep-is@~0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||
|
@ -5574,11 +5561,6 @@ delayed-stream@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
delegates@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
||||
integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=
|
||||
|
||||
depd@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
|
@ -5616,11 +5598,6 @@ detect-installed@^2.0.4:
|
|||
dependencies:
|
||||
get-installed-path "^2.0.3"
|
||||
|
||||
detect-libc@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
|
||||
integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=
|
||||
|
||||
detect-newline@2.X:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
|
||||
|
@ -7903,20 +7880,6 @@ ganache-core@2.7.0:
|
|||
ethereumjs-wallet "0.6.3"
|
||||
web3 "1.2.1"
|
||||
|
||||
gauge@~2.7.3:
|
||||
version "2.7.4"
|
||||
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
|
||||
integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=
|
||||
dependencies:
|
||||
aproba "^1.0.3"
|
||||
console-control-strings "^1.0.0"
|
||||
has-unicode "^2.0.0"
|
||||
object-assign "^4.1.0"
|
||||
signal-exit "^3.0.0"
|
||||
string-width "^1.0.1"
|
||||
strip-ansi "^3.0.1"
|
||||
wide-align "^1.1.0"
|
||||
|
||||
gensync@^1.0.0-beta.1:
|
||||
version "1.0.0-beta.1"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269"
|
||||
|
@ -8343,11 +8306,6 @@ has-to-string-tag-x@^1.2.0:
|
|||
dependencies:
|
||||
has-symbol-support-x "^1.4.1"
|
||||
|
||||
has-unicode@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
|
||||
integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=
|
||||
|
||||
has-value@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
|
||||
|
@ -8726,7 +8684,7 @@ ice-cap@0.0.4:
|
|||
cheerio "0.20.0"
|
||||
color-logger "0.0.3"
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@~0.4.13:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
|
@ -8762,13 +8720,6 @@ iferr@^0.1.5:
|
|||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
|
||||
|
||||
ignore-walk@^3.0.1:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37"
|
||||
integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==
|
||||
dependencies:
|
||||
minimatch "^3.0.4"
|
||||
|
||||
ignore@^3.3.5:
|
||||
version "3.3.10"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
|
||||
|
@ -8896,7 +8847,7 @@ inherits@2.0.3:
|
|||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
|
||||
ini@^1.3.4, ini@^1.3.5:
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
||||
|
@ -11567,15 +11518,6 @@ natural-compare@^1.4.0:
|
|||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
||||
|
||||
needle@^2.2.1:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/needle/-/needle-2.3.3.tgz#a041ad1d04a871b0ebb666f40baaf1fb47867117"
|
||||
integrity sha512-EkY0GeSq87rWp1hoq/sH/wnTWgFVhYlnIkbJ0YJFfRgEFlz2RraCjBpFQ+vrEgEdp0ThfyHADmkChEhcb7PKyw==
|
||||
dependencies:
|
||||
debug "^3.2.6"
|
||||
iconv-lite "^0.4.4"
|
||||
sax "^1.2.4"
|
||||
|
||||
negotiator@0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||
|
@ -11694,22 +11636,6 @@ node-notifier@^6.0.0:
|
|||
shellwords "^0.1.1"
|
||||
which "^1.3.1"
|
||||
|
||||
node-pre-gyp@*:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83"
|
||||
integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==
|
||||
dependencies:
|
||||
detect-libc "^1.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
needle "^2.2.1"
|
||||
nopt "^4.0.1"
|
||||
npm-packlist "^1.1.6"
|
||||
npmlog "^4.0.2"
|
||||
rc "^1.2.7"
|
||||
rimraf "^2.6.1"
|
||||
semver "^5.3.0"
|
||||
tar "^4.4.2"
|
||||
|
||||
node-releases@^1.1.47, node-releases@^1.1.50:
|
||||
version "1.1.51"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.51.tgz#70d0e054221343d2966006bfbd4d98622cc00bd0"
|
||||
|
@ -11717,14 +11643,6 @@ node-releases@^1.1.47, node-releases@^1.1.50:
|
|||
dependencies:
|
||||
semver "^6.3.0"
|
||||
|
||||
nopt@^4.0.1:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48"
|
||||
integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==
|
||||
dependencies:
|
||||
abbrev "1"
|
||||
osenv "^0.1.4"
|
||||
|
||||
normalize-hex@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/normalize-hex/-/normalize-hex-0.0.2.tgz#5491c43759db2f06b7168d8419f4925c271ab27e"
|
||||
|
@ -11788,27 +11706,6 @@ normalize-url@^4.1.0:
|
|||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
npm-bundled@^1.0.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b"
|
||||
integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==
|
||||
dependencies:
|
||||
npm-normalize-package-bin "^1.0.1"
|
||||
|
||||
npm-normalize-package-bin@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2"
|
||||
integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==
|
||||
|
||||
npm-packlist@^1.1.6:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e"
|
||||
integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==
|
||||
dependencies:
|
||||
ignore-walk "^3.0.1"
|
||||
npm-bundled "^1.0.1"
|
||||
npm-normalize-package-bin "^1.0.1"
|
||||
|
||||
npm-programmatic@0.0.6:
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/npm-programmatic/-/npm-programmatic-0.0.6.tgz#3c8f4dbb210efd65b99ee6a5ac76f27b4d5d6b78"
|
||||
|
@ -11830,16 +11727,6 @@ npm-run-path@^4.0.0:
|
|||
dependencies:
|
||||
path-key "^3.0.0"
|
||||
|
||||
npmlog@^4.0.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
||||
dependencies:
|
||||
are-we-there-yet "~1.1.2"
|
||||
console-control-strings "~1.1.0"
|
||||
gauge "~2.7.3"
|
||||
set-blocking "~2.0.0"
|
||||
|
||||
nth-check@^1.0.2, nth-check@~1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
||||
|
@ -12170,19 +12057,11 @@ os-locale@^3.0.0, os-locale@^3.1.0:
|
|||
lcid "^2.0.0"
|
||||
mem "^4.0.0"
|
||||
|
||||
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
|
||||
os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
||||
|
||||
osenv@^0.1.4:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
|
||||
integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==
|
||||
dependencies:
|
||||
os-homedir "^1.0.0"
|
||||
os-tmpdir "^1.0.0"
|
||||
|
||||
p-cancelable@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
|
||||
|
@ -13408,16 +13287,6 @@ raw-body@2.4.0:
|
|||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
|
||||
dependencies:
|
||||
deep-extend "^0.6.0"
|
||||
ini "~1.3.0"
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-dev-utils@^10.0.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.2.0.tgz#b11cc48aa2be2502fb3c27a50d1dfa95cfa9dfe0"
|
||||
|
@ -13634,7 +13503,7 @@ read-pkg@^3.0.0:
|
|||
normalize-package-data "^2.3.2"
|
||||
path-type "^3.0.0"
|
||||
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
|
@ -14243,7 +14112,7 @@ rimraf@2.6.3, rimraf@~2.6.2:
|
|||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3, rimraf@^2.7.1:
|
||||
rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.3, rimraf@^2.7.1:
|
||||
version "2.7.1"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||
|
@ -14390,7 +14259,7 @@ sane@^4.0.2, sane@^4.0.3:
|
|||
minimist "^1.1.1"
|
||||
walker "~1.0.5"
|
||||
|
||||
sax@^1.1.4, sax@^1.2.4, sax@~1.2.4:
|
||||
sax@^1.1.4, sax@~1.2.4:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
@ -14658,7 +14527,7 @@ servify@^0.1.12:
|
|||
request "^2.79.0"
|
||||
xhr "^2.3.3"
|
||||
|
||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||
|
@ -15241,7 +15110,7 @@ string-width@^1.0.1:
|
|||
is-fullwidth-code-point "^1.0.0"
|
||||
strip-ansi "^3.0.0"
|
||||
|
||||
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||
string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||
|
@ -15421,7 +15290,7 @@ strip-indent@^3.0.0:
|
|||
dependencies:
|
||||
min-indent "^1.0.0"
|
||||
|
||||
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
||||
strip-json-comments@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
@ -15625,7 +15494,7 @@ tar-stream@^1.5.2:
|
|||
to-buffer "^1.1.1"
|
||||
xtend "^4.0.0"
|
||||
|
||||
tar@^4.0.2, tar@^4.4.2:
|
||||
tar@^4.0.2:
|
||||
version "4.4.13"
|
||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
||||
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
||||
|
@ -17347,9 +17216,9 @@ web3-provider-engine@^15.0.4:
|
|||
xhr "^2.2.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
"web3-provider-engine@git+https://github.com/trufflesuite/provider-engine.git#web3-one":
|
||||
"web3-provider-engine@https://github.com/trufflesuite/provider-engine#web3-one":
|
||||
version "14.0.6"
|
||||
resolved "git+https://github.com/trufflesuite/provider-engine.git#3538c60bc4836b73ccae1ac3f64c8fed8ef19c1a"
|
||||
resolved "https://github.com/trufflesuite/provider-engine#3538c60bc4836b73ccae1ac3f64c8fed8ef19c1a"
|
||||
dependencies:
|
||||
async "^2.5.0"
|
||||
backoff "^2.5.0"
|
||||
|
@ -17789,13 +17658,6 @@ which@^2.0.1:
|
|||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
wide-align@^1.1.0:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
|
||||
integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
|
||||
dependencies:
|
||||
string-width "^1.0.2 || 2"
|
||||
|
||||
window-size@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||
|
|
Loading…
Reference in New Issue