Collectibles utils

This commit is contained in:
Mati Dastugue 2020-12-09 11:33:22 -03:00
parent 433fe9be79
commit 878c32c4a3
2 changed files with 211 additions and 0 deletions

View File

@ -0,0 +1,68 @@
import { getTransferMethodByContractAddress } from 'src/logic/collectibles/utils'
jest.mock('src/config', () => {
// Require the original module to not be mocked...
const originalModule = jest.requireActual('src/config')
return {
__esModule: true, // Use it when dealing with esModules
...originalModule,
getNetworkId: jest.fn().mockReturnValue(4),
}
})
describe('getTransferMethodByContractAddress', () => {
const config = require('src/config')
afterAll(() => {
jest.unmock('src/config')
})
it(`should return "transfer" method, if CK address is provided for MAINNET`, () => {
// Given
config.getNetworkId.mockReturnValue(1)
const contractAddress = '0x06012c8cf97bead5deae237070f9587f8e7a266d'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('transfer')
})
it(`should return "transfer" method, if CK address is provided for RINKEBY`, () => {
// Given
config.getNetworkId.mockReturnValue(4)
const contractAddress = '0x16baf0de678e52367adc69fd067e5edd1d33e3bf'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('transfer')
})
it(`should return "0x42842e0e" method, if CK address is provided any other network`, () => {
// Given
config.getNetworkId.mockReturnValue(100)
const contractAddress = '0x06012c8cf97bead5deae237070f9587f8e7a266d'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('0x42842e0e')
})
it(`should return "0x42842e0e" method, if non-CK address is provided`, () => {
// Given
config.getNetworkId.mockReturnValue(4)
const contractAddress = '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85'
// When
const selectedMethod = getTransferMethodByContractAddress(contractAddress)
// Then
expect(selectedMethod).toBe('0x42842e0e')
})
})

View File

@ -0,0 +1,143 @@
import { getNetworkId, getNetworkInfo } from 'src/config'
import { ETHEREUM_NETWORK } from 'src/config/networks/network.d'
import { nftAssetsListAddressesSelector } from 'src/logic/collectibles/store/selectors'
import { TxServiceModel } from 'src/logic/safe/store/actions/transactions/fetchTransactions/loadOutgoingTransactions'
import { TOKEN_TRANSFER_METHODS_NAMES } from 'src/logic/safe/store/models/types/transactions.d'
import { getERC721TokenContract, getStandardTokenContract } from 'src/logic/tokens/store/actions/fetchTokens'
import { sameAddress } from 'src/logic/wallets/ethAddresses'
import { CollectibleTx } from 'src/routes/safe/components/Balances/SendModal/screens/ReviewCollectible'
import { store } from 'src/store'
import { sameString } from 'src/utils/strings'
// CryptoKitties Contract Addresses by network
// This is an exception made for a popular NFT that's not ERC721 standard-compatible,
// so we can allow the user to transfer the assets by using `transferFrom` instead of
// the standard `safeTransferFrom` method.
export const CK_ADDRESS = {
[ETHEREUM_NETWORK.MAINNET]: '0x06012c8cf97bead5deae237070f9587f8e7a266d',
[ETHEREUM_NETWORK.RINKEBY]: '0x16baf0de678e52367adc69fd067e5edd1d33e3bf',
}
// Note: xDAI ENS is missing, once we have it we need to add it here
const ENS_CONTRACT_ADDRESS = {
[ETHEREUM_NETWORK.MAINNET]: '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85',
[ETHEREUM_NETWORK.RINKEBY]: '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85',
[ETHEREUM_NETWORK.ENERGY_WEB_CHAIN]: '0x0A6d64413c07E10E890220BBE1c49170080C6Ca0',
[ETHEREUM_NETWORK.VOLTA]: '0xd7CeF70Ba7efc2035256d828d5287e2D285CD1ac',
}
// safeTransferFrom(address,address,uint256)
export const SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH = '42842e0e'
/**
* Verifies that a tx received by the transaction service is an ERC721 token-related transaction
* @param {TxServiceModel} tx
* @returns boolean
*/
export const isSendERC721Transaction = (tx: TxServiceModel): boolean => {
let hasERC721Transfer = false
if (tx.dataDecoded && sameString(tx.dataDecoded.method, TOKEN_TRANSFER_METHODS_NAMES.SAFE_TRANSFER_FROM)) {
hasERC721Transfer = tx.dataDecoded.parameters.findIndex((param) => sameString(param.name, 'tokenId')) !== -1
}
// Note: this is only valid with our current case (client rendering), if we move to server side rendering we need to refactor this
const state = store.getState()
const knownAssets = nftAssetsListAddressesSelector(state)
return knownAssets.includes(tx.to) || hasERC721Transfer
}
/**
* Returns the symbol of the provided ERC721 contract
* @param {string} contractAddress
* @returns Promise<string>
*/
export const getERC721Symbol = async (contractAddress: string): Promise<string> => {
let tokenSymbol = 'UNKNOWN'
try {
const ERC721token = await getERC721TokenContract()
const tokenInstance = await ERC721token.at(contractAddress)
tokenSymbol = await tokenInstance.symbol()
} catch (err) {
// If the contract address is an ENS token contract, we know that the ERC721 standard is not proper implemented
// The method symbol() is missing
if (isENSContract(contractAddress)) {
return 'ENS'
}
console.error(`Failed to retrieve token symbol for ERC721 token ${contractAddress}`)
}
return tokenSymbol
}
export const isENSContract = (contractAddress: string): boolean => {
const { id } = getNetworkInfo()
return sameAddress(contractAddress, ENS_CONTRACT_ADDRESS[id])
}
/**
* Verifies if the provided contract is a valid ERC721
* @param {string} contractAddress
* @returns boolean
*/
export const isERC721Contract = async (contractAddress: string): Promise<boolean> => {
const ERC721Token = await getStandardTokenContract()
let isERC721 = false
try {
await ERC721Token.at(contractAddress)
isERC721 = true
} catch (error) {
console.warn('Asset not found')
}
return isERC721
}
/**
* Returns a method identifier based on the asset specified and the current network
* @param {string} contractAddress
* @returns string
*/
export const getTransferMethodByContractAddress = (contractAddress: string): string => {
if (sameAddress(contractAddress, CK_ADDRESS[getNetworkId()])) {
// on mainnet `transferFrom` seems to work fine but we can assure that `transfer` will work on both networks
// so that's the reason why we're falling back to `transfer` for CryptoKitties
return 'transfer'
}
return `0x${SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH}`
}
/**
* Builds the encodedABI data for the transfer of an NFT token
* @param {CollectibleTx} tx
* @param {string} safeAddress
* @returns Promise<string>
*/
export const generateERC721TransferTxData = async (
tx: CollectibleTx,
safeAddress: string | undefined,
): Promise<string> => {
if (!safeAddress) {
throw new Error('Failed to build NFT transfer tx data. SafeAddress is not defined.')
}
const methodToCall = getTransferMethodByContractAddress(tx.assetAddress)
let transferParams = [tx.recipientAddress, tx.nftTokenId]
let NFTTokenContract
if (methodToCall.includes(SAFE_TRANSFER_FROM_WITHOUT_DATA_HASH)) {
// we add the `from` param for the `safeTransferFrom` method call
transferParams = [safeAddress, ...transferParams]
NFTTokenContract = await getERC721TokenContract()
} else {
// we fallback to an ERC20 Token contract whose ABI implements the `transfer` method
NFTTokenContract = await getStandardTokenContract()
}
const tokenInstance = await NFTTokenContract.at(tx.assetAddress)
return tokenInstance.contract.methods[methodToCall](...transferParams).encodeABI()
}