(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/core": "4.9.5",
|
||||||
"@material-ui/icons": "4.9.1",
|
"@material-ui/icons": "4.9.1",
|
||||||
"@material-ui/lab": "4.0.0-alpha.39",
|
"@material-ui/lab": "4.0.0-alpha.39",
|
||||||
|
"@openzeppelin/contracts": "^2.5.0",
|
||||||
"@testing-library/jest-dom": "5.1.1",
|
"@testing-library/jest-dom": "5.1.1",
|
||||||
"@welldone-software/why-did-you-render": "4.0.5",
|
"@welldone-software/why-did-you-render": "4.0.5",
|
||||||
|
"async-sema": "^3.1.0",
|
||||||
"axios": "0.19.2",
|
"axios": "0.19.2",
|
||||||
"bignumber.js": "9.0.0",
|
"bignumber.js": "9.0.0",
|
||||||
"bnc-onboard": "1.3.5",
|
"bnc-onboard": "1.3.5",
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
import IconButton from '@material-ui/core/IconButton'
|
||||||
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
import { createStyles, makeStyles } from '@material-ui/core/styles'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import React from 'react'
|
import React, { type Node } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
|
@ -40,8 +40,8 @@ const useStyles = makeStyles(() =>
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string,
|
title: string,
|
||||||
body: React.Node,
|
body: Node,
|
||||||
footer: React.Node,
|
footer: Node,
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,6 +30,7 @@ const useStyles = makeStyles({
|
||||||
padding: '27px 15px',
|
padding: '27px 15px',
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
zIndex: '5',
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
maxWidth: '100%',
|
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
|
// @flow
|
||||||
import semverLessThan from 'semver/functions/lt'
|
import semverLessThan from 'semver/functions/lt'
|
||||||
|
import semverSatisfies from 'semver/functions/satisfies'
|
||||||
import semverValid from 'semver/functions/valid'
|
import semverValid from 'semver/functions/valid'
|
||||||
|
|
||||||
import { getSafeLastVersion } from '~/config'
|
import { getSafeLastVersion } from '~/config'
|
||||||
import { getGnosisSafeInstanceAt, getSafeMasterContract } from '~/logic/contracts/safeContracts'
|
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) => {
|
export const checkIfSafeNeedsUpdate = async (gnosisSafeInstance, lastSafeVersion) => {
|
||||||
if (!gnosisSafeInstance || !lastSafeVersion) {
|
if (!gnosisSafeInstance || !lastSafeVersion) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const safeMasterVersion = await gnosisSafeInstance.VERSION()
|
const safeMasterVersion = await getCurrentSafeVersion(gnosisSafeInstance)
|
||||||
const current = semverValid(safeMasterVersion)
|
const current = semverValid(safeMasterVersion)
|
||||||
const latest = semverValid(lastSafeVersion)
|
const latest = semverValid(lastSafeVersion)
|
||||||
const needUpdate = latest ? semverLessThan(current, latest) : false
|
const needUpdate = safeNeedsUpdate(safeMasterVersion, lastSafeVersion)
|
||||||
|
|
||||||
return { current, latest, needUpdate }
|
return { current, latest, needUpdate }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCurrentMasterContractLastVersion = async () => {
|
export const getCurrentMasterContractLastVersion = async () => {
|
||||||
const safeMaster = await getSafeMasterContract()
|
const safeMaster = await getSafeMasterContract()
|
||||||
let safeMasterVersion
|
let safeMasterVersion
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
|
import StandardToken from '@gnosis.pm/util-contracts/build/contracts/GnosisStandardToken.json'
|
||||||
import HumanFriendlyToken from '@gnosis.pm/util-contracts/build/contracts/HumanFriendlyToken.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 { List } from 'immutable'
|
||||||
import type { Dispatch as ReduxDispatch } from 'redux'
|
import type { Dispatch as ReduxDispatch } from 'redux'
|
||||||
import contract from 'truffle-contract'
|
import contract from 'truffle-contract'
|
||||||
|
@ -28,10 +29,27 @@ const createHumanFriendlyTokenContract = async () => {
|
||||||
return humanErc20Token
|
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 getHumanFriendlyToken = ensureOnce(createHumanFriendlyTokenContract)
|
||||||
|
|
||||||
export const getStandardTokenContract = ensureOnce(createStandardTokenContract)
|
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>) => {
|
export const fetchTokens = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
|
|
|
@ -222,7 +222,7 @@ const AddressBookTable = ({ classes }: Props) => {
|
||||||
onClose={() => setDeleteEntryModalOpen(false)}
|
onClose={() => setDeleteEntryModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<SendModal
|
<SendModal
|
||||||
activeScreenType="sendFunds"
|
activeScreenType="chooseTxType"
|
||||||
ethBalance={ethBalance}
|
ethBalance={ethBalance}
|
||||||
isOpen={sendFundsModalOpen}
|
isOpen={sendFundsModalOpen}
|
||||||
onClose={() => setSendFundsModalOpen(false)}
|
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
|
// @flow
|
||||||
import CircularProgress from '@material-ui/core/CircularProgress'
|
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 cn from 'classnames'
|
||||||
import { List } from 'immutable'
|
import { List } from 'immutable'
|
||||||
import React, { Suspense, useEffect, useState } from 'react'
|
import React, { Suspense, useEffect, useState } from 'react'
|
||||||
|
|
||||||
import Modal from '~/components/Modal'
|
import Modal from '~/components/Modal'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
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 ChooseTxType = React.lazy(() => import('./screens/ChooseTxType'))
|
||||||
|
|
||||||
const SendFunds = React.lazy(() => import('./screens/SendFunds'))
|
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 ReviewTx = React.lazy(() => import('./screens/ReviewTx'))
|
||||||
|
|
||||||
const SendCustomTx = React.lazy(() => import('./screens/SendCustomTx'))
|
const SendCustomTx = React.lazy(() => import('./screens/SendCustomTx'))
|
||||||
|
|
||||||
const ReviewCustomTx = React.lazy(() => import('./screens/ReviewCustomTx'))
|
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 = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
classes: Object,
|
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
ethBalance: string,
|
ethBalance: string,
|
||||||
tokens: List<Token>,
|
tokens: List<Token>,
|
||||||
selectedToken: string,
|
selectedToken?: string | NFTToken | {},
|
||||||
createTransaction: Function,
|
createTransaction?: Function,
|
||||||
activeScreenType: ActiveScreen,
|
activeScreenType: ActiveScreen,
|
||||||
recipientAddress?: string,
|
recipientAddress?: string,
|
||||||
}
|
}
|
||||||
|
@ -43,7 +54,7 @@ type TxStateType =
|
||||||
}
|
}
|
||||||
| Object
|
| Object
|
||||||
|
|
||||||
const styles = () => ({
|
const useStyles = makeStyles({
|
||||||
scalableModalWindow: {
|
scalableModalWindow: {
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
},
|
},
|
||||||
|
@ -51,19 +62,17 @@ const styles = () => ({
|
||||||
height: 'auto',
|
height: 'auto',
|
||||||
position: 'static',
|
position: 'static',
|
||||||
},
|
},
|
||||||
})
|
loaderStyle: {
|
||||||
|
|
||||||
const loaderStyle = {
|
|
||||||
height: '500px',
|
height: '500px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}
|
},
|
||||||
|
})
|
||||||
|
|
||||||
const Send = ({
|
const SendModal = ({
|
||||||
activeScreenType,
|
activeScreenType,
|
||||||
classes,
|
|
||||||
createTransaction,
|
createTransaction,
|
||||||
ethBalance,
|
ethBalance,
|
||||||
isOpen,
|
isOpen,
|
||||||
|
@ -74,6 +83,7 @@ const Send = ({
|
||||||
selectedToken,
|
selectedToken,
|
||||||
tokens,
|
tokens,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const classes = useStyles()
|
||||||
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType')
|
const [activeScreen, setActiveScreen] = useState<ActiveScreen>(activeScreenType || 'chooseTxType')
|
||||||
const [tx, setTx] = useState<TxStateType>({})
|
const [tx, setTx] = useState<TxStateType>({})
|
||||||
|
|
||||||
|
@ -94,6 +104,11 @@ const Send = ({
|
||||||
setTx(customTxInfo)
|
setTx(customTxInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSendCollectible = txInfo => {
|
||||||
|
setActiveScreen('reviewCollectible')
|
||||||
|
setTx(txInfo)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
description="Send Tokens Form"
|
description="Send Tokens Form"
|
||||||
|
@ -104,12 +119,14 @@ const Send = ({
|
||||||
>
|
>
|
||||||
<Suspense
|
<Suspense
|
||||||
fallback={
|
fallback={
|
||||||
<div style={loaderStyle}>
|
<div className={classes.loaderStyle}>
|
||||||
<CircularProgress size={40} />
|
<CircularProgress size={40} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{activeScreen === 'chooseTxType' && <ChooseTxType onClose={onClose} setActiveScreen={setActiveScreen} />}
|
{activeScreen === 'chooseTxType' && (
|
||||||
|
<ChooseTxType onClose={onClose} recipientAddress={recipientAddress} setActiveScreen={setActiveScreen} />
|
||||||
|
)}
|
||||||
{activeScreen === 'sendFunds' && (
|
{activeScreen === 'sendFunds' && (
|
||||||
<SendFunds
|
<SendFunds
|
||||||
ethBalance={ethBalance}
|
ethBalance={ethBalance}
|
||||||
|
@ -141,6 +158,7 @@ const Send = ({
|
||||||
initialValues={tx}
|
initialValues={tx}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={handleCustomTxCreation}
|
onSubmit={handleCustomTxCreation}
|
||||||
|
recipientAddress={recipientAddress}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
safeName={safeName}
|
safeName={safeName}
|
||||||
/>
|
/>
|
||||||
|
@ -156,9 +174,21 @@ const Send = ({
|
||||||
tx={tx}
|
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>
|
</Suspense>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Send)
|
export default SendModal
|
||||||
|
|
|
@ -1,22 +1,26 @@
|
||||||
// @flow
|
// @flow
|
||||||
import IconButton from '@material-ui/core/IconButton'
|
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 Close from '@material-ui/icons/Close'
|
||||||
import classNames from 'classnames/bind'
|
import classNames from 'classnames/bind'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import Code from '../assets/code.svg'
|
import Code from '../assets/code.svg'
|
||||||
|
import Collectible from '../assets/collectibles.svg'
|
||||||
import Token from '../assets/token.svg'
|
import Token from '../assets/token.svg'
|
||||||
|
|
||||||
|
import { mustBeEthereumContractAddress } from '~/components/forms/validator'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
import Col from '~/components/layout/Col'
|
import Col from '~/components/layout/Col'
|
||||||
import Hairline from '~/components/layout/Hairline'
|
import Hairline from '~/components/layout/Hairline'
|
||||||
import Img from '~/components/layout/Img'
|
import Img from '~/components/layout/Img'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
|
import { safeSelector } from '~/routes/safe/store/selectors'
|
||||||
import { lg, md, sm } from '~/theme/variables'
|
import { lg, md, sm } from '~/theme/variables'
|
||||||
|
|
||||||
const styles = () => ({
|
const useStyles = makeStyles({
|
||||||
heading: {
|
heading: {
|
||||||
padding: `${md} ${lg}`,
|
padding: `${md} ${lg}`,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
|
@ -26,6 +30,14 @@ const styles = () => ({
|
||||||
manage: {
|
manage: {
|
||||||
fontSize: lg,
|
fontSize: lg,
|
||||||
},
|
},
|
||||||
|
disclaimer: {
|
||||||
|
marginBottom: `-${md}`,
|
||||||
|
paddingTop: md,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
disclaimerText: {
|
||||||
|
fontSize: md,
|
||||||
|
},
|
||||||
closeIcon: {
|
closeIcon: {
|
||||||
height: '35px',
|
height: '35px',
|
||||||
width: '35px',
|
width: '35px',
|
||||||
|
@ -51,11 +63,32 @@ const styles = () => ({
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
classes: Object,
|
recipientAddress?: string,
|
||||||
setActiveScreen: Function,
|
setActiveScreen: Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
const ChooseTxType = ({ onClose, recipientAddress, setActiveScreen }: Props) => {
|
||||||
|
const classes = useStyles()
|
||||||
|
const { featuresEnabled } = useSelector(safeSelector)
|
||||||
|
const erc721Enabled = featuresEnabled.includes('ERC721')
|
||||||
|
const [disableCustomTx, setDisableCustomTx] = React.useState(!!recipientAddress)
|
||||||
|
|
||||||
|
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>
|
<Row align="center" className={classes.heading} grow>
|
||||||
<Paragraph className={classes.manage} noMargin weight="bolder">
|
<Paragraph className={classes.manage} noMargin weight="bolder">
|
||||||
|
@ -66,6 +99,15 @@ const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Row>
|
</Row>
|
||||||
<Hairline />
|
<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">
|
<Row align="center">
|
||||||
<Col className={classes.buttonColumn} layout="column" middle="xs">
|
<Col className={classes.buttonColumn} layout="column" middle="xs">
|
||||||
<Button
|
<Button
|
||||||
|
@ -79,8 +121,26 @@ const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||||
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
<Img alt="Send funds" className={classNames(classes.leftIcon, classes.iconSmall)} src={Token} />
|
||||||
Send funds
|
Send funds
|
||||||
</Button>
|
</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
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={disableCustomTx}
|
||||||
minHeight={52}
|
minHeight={52}
|
||||||
minWidth={260}
|
minWidth={260}
|
||||||
onClick={() => setActiveScreen('sendCustomTx')}
|
onClick={() => setActiveScreen('sendCustomTx')}
|
||||||
|
@ -93,5 +153,6 @@ const ChooseTxType = ({ classes, onClose, setActiveScreen }: Props) => (
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(ChooseTxType)
|
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 = {
|
type Props = {
|
||||||
onClose: () => void,
|
onClose: () => void,
|
||||||
classes: Object,
|
classes: Object,
|
||||||
|
recipientAddress: string,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
ethBalance: string,
|
ethBalance: string,
|
||||||
|
@ -41,10 +42,19 @@ type Props = {
|
||||||
initialValues: Object,
|
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 [qrModalOpen, setQrModalOpen] = useState<boolean>(false)
|
||||||
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
const [selectedEntry, setSelectedEntry] = useState<Object | null>({
|
||||||
address: '',
|
address: recipientAddress || initialValues.recipientAddress,
|
||||||
name: '',
|
name: '',
|
||||||
})
|
})
|
||||||
const [pristine, setPristine] = useState<boolean>(true)
|
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 AssetTableCell from './AssetTableCell'
|
||||||
import Receive from './Receive'
|
import Receive from './Receive'
|
||||||
import SendModal from './SendModal'
|
|
||||||
import Tokens from './Tokens'
|
import Tokens from './Tokens'
|
||||||
import { BALANCE_TABLE_ASSET_ID, type BalanceRow, generateColumns, getBalanceData } from './dataFetcher'
|
import { BALANCE_TABLE_ASSET_ID, type BalanceRow, generateColumns, getBalanceData } from './dataFetcher'
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
@ -22,36 +21,47 @@ import { type Column, cellWidth } from '~/components/Table/TableHead'
|
||||||
import Button from '~/components/layout/Button'
|
import Button from '~/components/layout/Button'
|
||||||
import ButtonLink from '~/components/layout/ButtonLink'
|
import ButtonLink from '~/components/layout/ButtonLink'
|
||||||
import Col from '~/components/layout/Col'
|
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 Row from '~/components/layout/Row'
|
||||||
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
|
import type { BalanceCurrencyType } from '~/logic/currencyValues/store/model/currencyValues'
|
||||||
import { type Token } from '~/logic/tokens/store/model/token'
|
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 { BALANCE_TABLE_BALANCE_ID, BALANCE_TABLE_VALUE_ID } from '~/routes/safe/components/Balances/dataFetcher'
|
||||||
import DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
|
import DropdownCurrency from '~/routes/safe/components/DropdownCurrency'
|
||||||
|
import { history } from '~/store'
|
||||||
|
|
||||||
export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn'
|
export const MANAGE_TOKENS_BUTTON_TEST_ID = 'manage-tokens-btn'
|
||||||
export const BALANCE_ROW_TEST_ID = 'balance-row'
|
export const BALANCE_ROW_TEST_ID = 'balance-row'
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showToken: boolean,
|
erc721Enabled: boolean,
|
||||||
showReceive: boolean,
|
subMenuOptions: string[],
|
||||||
sendFunds: Object,
|
sendFunds: Object,
|
||||||
|
showCoins: boolean,
|
||||||
|
showCollectibles: boolean,
|
||||||
|
showReceive: boolean,
|
||||||
|
showToken: boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
classes: Object,
|
activateTokensByBalance: Function,
|
||||||
granted: boolean,
|
|
||||||
tokens: List<Token>,
|
|
||||||
activeTokens: List<Token>,
|
activeTokens: List<Token>,
|
||||||
blacklistedTokens: List<Token>,
|
blacklistedTokens: List<Token>,
|
||||||
activateTokensByBalance: Function,
|
classes: Object,
|
||||||
fetchTokens: Function,
|
|
||||||
safeAddress: string,
|
|
||||||
safeName: string,
|
|
||||||
ethBalance: string,
|
|
||||||
createTransaction: Function,
|
createTransaction: Function,
|
||||||
currencySelected: string,
|
currencySelected: string,
|
||||||
fetchCurrencyValues: Function,
|
|
||||||
currencyValues: BalanceCurrencyType[],
|
currencyValues: BalanceCurrencyType[],
|
||||||
|
ethBalance: string,
|
||||||
|
featuresEnabled: string[],
|
||||||
|
fetchCurrencyValues: Function,
|
||||||
|
fetchTokens: Function,
|
||||||
|
granted: boolean,
|
||||||
|
safeAddress: string,
|
||||||
|
safeName: string,
|
||||||
|
tokens: List<Token>,
|
||||||
}
|
}
|
||||||
|
|
||||||
type Action = 'Token' | 'Send' | 'Receive'
|
type Action = 'Token' | 'Send' | 'Receive'
|
||||||
|
@ -60,20 +70,58 @@ class Balances extends React.Component<Props, State> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
|
erc721Enabled: false,
|
||||||
|
subMenuOptions: [],
|
||||||
showToken: false,
|
showToken: false,
|
||||||
sendFunds: {
|
sendFunds: {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
selectedToken: undefined,
|
selectedToken: undefined,
|
||||||
},
|
},
|
||||||
|
showCoins: true,
|
||||||
|
showCollectibles: false,
|
||||||
showReceive: false,
|
showReceive: false,
|
||||||
}
|
}
|
||||||
props.fetchTokens()
|
props.fetchTokens()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isCoinsLocation = /\/balances\/?$/
|
||||||
|
static isCollectiblesLocation = /\/balances\/collectibles$/
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
const { activateTokensByBalance, fetchCurrencyValues, safeAddress } = this.props
|
||||||
fetchCurrencyValues(safeAddress)
|
fetchCurrencyValues(safeAddress)
|
||||||
activateTokensByBalance(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) => () => {
|
onShow = (action: Action) => () => {
|
||||||
|
@ -103,7 +151,7 @@ class Balances extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { sendFunds, showReceive, showToken } = this.state
|
const { erc721Enabled, sendFunds, showCoins, showCollectibles, showReceive, showToken, subMenuOptions } = this.state
|
||||||
const {
|
const {
|
||||||
activeTokens,
|
activeTokens,
|
||||||
blacklistedTokens,
|
blacklistedTokens,
|
||||||
|
@ -125,10 +173,34 @@ class Balances extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row align="center" className={classes.message}>
|
<Row align="center" className={classes.controls}>
|
||||||
<Col end="sm" xs={12}>
|
<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 />
|
<DropdownCurrency />
|
||||||
<ButtonLink onClick={this.onShow('Token')} size="lg" testId="manage-tokens-btn">
|
<ButtonLink
|
||||||
|
className={classes.manageTokensButton}
|
||||||
|
onClick={this.onShow('Token')}
|
||||||
|
size="lg"
|
||||||
|
testId="manage-tokens-btn"
|
||||||
|
>
|
||||||
Manage List
|
Manage List
|
||||||
</ButtonLink>
|
</ButtonLink>
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -145,8 +217,11 @@ class Balances extends React.Component<Props, State> {
|
||||||
tokens={tokens}
|
tokens={tokens}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{showCoins && (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
@ -225,6 +300,8 @@ class Balances extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
)}
|
||||||
|
{erc721Enabled && showCollectibles && <Collectibles />}
|
||||||
<SendModal
|
<SendModal
|
||||||
activeScreenType="sendFunds"
|
activeScreenType="sendFunds"
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
|
|
|
@ -1,17 +1,66 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { md, sm, xs } from '~/theme/variables'
|
import { md, screenSm, secondary, sm, xs } from '~/theme/variables'
|
||||||
|
|
||||||
export const styles = (theme: Object) => ({
|
export const styles = (theme: Object) => ({
|
||||||
root: {
|
root: {
|
||||||
width: '20px',
|
|
||||||
marginRight: sm,
|
marginRight: sm,
|
||||||
|
width: '20px',
|
||||||
},
|
},
|
||||||
message: {
|
controls: {
|
||||||
margin: `${sm} 0`,
|
alignItems: 'center',
|
||||||
padding: `${md} 0`,
|
|
||||||
maxHeight: '54px',
|
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
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',
|
justifyContent: 'flex-end',
|
||||||
|
order: '2',
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
manageTokensButton: {
|
||||||
|
marginLeft: 'auto',
|
||||||
|
|
||||||
|
[`@media (min-width: ${screenSm}px)`]: {
|
||||||
|
marginLeft: '0',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actionIcon: {
|
actionIcon: {
|
||||||
marginRight: theme.spacing(1),
|
marginRight: theme.spacing(1),
|
||||||
|
|
|
@ -13,7 +13,6 @@ import { type Actions } from '../container/actions'
|
||||||
|
|
||||||
import Balances from './Balances'
|
import Balances from './Balances'
|
||||||
import Receive from './Balances/Receive'
|
import Receive from './Balances/Receive'
|
||||||
import SendModal from './Balances/SendModal'
|
|
||||||
import Settings from './Settings'
|
import Settings from './Settings'
|
||||||
import Transactions from './Transactions'
|
import Transactions from './Transactions'
|
||||||
import { AddressBookIcon } from './assets/AddressBookIcon'
|
import { AddressBookIcon } from './assets/AddressBookIcon'
|
||||||
|
@ -35,9 +34,9 @@ import Hairline from '~/components/layout/Hairline'
|
||||||
import Heading from '~/components/layout/Heading'
|
import Heading from '~/components/layout/Heading'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
|
||||||
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getEtherScanLink, getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import AddressBookTable from '~/routes/safe/components/AddressBook'
|
import AddressBookTable from '~/routes/safe/components/AddressBook'
|
||||||
|
import SendModal from '~/routes/safe/components/Balances/SendModal'
|
||||||
import { type SelectorProps } from '~/routes/safe/container/selector'
|
import { type SelectorProps } from '~/routes/safe/container/selector'
|
||||||
import { border } from '~/theme/variables'
|
import { border } from '~/theme/variables'
|
||||||
|
|
||||||
|
@ -107,23 +106,6 @@ const Layout = (props: Props) => {
|
||||||
onClose: null,
|
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 handleCallToRouter = (_, value) => {
|
||||||
const { history } = props
|
const { history } = props
|
||||||
|
|
||||||
|
@ -134,7 +116,7 @@ const Layout = (props: Props) => {
|
||||||
return <NoSafe provider={provider} text="Safe not found" />
|
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 etherScanLink = getEtherScanLink('address', address)
|
||||||
const web3Instance = getWeb3()
|
const web3Instance = getWeb3()
|
||||||
|
|
||||||
|
@ -176,7 +158,7 @@ const Layout = (props: Props) => {
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent=""
|
badgeContent=""
|
||||||
color="error"
|
color="error"
|
||||||
invisible={!needUpdate || !granted}
|
invisible={!safe.needsUpdate || !granted}
|
||||||
style={{ paddingRight: '10px' }}
|
style={{ paddingRight: '10px' }}
|
||||||
variant="dot"
|
variant="dot"
|
||||||
>
|
>
|
||||||
|
@ -187,7 +169,7 @@ const Layout = (props: Props) => {
|
||||||
const labelBalances = (
|
const labelBalances = (
|
||||||
<>
|
<>
|
||||||
<BalancesIcon />
|
<BalancesIcon />
|
||||||
Balances
|
Assets
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
const labelTransactions = (
|
const labelTransactions = (
|
||||||
|
@ -212,6 +194,18 @@ const Layout = (props: Props) => {
|
||||||
</React.Suspense>
|
</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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Block className={classes.container} margin="xl">
|
<Block className={classes.container} margin="xl">
|
||||||
|
@ -261,7 +255,7 @@ const Layout = (props: Props) => {
|
||||||
indicatorColor="secondary"
|
indicatorColor="secondary"
|
||||||
onChange={handleCallToRouter}
|
onChange={handleCallToRouter}
|
||||||
textColor="secondary"
|
textColor="secondary"
|
||||||
value={location.pathname}
|
value={tabsValue()}
|
||||||
variant="scrollable"
|
variant="scrollable"
|
||||||
>
|
>
|
||||||
<Tab
|
<Tab
|
||||||
|
@ -316,7 +310,7 @@ const Layout = (props: Props) => {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={`${match.path}/balances`}
|
path={`${match.path}/balances/:assetType?`}
|
||||||
render={() => (
|
render={() => (
|
||||||
<Balances
|
<Balances
|
||||||
activateTokensByBalance={activateTokensByBalance}
|
activateTokensByBalance={activateTokensByBalance}
|
||||||
|
@ -326,6 +320,7 @@ const Layout = (props: Props) => {
|
||||||
currencySelected={currencySelected}
|
currencySelected={currencySelected}
|
||||||
currencyValues={currencyValues}
|
currencyValues={currencyValues}
|
||||||
ethBalance={ethBalance}
|
ethBalance={ethBalance}
|
||||||
|
featuresEnabled={featuresEnabled}
|
||||||
fetchCurrencyValues={fetchCurrencyValues}
|
fetchCurrencyValues={fetchCurrencyValues}
|
||||||
fetchTokens={fetchTokens}
|
fetchTokens={fetchTokens}
|
||||||
granted={granted}
|
granted={granted}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { makeStyles } from '@material-ui/core/styles'
|
import { makeStyles } from '@material-ui/core/styles'
|
||||||
import { withSnackbar } from 'notistack'
|
import { withSnackbar } from 'notistack'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
@ -19,9 +19,9 @@ import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
import { getNotificationsFromTxType, showSnackbar } from '~/logic/notifications'
|
||||||
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
import { TX_NOTIFICATION_TYPES } from '~/logic/safe/transactions'
|
||||||
import { getSafeVersion } from '~/logic/safe/utils/safeVersion'
|
|
||||||
import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal'
|
import UpdateSafeModal from '~/routes/safe/components/Settings/UpdateSafeModal'
|
||||||
import { grantedSelector } from '~/routes/safe/container/selector'
|
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_INPUT_TEST_ID = 'safe-name-input'
|
||||||
export const SAFE_NAME_SUBMIT_BTN_TEST_ID = 'change-safe-name-btn'
|
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 = {
|
type Props = {
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
|
safeCurrentVersion: string,
|
||||||
safeName: string,
|
safeName: string,
|
||||||
|
safeNeedsUpdate: boolean,
|
||||||
updateSafe: Function,
|
updateSafe: Function,
|
||||||
enqueueSnackbar: Function,
|
enqueueSnackbar: Function,
|
||||||
createTransaction: Function,
|
createTransaction: Function,
|
||||||
|
@ -40,11 +42,19 @@ const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const SafeDetails = (props: Props) => {
|
const SafeDetails = (props: Props) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const [safeVersions, setSafeVersions] = React.useState({ current: null, latest: null, needUpdate: false })
|
|
||||||
const isUserOwner = useSelector(grantedSelector)
|
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 = () => {
|
const toggleModal = () => {
|
||||||
setModalOpen(prevOpen => !prevOpen)
|
setModalOpen(prevOpen => !prevOpen)
|
||||||
|
@ -61,19 +71,6 @@ const SafeDetails = (props: Props) => {
|
||||||
setModalOpen(true)
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<GnoForm onSubmit={handleSubmit}>
|
<GnoForm onSubmit={handleSubmit}>
|
||||||
|
@ -83,11 +80,11 @@ const SafeDetails = (props: Props) => {
|
||||||
<Heading tag="h2">Safe Version</Heading>
|
<Heading tag="h2">Safe Version</Heading>
|
||||||
<Row align="end" grow>
|
<Row align="end" grow>
|
||||||
<Paragraph className={classes.versionNumber}>
|
<Paragraph className={classes.versionNumber}>
|
||||||
{safeVersions.current}
|
{safeCurrentVersion}
|
||||||
{safeVersions.needUpdate && ` (there's a newer version: ${safeVersions.latest})`}
|
{safeNeedsUpdate && ` (there's a newer version: ${latestMasterContractVersion})`}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
</Row>
|
</Row>
|
||||||
{safeVersions.needUpdate && isUserOwner ? (
|
{safeNeedsUpdate && isUserOwner ? (
|
||||||
<Row align="end" grow>
|
<Row align="end" grow>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
<Button
|
<Button
|
||||||
|
@ -138,7 +135,7 @@ const SafeDetails = (props: Props) => {
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
<Modal description="Update Safe" handleClose={toggleModal} open={isModalOpen} title="Update Safe">
|
<Modal description="Update Safe" handleClose={toggleModal} open={isModalOpen} title="Update Safe">
|
||||||
<UpdateSafeModal createTransaction={createTransaction} onClose={toggleModal} safeAddress={safeAddress} />
|
<UpdateSafeModal onClose={toggleModal} safeAddress={safeAddress} />
|
||||||
</Modal>
|
</Modal>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,6 +3,8 @@ import IconButton from '@material-ui/core/IconButton'
|
||||||
import Close from '@material-ui/icons/Close'
|
import Close from '@material-ui/icons/Close'
|
||||||
import { withStyles } from '@material-ui/styles'
|
import { withStyles } from '@material-ui/styles'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { bindActionCreators } from 'redux'
|
||||||
|
|
||||||
import { styles } from './style'
|
import { styles } from './style'
|
||||||
|
|
||||||
|
@ -13,18 +15,19 @@ import Hairline from '~/components/layout/Hairline'
|
||||||
import Paragraph from '~/components/layout/Paragraph'
|
import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import { upgradeSafeToLatestVersion } from '~/logic/safe/utils/upgradeSafe'
|
import { upgradeSafeToLatestVersion } from '~/logic/safe/utils/upgradeSafe'
|
||||||
|
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: Function,
|
onClose: Function,
|
||||||
createTransaction: Function,
|
|
||||||
classes: Object,
|
classes: Object,
|
||||||
safeAddress: string,
|
safeAddress: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateSafeModal = ({ classes, createTransaction, onClose, safeAddress }: Props) => {
|
const UpdateSafeModal = ({ classes, onClose, safeAddress }: Props) => {
|
||||||
|
const dispatch = useDispatch()
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
// Call the update safe method
|
// Call the update safe method
|
||||||
await upgradeSafeToLatestVersion(safeAddress, createTransaction)
|
await upgradeSafeToLatestVersion(safeAddress, bindActionCreators(createTransaction, dispatch))
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@ import Paragraph from '~/components/layout/Paragraph'
|
||||||
import Row from '~/components/layout/Row'
|
import Row from '~/components/layout/Row'
|
||||||
import Span from '~/components/layout/Span'
|
import Span from '~/components/layout/Span'
|
||||||
import type { AddressBook } from '~/logic/addressBook/model/addressBook'
|
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 Owner } from '~/routes/safe/store/models/owner'
|
||||||
import type { Safe } from '~/routes/safe/store/models/safe'
|
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 = {
|
type State = {
|
||||||
showRemoveSafe: boolean,
|
showRemoveSafe: boolean,
|
||||||
menuOptionIndex: number,
|
menuOptionIndex: number,
|
||||||
needUpdate: boolean,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = Actions & {
|
type Props = Actions & {
|
||||||
|
@ -68,25 +66,9 @@ class Settings extends React.Component<Props, State> {
|
||||||
this.state = {
|
this.state = {
|
||||||
showRemoveSafe: false,
|
showRemoveSafe: false,
|
||||||
menuOptionIndex: 1,
|
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 => () => {
|
handleChange = menuOptionIndex => () => {
|
||||||
this.setState({ menuOptionIndex })
|
this.setState({ menuOptionIndex })
|
||||||
}
|
}
|
||||||
|
@ -148,7 +130,7 @@ class Settings extends React.Component<Props, State> {
|
||||||
<Badge
|
<Badge
|
||||||
badgeContent=" "
|
badgeContent=" "
|
||||||
color="error"
|
color="error"
|
||||||
invisible={!this.state.needUpdate || !granted}
|
invisible={!safe.needsUpdate || !granted}
|
||||||
style={{ paddingRight: '10px' }}
|
style={{ paddingRight: '10px' }}
|
||||||
variant="dot"
|
variant="dot"
|
||||||
>
|
>
|
||||||
|
@ -184,7 +166,9 @@ class Settings extends React.Component<Props, State> {
|
||||||
<SafeDetails
|
<SafeDetails
|
||||||
createTransaction={createTransaction}
|
createTransaction={createTransaction}
|
||||||
safeAddress={safeAddress}
|
safeAddress={safeAddress}
|
||||||
|
safeCurrentVersion={safe.currentVersion}
|
||||||
safeName={safeName}
|
safeName={safeName}
|
||||||
|
safeNeedsUpdate={safe.needsUpdate}
|
||||||
updateSafe={updateSafe}
|
updateSafe={updateSafe}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -21,6 +21,7 @@ type Props = {
|
||||||
const statusToIcon = {
|
const statusToIcon = {
|
||||||
success: OkIcon,
|
success: OkIcon,
|
||||||
cancelled: ErrorIcon,
|
cancelled: ErrorIcon,
|
||||||
|
failed: ErrorIcon,
|
||||||
awaiting_your_confirmation: AwaitingIcon,
|
awaiting_your_confirmation: AwaitingIcon,
|
||||||
awaiting_confirmations: AwaitingIcon,
|
awaiting_confirmations: AwaitingIcon,
|
||||||
awaiting_execution: AwaitingIcon,
|
awaiting_execution: AwaitingIcon,
|
||||||
|
@ -30,6 +31,7 @@ const statusToIcon = {
|
||||||
const statusToLabel = {
|
const statusToLabel = {
|
||||||
success: 'Success',
|
success: 'Success',
|
||||||
cancelled: 'Cancelled',
|
cancelled: 'Cancelled',
|
||||||
|
failed: 'Failed',
|
||||||
awaiting_your_confirmation: 'Awaiting your confirmation',
|
awaiting_your_confirmation: 'Awaiting your confirmation',
|
||||||
awaiting_confirmations: 'Awaiting confirmations',
|
awaiting_confirmations: 'Awaiting confirmations',
|
||||||
awaiting_execution: 'Awaiting execution',
|
awaiting_execution: 'Awaiting execution',
|
||||||
|
|
|
@ -23,6 +23,11 @@ export const styles = () => ({
|
||||||
color: error,
|
color: error,
|
||||||
border: `1px solid ${error}`,
|
border: `1px solid ${error}`,
|
||||||
},
|
},
|
||||||
|
failed: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
color: error,
|
||||||
|
border: `1px solid ${error}`,
|
||||||
|
},
|
||||||
awaiting_your_confirmation: {
|
awaiting_your_confirmation: {
|
||||||
backgroundColor: '#d4d5d3',
|
backgroundColor: '#d4d5d3',
|
||||||
color: disabled,
|
color: disabled,
|
||||||
|
|
|
@ -125,7 +125,7 @@ const TxsTable = ({
|
||||||
{autoColumns.map((column: Column) => (
|
{autoColumns.map((column: Column) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
align={column.align}
|
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"
|
component="td"
|
||||||
key={column.id}
|
key={column.id}
|
||||||
style={cellWidth(column.width)}
|
style={cellWidth(column.width)}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
// @flow
|
// @flow
|
||||||
import loadAddressBookFromStorage from '~/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
import loadAddressBookFromStorage from '~/logic/addressBook/store/actions/loadAddressBookFromStorage'
|
||||||
import { updateAddressBookEntry } from '~/logic/addressBook/store/actions/updateAddressBookEntry'
|
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 fetchCurrencyValues from '~/logic/currencyValues/store/actions/fetchCurrencyValues'
|
||||||
import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe'
|
import addViewedSafe from '~/logic/currentSession/store/actions/addViewedSafe'
|
||||||
import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
|
import activateTokensByBalance from '~/logic/tokens/store/actions/activateTokensByBalance'
|
||||||
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
import fetchTokens from '~/logic/tokens/store/actions/fetchTokens'
|
||||||
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
import createTransaction from '~/routes/safe/store/actions/createTransaction'
|
||||||
import fetchEtherBalance from '~/routes/safe/store/actions/fetchEtherBalance'
|
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 fetchSafe, { checkAndUpdateSafe } from '~/routes/safe/store/actions/fetchSafe'
|
||||||
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
import fetchTokenBalances from '~/routes/safe/store/actions/fetchTokenBalances'
|
||||||
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
import fetchTransactions from '~/routes/safe/store/actions/fetchTransactions'
|
||||||
|
@ -19,9 +21,11 @@ export type Actions = {
|
||||||
createTransaction: typeof createTransaction,
|
createTransaction: typeof createTransaction,
|
||||||
fetchTransactions: typeof fetchTransactions,
|
fetchTransactions: typeof fetchTransactions,
|
||||||
updateSafe: typeof updateSafe,
|
updateSafe: typeof updateSafe,
|
||||||
|
fetchCollectibles: typeof fetchCollectibles,
|
||||||
fetchTokens: typeof fetchTokens,
|
fetchTokens: typeof fetchTokens,
|
||||||
processTransaction: typeof processTransaction,
|
processTransaction: typeof processTransaction,
|
||||||
fetchEtherBalance: typeof fetchEtherBalance,
|
fetchEtherBalance: typeof fetchEtherBalance,
|
||||||
|
fetchLatestMasterContractVersion: typeof fetchLatestMasterContractVersion,
|
||||||
activateTokensByBalance: typeof activateTokensByBalance,
|
activateTokensByBalance: typeof activateTokensByBalance,
|
||||||
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
|
checkAndUpdateSafeOwners: typeof checkAndUpdateSafe,
|
||||||
fetchCurrencyValues: typeof fetchCurrencyValues,
|
fetchCurrencyValues: typeof fetchCurrencyValues,
|
||||||
|
@ -35,11 +39,13 @@ export default {
|
||||||
fetchTokenBalances,
|
fetchTokenBalances,
|
||||||
createTransaction,
|
createTransaction,
|
||||||
processTransaction,
|
processTransaction,
|
||||||
|
fetchCollectibles,
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
fetchTransactions,
|
fetchTransactions,
|
||||||
activateTokensByBalance,
|
activateTokensByBalance,
|
||||||
updateSafe,
|
updateSafe,
|
||||||
fetchEtherBalance,
|
fetchEtherBalance,
|
||||||
|
fetchLatestMasterContractVersion,
|
||||||
fetchCurrencyValues,
|
fetchCurrencyValues,
|
||||||
checkAndUpdateSafeOwners: checkAndUpdateSafe,
|
checkAndUpdateSafeOwners: checkAndUpdateSafe,
|
||||||
loadAddressBook: loadAddressBookFromStorage,
|
loadAddressBook: loadAddressBookFromStorage,
|
||||||
|
|
|
@ -34,11 +34,15 @@ class SafeView extends React.Component<Props, State> {
|
||||||
|
|
||||||
intervalId: IntervalID
|
intervalId: IntervalID
|
||||||
|
|
||||||
|
longIntervalId: IntervalID
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
activeTokens,
|
activeTokens,
|
||||||
addViewedSafe,
|
addViewedSafe,
|
||||||
|
fetchCollectibles,
|
||||||
fetchCurrencyValues,
|
fetchCurrencyValues,
|
||||||
|
fetchLatestMasterContractVersion,
|
||||||
fetchSafe,
|
fetchSafe,
|
||||||
fetchTokenBalances,
|
fetchTokenBalances,
|
||||||
fetchTokens,
|
fetchTokens,
|
||||||
|
@ -47,10 +51,13 @@ class SafeView extends React.Component<Props, State> {
|
||||||
safeUrl,
|
safeUrl,
|
||||||
} = this.props
|
} = this.props
|
||||||
|
|
||||||
fetchSafe(safeUrl).then(() => {
|
fetchLatestMasterContractVersion()
|
||||||
|
.then(() => fetchSafe(safeUrl))
|
||||||
|
.then(() => {
|
||||||
// The safe needs to be loaded before fetching the transactions
|
// The safe needs to be loaded before fetching the transactions
|
||||||
fetchTransactions(safeUrl)
|
fetchTransactions(safeUrl)
|
||||||
addViewedSafe(safeUrl)
|
addViewedSafe(safeUrl)
|
||||||
|
fetchCollectibles()
|
||||||
})
|
})
|
||||||
fetchTokenBalances(safeUrl, activeTokens)
|
fetchTokenBalances(safeUrl, activeTokens)
|
||||||
// fetch tokens there to get symbols for tokens in TXs list
|
// fetch tokens there to get symbols for tokens in TXs list
|
||||||
|
@ -61,6 +68,10 @@ class SafeView extends React.Component<Props, State> {
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval(() => {
|
||||||
this.checkForUpdates()
|
this.checkForUpdates()
|
||||||
}, TIMEOUT)
|
}, TIMEOUT)
|
||||||
|
|
||||||
|
this.longIntervalId = setInterval(() => {
|
||||||
|
fetchCollectibles()
|
||||||
|
}, TIMEOUT * 3)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -78,6 +89,7 @@ class SafeView extends React.Component<Props, State> {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
clearInterval(this.intervalId)
|
clearInterval(this.intervalId)
|
||||||
|
clearInterval(this.longIntervalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
onShow = (action: Action) => () => {
|
onShow = (action: Action) => () => {
|
||||||
|
|
|
@ -63,6 +63,10 @@ const getTxStatus = (tx: Transaction, userAddress: string, safe: Safe): Transact
|
||||||
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
|
txStatus = !userConfirmed && userIsSafeOwner ? 'awaiting_your_confirmation' : 'awaiting_confirmations'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tx.isSuccessful === false) {
|
||||||
|
txStatus = 'failed'
|
||||||
|
}
|
||||||
|
|
||||||
return txStatus
|
return txStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,11 +102,11 @@ export const extendedSafeTokensSelector: Selector<GlobalState, RouterProps, List
|
||||||
map.set(tokenAddress, baseToken.set('balance', tokenBalance || '0'))
|
map.set(tokenAddress, baseToken.set('balance', tokenBalance || '0'))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
if (ethAsToken) {
|
if (ethAsToken) {
|
||||||
map.set(ethAsToken.address, ethAsToken)
|
return extendedTokens.set(ethAsToken.address, ethAsToken).toList()
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
return extendedTokens.toList()
|
return extendedTokens.toList()
|
||||||
},
|
},
|
||||||
|
|
|
@ -28,7 +28,7 @@ type CreateTransactionArgs = {
|
||||||
to: string,
|
to: string,
|
||||||
valueInWei: string,
|
valueInWei: string,
|
||||||
txData: string,
|
txData: string,
|
||||||
notifiedTransaction: NotifiedTransaction,
|
notifiedTransaction: $Values<NotifiedTransaction>,
|
||||||
enqueueSnackbar: Function,
|
enqueueSnackbar: Function,
|
||||||
closeSnackbar: Function,
|
closeSnackbar: Function,
|
||||||
txNonce?: number,
|
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 { getGnosisSafeInstanceAt } from '~/logic/contracts/safeContracts'
|
||||||
import { getLocalSafe, getSafeName } from '~/logic/safe/utils'
|
import { getLocalSafe, getSafeName } from '~/logic/safe/utils'
|
||||||
|
import { enabledFeatures, safeNeedsUpdate } from '~/logic/safe/utils/safeVersion'
|
||||||
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
import { sameAddress } from '~/logic/wallets/ethAddresses'
|
||||||
import { getBalanceInEtherOf, getWeb3 } from '~/logic/wallets/getWeb3'
|
import { getBalanceInEtherOf, getWeb3 } from '~/logic/wallets/getWeb3'
|
||||||
import addSafe from '~/routes/safe/store/actions/addSafe'
|
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 safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||||
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
const gnosisSafe = await getGnosisSafeInstanceAt(safeAddress)
|
||||||
const ethBalance = await getBalanceInEtherOf(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 threshold = Number(await gnosisSafe.getThreshold())
|
||||||
const nonce = Number(await gnosisSafe.nonce())
|
const nonce = Number(await gnosisSafe.nonce())
|
||||||
const owners = List(buildOwnersFrom(await gnosisSafe.getOwners(), await getLocalSafe(safeAddress)))
|
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 = {
|
const safe: SafeProps = {
|
||||||
address: safeAddress,
|
address: safeAddress,
|
||||||
|
@ -50,6 +54,9 @@ export const buildSafe = async (safeAdd: string, safeName: string) => {
|
||||||
owners,
|
owners,
|
||||||
ethBalance,
|
ethBalance,
|
||||||
nonce,
|
nonce,
|
||||||
|
currentVersion,
|
||||||
|
needsUpdate,
|
||||||
|
featuresEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
return safe
|
return safe
|
||||||
|
@ -93,11 +100,12 @@ export const checkAndUpdateSafe = (safeAdd: string) => async (dispatch: ReduxDis
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// 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 {
|
try {
|
||||||
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
const safeAddress = getWeb3().utils.toChecksumAddress(safeAdd)
|
||||||
const safeName = (await getSafeName(safeAddress)) || 'LOADED SAFE'
|
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))
|
dispatch(addSafe(safeProps))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -52,6 +52,7 @@ type TxServiceModel = {
|
||||||
executionDate: ?string,
|
executionDate: ?string,
|
||||||
confirmations: ConfirmationServiceModel[],
|
confirmations: ConfirmationServiceModel[],
|
||||||
isExecuted: boolean,
|
isExecuted: boolean,
|
||||||
|
isSuccessful: boolean,
|
||||||
transactionHash: ?string,
|
transactionHash: ?string,
|
||||||
creationTx?: boolean,
|
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 modifySettingsTx = sameAddress(tx.to, safeAddress) && Number(tx.value) === 0 && !!tx.data
|
||||||
const cancellationTx = 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 isMultiSendTx = isMultisendTransaction(tx.data, Number(tx.value))
|
||||||
const isUpgradeTx = isMultiSendTx && isUpgradeTransaction(tx.data)
|
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
|
let refundParams = null
|
||||||
if (tx.gasPrice > 0) {
|
if (tx.gasPrice > 0) {
|
||||||
|
@ -163,6 +167,7 @@ export const buildTransactionFrom = async (safeAddress: string, tx: TxServiceMod
|
||||||
refundReceiver: tx.refundReceiver,
|
refundReceiver: tx.refundReceiver,
|
||||||
refundParams,
|
refundParams,
|
||||||
isExecuted: tx.isExecuted,
|
isExecuted: tx.isExecuted,
|
||||||
|
isSuccessful: tx.isSuccessful,
|
||||||
submissionDate: tx.submissionDate,
|
submissionDate: tx.submissionDate,
|
||||||
executor: tx.executor,
|
executor: tx.executor,
|
||||||
executionDate: tx.executionDate,
|
executionDate: tx.executionDate,
|
||||||
|
|
|
@ -11,10 +11,10 @@ import { loadFromStorage } from '~/utils/storage'
|
||||||
|
|
||||||
const loadSafesFromStorage = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
const loadSafesFromStorage = () => async (dispatch: ReduxDispatch<GlobalState>) => {
|
||||||
try {
|
try {
|
||||||
const safes: ?{ [string]: SafeProps } = await loadFromStorage(SAFES_KEY)
|
const safes: ?{ [key: string]: SafeProps } = await loadFromStorage(SAFES_KEY)
|
||||||
|
|
||||||
if (safes) {
|
if (safes) {
|
||||||
Object.values(safes).forEach((safeProps: SafeProps) => {
|
Object.values(safes).forEach(safeProps => {
|
||||||
dispatch(addSafe(buildSafe(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,
|
nonce: number,
|
||||||
latestIncomingTxBlock?: number,
|
latestIncomingTxBlock?: number,
|
||||||
recurringUser?: boolean,
|
recurringUser?: boolean,
|
||||||
|
currentVersion: string,
|
||||||
|
needsUpdate: boolean,
|
||||||
|
featuresEnabled: string[],
|
||||||
}
|
}
|
||||||
|
|
||||||
const SafeRecord: RecordFactory<SafeProps> = Record({
|
const SafeRecord: RecordFactory<SafeProps> = Record({
|
||||||
|
@ -30,6 +33,9 @@ const SafeRecord: RecordFactory<SafeProps> = Record({
|
||||||
nonce: 0,
|
nonce: 0,
|
||||||
latestIncomingTxBlock: 0,
|
latestIncomingTxBlock: 0,
|
||||||
recurringUser: undefined,
|
recurringUser: undefined,
|
||||||
|
currentVersion: '',
|
||||||
|
needsUpdate: false,
|
||||||
|
featuresEnabled: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
export type Safe = RecordOf<SafeProps>
|
export type Safe = RecordOf<SafeProps>
|
||||||
|
|
|
@ -20,6 +20,7 @@ export type TransactionStatus =
|
||||||
| 'awaiting_your_confirmation'
|
| 'awaiting_your_confirmation'
|
||||||
| 'awaiting_confirmations'
|
| 'awaiting_confirmations'
|
||||||
| 'success'
|
| 'success'
|
||||||
|
| 'failed'
|
||||||
| 'cancelled'
|
| 'cancelled'
|
||||||
| 'awaiting_execution'
|
| 'awaiting_execution'
|
||||||
| 'pending'
|
| 'pending'
|
||||||
|
@ -39,6 +40,7 @@ export type TransactionProps = {
|
||||||
gasToken: string,
|
gasToken: string,
|
||||||
refundReceiver: string,
|
refundReceiver: string,
|
||||||
isExecuted: boolean,
|
isExecuted: boolean,
|
||||||
|
isSuccessful: boolean,
|
||||||
submissionDate: ?string,
|
submissionDate: ?string,
|
||||||
executionDate: ?string,
|
executionDate: ?string,
|
||||||
symbol: string,
|
symbol: string,
|
||||||
|
@ -75,6 +77,7 @@ export const makeTransaction: RecordFactory<TransactionProps> = Record({
|
||||||
gasToken: ZERO_ADDRESS,
|
gasToken: ZERO_ADDRESS,
|
||||||
refundReceiver: ZERO_ADDRESS,
|
refundReceiver: ZERO_ADDRESS,
|
||||||
isExecuted: false,
|
isExecuted: false,
|
||||||
|
isSuccessful: true,
|
||||||
submissionDate: '',
|
submissionDate: '',
|
||||||
executor: '',
|
executor: '',
|
||||||
executionDate: '',
|
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 { REMOVE_SAFE_OWNER } from '~/routes/safe/store/actions/removeSafeOwner'
|
||||||
import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner'
|
import { REPLACE_SAFE_OWNER } from '~/routes/safe/store/actions/replaceSafeOwner'
|
||||||
import { SET_DEFAULT_SAFE } from '~/routes/safe/store/actions/setDefaultSafe'
|
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 } from '~/routes/safe/store/actions/updateSafe'
|
||||||
import { UPDATE_SAFE_THRESHOLD } from '~/routes/safe/store/actions/updateSafeThreshold'
|
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'
|
import SafeRecord, { type SafeProps } from '~/routes/safe/store/models/safe'
|
||||||
|
|
||||||
export const SAFE_REDUCER_ID = 'safes'
|
export const SAFE_REDUCER_ID = 'safes'
|
||||||
|
@ -21,11 +22,8 @@ export const SAFE_REDUCER_ID = 'safes'
|
||||||
export type SafeReducerState = Map<string, *>
|
export type SafeReducerState = Map<string, *>
|
||||||
|
|
||||||
export const buildSafe = (storedSafe: SafeProps) => {
|
export const buildSafe = (storedSafe: SafeProps) => {
|
||||||
const names = storedSafe.owners.map((owner: OwnerProps) => owner.name)
|
const names = storedSafe.owners.map(owner => owner.name)
|
||||||
const addresses = storedSafe.owners.map((owner: OwnerProps) => {
|
const addresses = storedSafe.owners.map(owner => getWeb3().utils.toChecksumAddress(owner.address))
|
||||||
const checksumed = getWeb3().utils.toChecksumAddress(owner.address)
|
|
||||||
return checksumed
|
|
||||||
})
|
|
||||||
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
const owners = buildOwnersFrom(Array.from(names), Array.from(addresses))
|
||||||
const activeTokens = Set(storedSafe.activeTokens)
|
const activeTokens = Set(storedSafe.activeTokens)
|
||||||
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
const blacklistedTokens = Set(storedSafe.blacklistedTokens)
|
||||||
|
@ -53,7 +51,7 @@ export default handleActions<SafeReducerState, *>(
|
||||||
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
[ACTIVATE_TOKEN_FOR_ALL_SAFES]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||||
const tokenAddress = action.payload
|
const tokenAddress = action.payload
|
||||||
|
|
||||||
const newState = state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map
|
map
|
||||||
.get('safes')
|
.get('safes')
|
||||||
.keySeq()
|
.keySeq()
|
||||||
|
@ -64,8 +62,6 @@ export default handleActions<SafeReducerState, *>(
|
||||||
map.updateIn(['safes', safeAddress], prevSafe => prevSafe.merge({ activeTokens }))
|
map.updateIn(['safes', safeAddress], prevSafe => prevSafe.merge({ activeTokens }))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return newState
|
|
||||||
},
|
},
|
||||||
[ADD_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
[ADD_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState => {
|
||||||
const { safe }: { safe: SafeProps } = action.payload
|
const { safe }: { safe: SafeProps } = action.payload
|
||||||
|
@ -132,10 +128,14 @@ export default handleActions<SafeReducerState, *>(
|
||||||
},
|
},
|
||||||
[SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState =>
|
[SET_DEFAULT_SAFE]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState =>
|
||||||
state.set('defaultSafe', action.payload),
|
state.set('defaultSafe', action.payload),
|
||||||
|
[SET_LATEST_MASTER_CONTRACT_VERSION]: (state: SafeReducerState, action: ActionType<Function>): SafeReducerState =>
|
||||||
|
state.set('latestMasterContractVersion', action.payload),
|
||||||
},
|
},
|
||||||
Map({
|
Map({
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
defaultSafe: undefined,
|
defaultSafe: undefined,
|
||||||
safes: Map(),
|
safes: Map(),
|
||||||
|
// $FlowFixMe
|
||||||
|
latestMasterContractVersion: '',
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
@ -52,6 +52,14 @@ export const defaultSafeSelector: OutputSelector<GlobalState, {}, string> = crea
|
||||||
(safeState: Map<string, *>): string => safeState.get('defaultSafe'),
|
(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 transactionsSelector = (state: GlobalState): TransactionsState => state[TRANSACTIONS_REDUCER_ID]
|
||||||
|
|
||||||
const cancellationTransactionsSelector = (state: GlobalState): CancelTransactionsState =>
|
const cancellationTransactionsSelector = (state: GlobalState): CancelTransactionsState =>
|
||||||
|
|
|
@ -6,6 +6,14 @@ import thunk from 'redux-thunk'
|
||||||
|
|
||||||
import addressBookMiddleware from '~/logic/addressBook/store/middleware/addressBookMiddleware'
|
import addressBookMiddleware from '~/logic/addressBook/store/middleware/addressBookMiddleware'
|
||||||
import addressBook, { ADDRESS_BOOK_REDUCER_ID } from '~/logic/addressBook/store/reducer/addressBook'
|
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 cookies, { COOKIES_REDUCER_ID } from '~/logic/cookies/store/reducer/cookies'
|
||||||
import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
|
import currencyValues, { CURRENCY_VALUES_KEY } from '~/logic/currencyValues/store/reducer/currencyValues'
|
||||||
import currentSession, {
|
import currentSession, {
|
||||||
|
@ -54,6 +62,8 @@ export type GlobalState = {
|
||||||
providers: ProviderState,
|
providers: ProviderState,
|
||||||
router: RouterHistory,
|
router: RouterHistory,
|
||||||
safes: SafeState,
|
safes: SafeState,
|
||||||
|
nftAssets: NFTAssetsState,
|
||||||
|
nftTokens: NFTTokensState,
|
||||||
tokens: TokensState,
|
tokens: TokensState,
|
||||||
transactions: TransactionsState,
|
transactions: TransactionsState,
|
||||||
cancellationTransactions: CancelTransactionsState,
|
cancellationTransactions: CancelTransactionsState,
|
||||||
|
@ -68,6 +78,8 @@ const reducers: CombinedReducer<GlobalState, *> = combineReducers({
|
||||||
router: connectRouter(history),
|
router: connectRouter(history),
|
||||||
[PROVIDER_REDUCER_ID]: provider,
|
[PROVIDER_REDUCER_ID]: provider,
|
||||||
[SAFE_REDUCER_ID]: safe,
|
[SAFE_REDUCER_ID]: safe,
|
||||||
|
[NFT_ASSETS_REDUCER_ID]: nftAssetReducer,
|
||||||
|
[NFT_TOKENS_REDUCER_ID]: nftTokensReducer,
|
||||||
[TOKEN_REDUCER_ID]: tokens,
|
[TOKEN_REDUCER_ID]: tokens,
|
||||||
[TRANSACTIONS_REDUCER_ID]: transactions,
|
[TRANSACTIONS_REDUCER_ID]: transactions,
|
||||||
[CANCELLATION_TRANSACTIONS_REDUCER_ID]: cancellationTransactions,
|
[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: {
|
MuiTablePagination: {
|
||||||
toolbar: {
|
toolbar: {
|
||||||
paddingRight: '15px',
|
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"
|
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
|
||||||
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
|
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":
|
"@portis/eth-json-rpc-middleware@^4.1.2":
|
||||||
version "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"
|
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"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a"
|
||||||
integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==
|
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:
|
abi-decoder@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/abi-decoder/-/abi-decoder-1.2.0.tgz#c42882dbb91b444805f0cd203a87a5cc3c22f4a8"
|
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"
|
resolved "https://registry.yarnpkg.com/app-module-path/-/app-module-path-2.2.0.tgz#641aa55dfb7d6a6f0a8141c4b9c0aa50b6c24dd5"
|
||||||
integrity sha1-ZBqlXft9am8KgUHEucCqULbCTdU=
|
integrity sha1-ZBqlXft9am8KgUHEucCqULbCTdU=
|
||||||
|
|
||||||
aproba@^1.0.3, aproba@^1.1.1:
|
aproba@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
|
||||||
integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==
|
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:
|
argparse@^1.0.7:
|
||||||
version "1.0.10"
|
version "1.0.10"
|
||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
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"
|
resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
|
||||||
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
|
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:
|
async@2.6.1:
|
||||||
version "2.6.1"
|
version "2.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
|
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"
|
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||||
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
|
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:
|
constants-browserify@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
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:
|
dependencies:
|
||||||
ms "2.0.0"
|
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"
|
version "3.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b"
|
||||||
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==
|
||||||
|
@ -5464,11 +5456,6 @@ deep-equal@^1.0.1, deep-equal@~1.1.1:
|
||||||
object-keys "^1.1.1"
|
object-keys "^1.1.1"
|
||||||
regexp.prototype.flags "^1.2.0"
|
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:
|
deep-is@~0.1.3:
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
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"
|
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
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:
|
depd@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
|
@ -5616,11 +5598,6 @@ detect-installed@^2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-installed-path "^2.0.3"
|
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:
|
detect-newline@2.X:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2"
|
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"
|
ethereumjs-wallet "0.6.3"
|
||||||
web3 "1.2.1"
|
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:
|
gensync@^1.0.0-beta.1:
|
||||||
version "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"
|
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:
|
dependencies:
|
||||||
has-symbol-support-x "^1.4.1"
|
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:
|
has-value@^0.3.1:
|
||||||
version "0.3.1"
|
version "0.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f"
|
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"
|
cheerio "0.20.0"
|
||||||
color-logger "0.0.3"
|
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"
|
version "0.4.24"
|
||||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||||
|
@ -8762,13 +8720,6 @@ iferr@^0.1.5:
|
||||||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||||
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
|
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:
|
ignore@^3.3.5:
|
||||||
version "3.3.10"
|
version "3.3.10"
|
||||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043"
|
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"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
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"
|
version "1.3.5"
|
||||||
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
|
||||||
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
|
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"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
|
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:
|
negotiator@0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
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"
|
shellwords "^0.1.1"
|
||||||
which "^1.3.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:
|
node-releases@^1.1.47, node-releases@^1.1.50:
|
||||||
version "1.1.51"
|
version "1.1.51"
|
||||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.51.tgz#70d0e054221343d2966006bfbd4d98622cc00bd0"
|
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:
|
dependencies:
|
||||||
semver "^6.3.0"
|
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:
|
normalize-hex@0.0.2:
|
||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/normalize-hex/-/normalize-hex-0.0.2.tgz#5491c43759db2f06b7168d8419f4925c271ab27e"
|
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"
|
prop-types "^15.7.2"
|
||||||
react-is "^16.9.0"
|
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:
|
npm-programmatic@0.0.6:
|
||||||
version "0.0.6"
|
version "0.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/npm-programmatic/-/npm-programmatic-0.0.6.tgz#3c8f4dbb210efd65b99ee6a5ac76f27b4d5d6b78"
|
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:
|
dependencies:
|
||||||
path-key "^3.0.0"
|
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:
|
nth-check@^1.0.2, nth-check@~1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
|
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"
|
lcid "^2.0.0"
|
||||||
mem "^4.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"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||||
integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=
|
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:
|
p-cancelable@^0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa"
|
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"
|
iconv-lite "0.4.24"
|
||||||
unpipe "1.0.0"
|
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:
|
react-dev-utils@^10.0.0:
|
||||||
version "10.2.0"
|
version "10.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.2.0.tgz#b11cc48aa2be2502fb3c27a50d1dfa95cfa9dfe0"
|
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"
|
normalize-package-data "^2.3.2"
|
||||||
path-type "^3.0.0"
|
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"
|
version "2.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||||
|
@ -14243,7 +14112,7 @@ rimraf@2.6.3, rimraf@~2.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
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"
|
version "2.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
|
||||||
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==
|
||||||
|
@ -14390,7 +14259,7 @@ sane@^4.0.2, sane@^4.0.3:
|
||||||
minimist "^1.1.1"
|
minimist "^1.1.1"
|
||||||
walker "~1.0.5"
|
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"
|
version "1.2.4"
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
@ -14658,7 +14527,7 @@ servify@^0.1.12:
|
||||||
request "^2.79.0"
|
request "^2.79.0"
|
||||||
xhr "^2.3.3"
|
xhr "^2.3.3"
|
||||||
|
|
||||||
set-blocking@^2.0.0, set-blocking@~2.0.0:
|
set-blocking@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||||
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
|
||||||
|
@ -15241,7 +15110,7 @@ string-width@^1.0.1:
|
||||||
is-fullwidth-code-point "^1.0.0"
|
is-fullwidth-code-point "^1.0.0"
|
||||||
strip-ansi "^3.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"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
|
||||||
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
|
||||||
|
@ -15421,7 +15290,7 @@ strip-indent@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
min-indent "^1.0.0"
|
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"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||||
|
@ -15625,7 +15494,7 @@ tar-stream@^1.5.2:
|
||||||
to-buffer "^1.1.1"
|
to-buffer "^1.1.1"
|
||||||
xtend "^4.0.0"
|
xtend "^4.0.0"
|
||||||
|
|
||||||
tar@^4.0.2, tar@^4.4.2:
|
tar@^4.0.2:
|
||||||
version "4.4.13"
|
version "4.4.13"
|
||||||
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525"
|
||||||
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==
|
||||||
|
@ -17347,9 +17216,9 @@ web3-provider-engine@^15.0.4:
|
||||||
xhr "^2.2.0"
|
xhr "^2.2.0"
|
||||||
xtend "^4.0.1"
|
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"
|
version "14.0.6"
|
||||||
resolved "git+https://github.com/trufflesuite/provider-engine.git#3538c60bc4836b73ccae1ac3f64c8fed8ef19c1a"
|
resolved "https://github.com/trufflesuite/provider-engine#3538c60bc4836b73ccae1ac3f64c8fed8ef19c1a"
|
||||||
dependencies:
|
dependencies:
|
||||||
async "^2.5.0"
|
async "^2.5.0"
|
||||||
backoff "^2.5.0"
|
backoff "^2.5.0"
|
||||||
|
@ -17789,13 +17658,6 @@ which@^2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
isexe "^2.0.0"
|
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:
|
window-size@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||||
|
|
Loading…
Reference in New Issue