ethers/lib/utils#formatUnits TypeScript Examples
The following examples show how to use
ethers/lib/utils#formatUnits.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: utils.ts From mStable-apps with GNU Lesser General Public License v3.0 | 7 votes |
getProof = (balances: Record<string, string>, claimant: string): { balance: BigNumber; balanceSimple: number; proof: string[] } => {
let claimantLeaf: string | undefined
let claimantBalance: BigNumber | undefined
const leaves = Object.entries(balances).map(([_account, _balance]) => {
const balance = BigNumber.from(_balance)
const leaf = solidityKeccak256(['address', 'uint256'], [_account, balance.toString()])
if (!claimantLeaf && _account.toLowerCase() === claimant.toLowerCase()) {
claimantLeaf = leaf
claimantBalance = balance
}
return leaf
})
if (!claimantBalance || !claimantLeaf) throw new Error('Claim not found')
const tree = new MerkleTree(leaves, hashFn, { sort: true })
const proof = tree.getHexProof(claimantLeaf)
const balanceSimple = parseFloat(formatUnits(claimantBalance))
return { proof, balance: claimantBalance, balanceSimple }
}
Example #2
Source File: useBigNumberToNumber.ts From dxvote with GNU Affero General Public License v3.0 | 6 votes |
export default function useBigNumberToNumber(
number: BigNumber,
decimals: number,
precision: number = 2
) {
const stakeAmountParsed = useMemo(() => {
if (!number || !decimals) return null;
let formatted = Number.parseFloat(formatUnits(number, decimals));
return (
Math.round(formatted * Math.pow(10, precision)) / Math.pow(10, precision)
);
}, [number, decimals, precision]);
return stakeAmountParsed;
}
Example #3
Source File: index.ts From interface-v2 with GNU General Public License v3.0 | 6 votes |
export function formatCompact(
unformatted: number | string | BigNumber | BigNumberish | undefined | null,
decimals = 18,
maximumFractionDigits: number | undefined = 3,
maxPrecision: number | undefined = 4,
): string {
const formatter = Intl.NumberFormat('en', {
notation: 'compact',
maximumFractionDigits,
});
if (!unformatted) return '0';
if (unformatted === Infinity) return '∞';
let formatted: string | number = Number(unformatted);
if (unformatted instanceof BigNumber) {
formatted = Number(formatUnits(unformatted.toString(), decimals));
}
return formatter.format(Number(formatted.toPrecision(maxPrecision)));
}
Example #4
Source File: util.ts From noether with Apache License 2.0 | 6 votes |
formatCTSI = (value: BigNumberish) => {
return formatUnits(value, 18);
}
Example #5
Source File: MerkleClaimsProvider.tsx From mStable-apps with GNU Lesser General Public License v3.0 | 6 votes |
MerkleClaimsProvider: FC = ({ children }) => {
const account = useAccount()
const { merkleDrop: client } = useApolloClients()
const query = useMerkleDropAccountsQuery({ variables: { account: account as string }, skip: !account, client })
const merkleClaims = useMemo<FetchState<MerkleClaims>>(() => {
if (!query.data) return { fetching: true }
const value: MerkleClaims = Object.fromEntries(
query.data.accounts.map(({ merkleDrop: { id: merkleDrop, token }, claims }) => {
const tranches = claims.map(({ amount, tranche: { trancheId, uri } }) => ({
amount: BigNumber.from(amount),
trancheId,
uri,
}))
const totalUnclaimed = tranches.reduce((prev, { amount }) => prev.add(amount), BigNumber.from(0))
const totalUnclaimedSimple = parseFloat(formatUnits(totalUnclaimed))
return [merkleDrop, { address: merkleDrop, token, tranches, totalUnclaimed, totalUnclaimedSimple }]
}),
)
return { value }
}, [query.data])
return <merkleClaimsCtx.Provider value={merkleClaims}>{children}</merkleClaimsCtx.Provider>
}
Example #6
Source File: plantUmlStreamer.ts From tx2uml with MIT License | 6 votes |
writeTransactionDetails = (
plantUmlStream: Readable,
transaction: TransactionDetails,
options: PumlGenerationOptions = {}
): void => {
if (options.noTxDetails) {
return
}
plantUmlStream.push(`\nnote over ${participantId(transaction.from)}`)
if (transaction.error) {
plantUmlStream.push(
` ${FailureFillColor}\nError: ${transaction.error} \n`
)
} else {
// no error so will use default colour of tx details note
plantUmlStream.push("\n")
}
plantUmlStream.push(`Nonce: ${transaction.nonce.toLocaleString()}\n`)
plantUmlStream.push(
`Gas Price: ${formatUnits(transaction.gasPrice, "gwei")} Gwei\n`
)
plantUmlStream.push(
`Gas Limit: ${formatNumber(transaction.gasLimit.toString())}\n`
)
plantUmlStream.push(
`Gas Used: ${formatNumber(transaction.gasUsed.toString())}\n`
)
const txFeeInWei = transaction.gasUsed.mul(transaction.gasPrice)
const txFeeInEther = formatEther(txFeeInWei)
const tFeeInEtherFormatted = Number(txFeeInEther).toLocaleString()
plantUmlStream.push(`Tx Fee: ${tFeeInEtherFormatted} ETH\n`)
plantUmlStream.push("end note\n")
}
Example #7
Source File: VoteResults.tsx From dxvote with GNU Affero General Public License v3.0 | 5 votes |
VoteResultRow: React.FC<ResultRowProps> = ({
isPercent,
optionKey,
}) => {
const { guild_id: guildId, proposal_id: proposalId } = useParams<{
chain_name: string;
guild_id?: string;
proposal_id?: string;
}>();
const isReady = optionKey !== undefined;
const votingResults = useVotingResults();
const votingPowerPercent = useVotingPowerPercent(
votingResults?.options?.[optionKey],
votingResults?.totalLocked,
2
);
const theme = useTheme();
const { data: proposalMetadata } = useProposalMetadata(guildId, proposalId);
return (
<VotesRowWrapper>
<VoteOption>
<OptionBullet>
{isReady ? (
<Bullet color={theme?.colors?.votes?.[optionKey]} size={8} />
) : (
<Loading
loading
text
skeletonProps={{ circle: true, height: 16, width: 16 }}
/>
)}
</OptionBullet>
{isReady ? (
proposalMetadata?.voteOptions?.[optionKey] || 'Action ' + optionKey
) : (
<Loading loading text />
)}
</VoteOption>
{isReady && votingResults ? (
<span>
{isPercent
? `${formatUnits(votingResults?.options?.[optionKey] || 0)} ${
votingResults?.token?.symbol
}`
: `${votingPowerPercent}%`}
</span>
) : (
<Loading loading text skeletonProps={{ width: 50 }} />
)}
</VotesRowWrapper>
);
}
Example #8
Source File: formatBalance.ts From glide-frontend with GNU General Public License v3.0 | 5 votes |
formatBigNumber = (number: ethers.BigNumber, displayDecimals = 18, decimals = 18) => {
const remainder = number.mod(ethers.BigNumber.from(10).pow(decimals - displayDecimals))
return formatUnits(number.sub(remainder), decimals)
}
Example #9
Source File: formatBalance.ts From glide-frontend with GNU General Public License v3.0 | 5 votes |
formatBigNumberToFixed = (number: ethers.BigNumber, displayDecimals = 18, decimals = 18) => {
const formattedString = formatUnits(number, decimals)
return (+formattedString).toFixed(displayDecimals)
}
Example #10
Source File: index.ts From common-ts with MIT License | 5 votes |
formatGRT = (value: BigNumberish): string => formatUnits(value, 18)
Example #11
Source File: check-chainlink.ts From perpetual-protocol with GNU General Public License v3.0 | 5 votes |
export async function checkChainlink(address: string, env: HardhatRuntimeEnvironment): Promise<void> {
const AGGREGATOR_ABI = [
"function decimals() view returns (uint8)",
"function description() view returns (string memory)",
"function latestAnswer() external view returns (int256)",
]
const aggregator = await env.ethers.getContractAt(AGGREGATOR_ABI, address)
const chainlinkL1Artifact = await artifacts.readArtifact(ContractFullyQualifiedName.ChainlinkL1)
const chainlinkInterface = new Interface(chainlinkL1Artifact.abi)
const l2PriceFeedArtifact = await artifacts.readArtifact(ContractFullyQualifiedName.L2PriceFeed)
const l2PriceFeedInterface = new Interface(l2PriceFeedArtifact.abi)
const [decimals, pair, latestPrice] = await Promise.all([
aggregator.decimals(),
aggregator.description(),
aggregator.latestAnswer(),
])
const [baseSymbol, quoteSymbol] = pair.split("/").map((symbol: string) => symbol.trim())
const priceFeedKey = formatBytes32String(baseSymbol)
const functionDataL1 = chainlinkInterface.encodeFunctionData("addAggregator", [priceFeedKey, address])
const functionDataL2 = l2PriceFeedInterface.encodeFunctionData("addAggregator", [priceFeedKey])
const metadataSet = await getContractMetadataSet(env.network.name)
const filename = `addAggregator_${env.network.name}.txt`
const latestPriceNum = Number.parseFloat(formatUnits(latestPrice, decimals))
const lines = [
`pair: ${pair}`,
`base symbol: ${baseSymbol}`,
`quote symbol: ${quoteSymbol}`,
`latest price: ${latestPriceNum}`,
`maxHoldingBaseAsset (personal): ${100_000 / latestPriceNum}`,
`openInterestNotionalCap (total): 2_000_000`,
``,
`price feed key: ${priceFeedKey}`,
`aggregator address: ${address}`,
`functionData(ChainlinkL1,ChainlinkPriceFeed): ${functionDataL1}`,
`functionData(L2PriceFeed): ${functionDataL2}`,
"",
"Copy lines below to setup environment variables:",
`export ${env.network.name.toUpperCase()}_TOKEN_SYMBOL=${baseSymbol}`,
`export ${env.network.name.toUpperCase()}_PRICE_FEED_KEY=${priceFeedKey}`,
"",
`ABI information for gnosis safe is saved to ${filename}`,
]
const aggregatorInfoLines = [
`ChainlinkL1 abi: ${metadataSet.ChainlinkL1.abi}`,
`ChainlinkL1 proxy address: ${metadataSet.ChainlinkL1.proxy}`,
"",
`L2PriceFeed abi: ${metadataSet.L2PriceFeed.abi}`,
`L2PriceFeed proxy address: ${metadataSet.L2PriceFeed.proxy}`,
"",
`InsuranceFund abi: ${metadataSet.InsuranceFund.abi}`,
`InsuranceFund proxy address: ${metadataSet.InsuranceFund.proxy}`,
]
await fs.promises.writeFile(filename, aggregatorInfoLines.join("\n"))
console.log(lines.join("\n"))
}
Example #12
Source File: MemberActions.tsx From dxvote with GNU Affero General Public License v3.0 | 4 votes |
MemberActions = () => {
const [showMenu, setShowMenu] = useState(false);
const [showStakeModal, setShowStakeModal] = useState(false);
const { guild_id: guildAddress } = useParams<{ guild_id?: string }>();
const { account: userAddress } = useWeb3React();
const { ensName, imageUrl } = useENSAvatar(userAddress, MAINNET_ID);
const { data: guildConfig } = useGuildConfig(guildAddress);
const { data: tokenInfo } = useERC20Info(guildConfig?.token);
const { data: userVotingPower } = useVotingPowerOf({
contractAddress: guildAddress,
userAddress,
});
const { data: unlockedTimestamp } = useVoterLockTimestamp(
guildAddress,
userAddress
);
useEffect(() => {
if (showStakeModal) setShowMenu(false);
}, [showStakeModal]);
const votingPowerPercent = useVotingPowerPercent(
userVotingPower,
guildConfig?.totalLocked
);
const roundedBalance = useBigNumberToNumber(
userVotingPower,
tokenInfo?.decimals,
3
);
const isUnlockable = unlockedTimestamp
? unlockedTimestamp.isBefore(moment.now())
: false;
const { createTransaction } = useTransactions();
const guildContract = useERC20Guild(guildAddress);
const withdrawTokens = async () => {
setShowMenu(false);
createTransaction(
`Unlock and withdraw ${formatUnits(
userVotingPower,
tokenInfo?.decimals
)} ${tokenInfo?.symbol} tokens`,
async () => guildContract.withdrawTokens(userVotingPower)
);
};
const memberMenuRef = useRef(null);
useDetectBlur(memberMenuRef, () => setShowMenu(false));
const { isRepGuild } = useGuildImplementationType(guildAddress);
return (
<>
<DropdownMenu ref={memberMenuRef}>
<UserActionButton iconLeft onClick={() => setShowMenu(!showMenu)}>
<div>
<IconHolder>
<Avatar src={imageUrl} defaultSeed={userAddress} size={18} />
</IconHolder>
<span>{ensName || shortenAddress(userAddress)}</span>
</div>
<VotingPower>
{votingPowerPercent != null ? (
`${votingPowerPercent}%`
) : (
<Loading loading text skeletonProps={{ width: '40px' }} />
)}
</VotingPower>
</UserActionButton>
<DropdownContent fullScreenMobile={true} show={showMenu}>
{isMobile && (
<DropdownHeader noTopPadding onClick={() => setShowMenu(false)}>
<FiArrowLeft /> <span>Membership</span>
</DropdownHeader>
)}
<MemberContainer>
<ContentItem>
Voting Power{' '}
<span>
{votingPowerPercent != null ? (
`${votingPowerPercent}%`
) : (
<Loading loading text skeletonProps={{ width: '40px' }} />
)}
</span>
</ContentItem>
<ContentItem>
{!isUnlockable ? 'Locked' : 'Staked'}{' '}
<span>
{userVotingPower && tokenInfo ? (
`${roundedBalance} ${tokenInfo.symbol}`
) : (
<Loading loading text skeletonProps={{ width: '40px' }} />
)}
</span>
</ContentItem>
<ContentItem>
{isUnlockable ? 'Unlocked' : 'Unlocked in'}{' '}
<span>
{unlockedTimestamp ? (
isUnlockable ? (
unlockedTimestamp?.fromNow()
) : (
unlockedTimestamp?.toNow(true)
)
) : (
<Loading loading text skeletonProps={{ width: '40px' }} />
)}
</span>
</ContentItem>
<LockButton onClick={() => setShowStakeModal(true)}>
Increase Voting Power
</LockButton>
{isUnlockable && !isRepGuild && (
<LockButton onClick={withdrawTokens}>Withdraw</LockButton>
)}
</MemberContainer>
</DropdownContent>
</DropdownMenu>
<StakeTokensModal
isOpen={showStakeModal}
onDismiss={() => setShowStakeModal(false)}
/>
</>
);
}
Example #13
Source File: StakeTokens.tsx From dxvote with GNU Affero General Public License v3.0 | 4 votes |
StakeTokens = () => {
const [stakeAmount, setStakeAmount] = useState<string>('');
const { account: userAddress } = useWeb3React();
const { guild_id: guildAddress } = useParams<{ guild_id?: string }>();
const { data: guildConfig } = useGuildConfig(guildAddress);
const { data: tokenInfo } = useERC20Info(guildConfig?.token);
const { data: tokenBalance } = useERC20Balance(
guildConfig?.token,
userAddress
);
const roundedBalance = useBigNumberToNumber(
tokenBalance,
tokenInfo?.decimals,
4
);
const { data: tokenAllowance } = useERC20Allowance(
guildConfig?.token,
userAddress,
guildConfig?.tokenVault
);
const { data: userVotingPower } = useVotingPowerOf({
contractAddress: guildAddress,
userAddress,
});
const stakeAmountParsed = useStringToBigNumber(
stakeAmount,
tokenInfo.decimals
);
const isStakeAmountValid = useMemo(
() =>
stakeAmountParsed?.gt(0) &&
tokenInfo?.decimals &&
stakeAmountParsed.lte(tokenBalance),
[stakeAmountParsed, tokenBalance, tokenInfo]
);
const { createTransaction } = useTransactions();
const guildContract = useERC20Guild(guildAddress);
const lockTokens = async () => {
if (!isStakeAmountValid) return;
createTransaction(
`Lock ${formatUnits(stakeAmountParsed, tokenInfo?.decimals)} ${
tokenInfo?.symbol
} tokens`,
async () => guildContract.lockTokens(stakeAmountParsed)
);
};
const tokenContract = useERC20(guildConfig?.token);
const approveTokenSpending = async () => {
if (!isStakeAmountValid) return;
createTransaction(`Approve ${tokenInfo?.symbol} token spending`, async () =>
tokenContract.approve(guildConfig?.tokenVault, stakeAmountParsed)
);
};
const votingPowerPercent = useVotingPowerPercent(
userVotingPower,
guildConfig?.totalLocked,
3
);
const nextVotingPowerPercent = useVotingPowerPercent(
stakeAmountParsed?.add(userVotingPower),
stakeAmountParsed?.add(guildConfig?.totalLocked),
3
);
const history = useHistory();
const location = useLocation();
const { isRepGuild } = useGuildImplementationType(guildAddress);
return (
<GuestContainer>
<DaoBrand>
<DaoIcon src={dxIcon} alt={'DXdao Logo'} />
<DaoTitle>
{guildConfig?.name || (
<Loading text loading skeletonProps={{ width: 100 }} />
)}
</DaoTitle>
</DaoBrand>
{!isRepGuild && (
<InfoItem>
{guildConfig?.lockTime ? (
`${moment
.duration(guildConfig.lockTime.toNumber(), 'seconds')
.humanize()} staking period`
) : (
<Loading loading text skeletonProps={{ width: 200 }} />
)}{' '}
</InfoItem>
)}
{!isRepGuild && (
<BalanceWidget>
<InfoRow>
<InfoLabel>Balance:</InfoLabel>
<InfoValue>
{tokenBalance && tokenInfo ? (
roundedBalance
) : (
<Loading loading text skeletonProps={{ width: 30 }} />
)}{' '}
{tokenInfo?.symbol || (
<Loading loading text skeletonProps={{ width: 10 }} />
)}
</InfoValue>
</InfoRow>
<InfoRow>
<StakeAmountInput
value={stakeAmount}
onUserInput={setStakeAmount}
/>
<Button
onClick={() =>
setStakeAmount(formatUnits(tokenBalance, tokenInfo?.decimals))
}
>
Max
</Button>
</InfoRow>
</BalanceWidget>
)}
{isRepGuild && (
<InfoRow>
<InfoLabel>Balance</InfoLabel>
<InfoValue>
{tokenBalance && tokenInfo ? (
roundedBalance
) : (
<Loading loading text skeletonProps={{ width: 30 }} />
)}{' '}
{tokenInfo?.symbol || (
<Loading loading text skeletonProps={{ width: 10 }} />
)}
</InfoValue>
</InfoRow>
)}
<InfoRow>
<InfoLabel>Your voting power</InfoLabel>
<InfoValue>
{isStakeAmountValid ? (
<>
<InfoOldValue>
{votingPowerPercent != null ? (
`${votingPowerPercent}%`
) : (
<Loading loading text skeletonProps={{ width: 40 }} />
)}{' '}
<FiArrowRight />
</InfoOldValue>{' '}
<strong>
{nextVotingPowerPercent != null ? (
`${nextVotingPowerPercent}%`
) : (
<Loading loading text skeletonProps={{ width: 40 }} />
)}
</strong>
</>
) : (
'-'
)}
</InfoValue>
</InfoRow>
{!isRepGuild && (
<InfoRow>
<InfoLabel>Unlock Date</InfoLabel>
<InfoValue>
{isStakeAmountValid ? (
<>
<strong>
{moment()
.add(guildConfig.lockTime.toNumber(), 'seconds')
.format('MMM Do, YYYY - h:mm a')}
</strong>{' '}
<FiInfo />
</>
) : (
'-'
)}
</InfoValue>
</InfoRow>
)}
{!isRepGuild ? (
stakeAmountParsed && tokenAllowance?.gte(stakeAmountParsed) ? (
<ActionButton disabled={!isStakeAmountValid} onClick={lockTokens}>
Lock{' '}
{tokenInfo?.symbol || (
<Loading loading text skeletonProps={{ width: 10 }} />
)}
</ActionButton>
) : (
<ActionButton
disabled={!isStakeAmountValid}
onClick={approveTokenSpending}
>
Approve{' '}
{tokenInfo?.symbol || (
<Loading loading text skeletonProps={{ width: 10 }} />
)}{' '}
Spending
</ActionButton>
)
) : (
<ActionButton
onClick={() => history.push(location.pathname + '/proposalType')}
>
Mint Rep
</ActionButton>
)}
</GuestContainer>
);
}
Example #14
Source File: Media.test.ts From core with GNU General Public License v3.0 | 4 votes |
describe('Media', () => {
let [
deployerWallet,
bidderWallet,
creatorWallet,
ownerWallet,
prevOwnerWallet,
otherWallet,
nonBidderWallet,
] = generatedWallets(provider);
let defaultBidShares = {
prevOwner: Decimal.new(10),
owner: Decimal.new(80),
creator: Decimal.new(10),
};
let defaultTokenId = 1;
let defaultAsk = {
amount: 100,
currency: '0x41A322b28D0fF354040e2CbC676F0320d8c8850d',
sellOnShare: Decimal.new(0),
};
const defaultBid = (
currency: string,
bidder: string,
recipient?: string
) => ({
amount: 100,
currency,
bidder,
recipient: recipient || bidder,
sellOnShare: Decimal.new(10),
});
let auctionAddress: string;
let tokenAddress: string;
async function tokenAs(wallet: Wallet) {
return MediaFactory.connect(tokenAddress, wallet);
}
async function deploy() {
const auction = await (
await new MarketFactory(deployerWallet).deploy()
).deployed();
auctionAddress = auction.address;
const token = await (
await new MediaFactory(deployerWallet).deploy(auction.address)
).deployed();
tokenAddress = token.address;
await auction.configure(tokenAddress);
}
async function mint(
token: Media,
metadataURI: string,
tokenURI: string,
contentHash: Bytes,
metadataHash: Bytes,
shares: BidShares
) {
const data: MediaData = {
tokenURI,
metadataURI,
contentHash,
metadataHash,
};
return token.mint(data, shares);
}
async function mintWithSig(
token: Media,
creator: string,
tokenURI: string,
metadataURI: string,
contentHash: Bytes,
metadataHash: Bytes,
shares: BidShares,
sig: EIP712Sig
) {
const data: MediaData = {
tokenURI,
metadataURI,
contentHash,
metadataHash,
};
return token.mintWithSig(creator, data, shares, sig);
}
async function setAsk(token: Media, tokenId: number, ask: Ask) {
return token.setAsk(tokenId, ask);
}
async function removeAsk(token: Media, tokenId: number) {
return token.removeAsk(tokenId);
}
async function setBid(token: Media, bid: Bid, tokenId: number) {
return token.setBid(tokenId, bid);
}
async function removeBid(token: Media, tokenId: number) {
return token.removeBid(tokenId);
}
async function acceptBid(token: Media, tokenId: number, bid: Bid) {
return token.acceptBid(tokenId, bid);
}
// Trade a token a few times and create some open bids
async function setupAuction(currencyAddr: string, tokenId = 0) {
const asCreator = await tokenAs(creatorWallet);
const asPrevOwner = await tokenAs(prevOwnerWallet);
const asOwner = await tokenAs(ownerWallet);
const asBidder = await tokenAs(bidderWallet);
const asOther = await tokenAs(otherWallet);
await mintCurrency(currencyAddr, creatorWallet.address, 10000);
await mintCurrency(currencyAddr, prevOwnerWallet.address, 10000);
await mintCurrency(currencyAddr, ownerWallet.address, 10000);
await mintCurrency(currencyAddr, bidderWallet.address, 10000);
await mintCurrency(currencyAddr, otherWallet.address, 10000);
await approveCurrency(currencyAddr, auctionAddress, creatorWallet);
await approveCurrency(currencyAddr, auctionAddress, prevOwnerWallet);
await approveCurrency(currencyAddr, auctionAddress, ownerWallet);
await approveCurrency(currencyAddr, auctionAddress, bidderWallet);
await approveCurrency(currencyAddr, auctionAddress, otherWallet);
await mint(
asCreator,
metadataURI,
tokenURI,
contentHashBytes,
metadataHashBytes,
defaultBidShares
);
await setBid(
asPrevOwner,
defaultBid(currencyAddr, prevOwnerWallet.address),
tokenId
);
await acceptBid(asCreator, tokenId, {
...defaultBid(currencyAddr, prevOwnerWallet.address),
});
await setBid(
asOwner,
defaultBid(currencyAddr, ownerWallet.address),
tokenId
);
await acceptBid(
asPrevOwner,
tokenId,
defaultBid(currencyAddr, ownerWallet.address)
);
await setBid(
asBidder,
defaultBid(currencyAddr, bidderWallet.address),
tokenId
);
await setBid(
asOther,
defaultBid(currencyAddr, otherWallet.address),
tokenId
);
}
beforeEach(async () => {
await blockchain.resetAsync();
metadataHex = ethers.utils.formatBytes32String('{}');
metadataHash = await sha256(metadataHex);
metadataHashBytes = ethers.utils.arrayify(metadataHash);
contentHex = ethers.utils.formatBytes32String('invert');
contentHash = await sha256(contentHex);
contentHashBytes = ethers.utils.arrayify(contentHash);
otherContentHex = ethers.utils.formatBytes32String('otherthing');
otherContentHash = await sha256(otherContentHex);
otherContentHashBytes = ethers.utils.arrayify(otherContentHash);
zeroContentHashBytes = ethers.utils.arrayify(ethers.constants.HashZero);
});
describe('#constructor', () => {
it('should be able to deploy', async () => {
await expect(deploy()).eventually.fulfilled;
});
});
describe('#mint', () => {
beforeEach(async () => {
await deploy();
});
it('should mint a token', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(
token,
metadataURI,
tokenURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
)
).fulfilled;
const t = await token.tokenByIndex(0);
const ownerT = await token.tokenOfOwnerByIndex(creatorWallet.address, 0);
const ownerOf = await token.ownerOf(0);
const creator = await token.tokenCreators(0);
const prevOwner = await token.previousTokenOwners(0);
const tokenContentHash = await token.tokenContentHashes(0);
const metadataContentHash = await token.tokenMetadataHashes(0);
const savedTokenURI = await token.tokenURI(0);
const savedMetadataURI = await token.tokenMetadataURI(0);
expect(toNumWei(t)).eq(toNumWei(ownerT));
expect(ownerOf).eq(creatorWallet.address);
expect(creator).eq(creatorWallet.address);
expect(prevOwner).eq(creatorWallet.address);
expect(tokenContentHash).eq(contentHash);
expect(metadataContentHash).eq(metadataHash);
expect(savedTokenURI).eq(tokenURI);
expect(savedMetadataURI).eq(metadataURI);
});
it('should revert if an empty content hash is specified', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(
token,
metadataURI,
tokenURI,
zeroContentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
)
).rejectedWith('Media: content hash must be non-zero');
});
it('should revert if the content hash already exists for a created token', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(
token,
metadataURI,
tokenURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
)
).fulfilled;
await expect(
mint(
token,
metadataURI,
tokenURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
)
).rejectedWith(
'Media: a token has already been created with this content hash'
);
});
it('should revert if the metadataHash is empty', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(
token,
metadataURI,
tokenURI,
contentHashBytes,
zeroContentHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
)
).rejectedWith('Media: metadata hash must be non-zero');
});
it('should revert if the tokenURI is empty', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(token, metadataURI, '', zeroContentHashBytes, metadataHashBytes, {
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
})
).rejectedWith('Media: specified uri must be non-empty');
});
it('should revert if the metadataURI is empty', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(token, '', tokenURI, zeroContentHashBytes, metadataHashBytes, {
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
})
).rejectedWith('Media: specified uri must be non-empty');
});
it('should not be able to mint a token with bid shares summing to less than 100', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(
token,
metadataURI,
tokenURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(15),
owner: Decimal.new(15),
creator: Decimal.new(15),
}
)
).rejectedWith('Market: Invalid bid shares, must sum to 100');
});
it('should not be able to mint a token with bid shares summing to greater than 100', async () => {
const token = await tokenAs(creatorWallet);
await expect(
mint(token, metadataURI, '222', contentHashBytes, metadataHashBytes, {
prevOwner: Decimal.new(99),
owner: Decimal.new(1),
creator: Decimal.new(1),
})
).rejectedWith('Market: Invalid bid shares, must sum to 100');
});
});
describe('#mintWithSig', () => {
beforeEach(async () => {
await deploy();
});
it('should mint a token for a given creator with a valid signature', async () => {
const token = await tokenAs(otherWallet);
const market = await MarketFactory.connect(auctionAddress, otherWallet);
const sig = await signMintWithSig(
creatorWallet,
token.address,
creatorWallet.address,
contentHash,
metadataHash,
Decimal.new(5).value.toString(),
1
);
const beforeNonce = await token.mintWithSigNonces(creatorWallet.address);
await expect(
mintWithSig(
token,
creatorWallet.address,
tokenURI,
metadataURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(0),
owner: Decimal.new(95),
creator: Decimal.new(5),
},
sig
)
).fulfilled;
const recovered = await token.tokenCreators(0);
const recoveredTokenURI = await token.tokenURI(0);
const recoveredMetadataURI = await token.tokenMetadataURI(0);
const recoveredContentHash = await token.tokenContentHashes(0);
const recoveredMetadataHash = await token.tokenMetadataHashes(0);
const recoveredCreatorBidShare = formatUnits(
(await market.bidSharesForToken(0)).creator.value,
'ether'
);
const afterNonce = await token.mintWithSigNonces(creatorWallet.address);
expect(recovered).to.eq(creatorWallet.address);
expect(recoveredTokenURI).to.eq(tokenURI);
expect(recoveredMetadataURI).to.eq(metadataURI);
expect(recoveredContentHash).to.eq(contentHash);
expect(recoveredMetadataHash).to.eq(metadataHash);
expect(recoveredCreatorBidShare).to.eq('5.0');
expect(toNumWei(afterNonce)).to.eq(toNumWei(beforeNonce) + 1);
});
it('should not mint a token for a different creator', async () => {
const token = await tokenAs(otherWallet);
const sig = await signMintWithSig(
bidderWallet,
token.address,
creatorWallet.address,
tokenURI,
metadataURI,
Decimal.new(5).value.toString(),
1
);
await expect(
mintWithSig(
token,
creatorWallet.address,
tokenURI,
metadataURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(0),
owner: Decimal.new(95),
creator: Decimal.new(5),
},
sig
)
).rejectedWith('Media: Signature invalid');
});
it('should not mint a token for a different contentHash', async () => {
const badContent = 'bad bad bad';
const badContentHex = formatBytes32String(badContent);
const badContentHash = sha256(badContentHex);
const badContentHashBytes = arrayify(badContentHash);
const token = await tokenAs(otherWallet);
const sig = await signMintWithSig(
creatorWallet,
token.address,
creatorWallet.address,
contentHash,
metadataHash,
Decimal.new(5).value.toString(),
1
);
await expect(
mintWithSig(
token,
creatorWallet.address,
tokenURI,
metadataURI,
badContentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(0),
owner: Decimal.new(95),
creator: Decimal.new(5),
},
sig
)
).rejectedWith('Media: Signature invalid');
});
it('should not mint a token for a different metadataHash', async () => {
const badMetadata = '{"some": "bad", "data": ":)"}';
const badMetadataHex = formatBytes32String(badMetadata);
const badMetadataHash = sha256(badMetadataHex);
const badMetadataHashBytes = arrayify(badMetadataHash);
const token = await tokenAs(otherWallet);
const sig = await signMintWithSig(
creatorWallet,
token.address,
creatorWallet.address,
contentHash,
metadataHash,
Decimal.new(5).value.toString(),
1
);
await expect(
mintWithSig(
token,
creatorWallet.address,
tokenURI,
metadataURI,
contentHashBytes,
badMetadataHashBytes,
{
prevOwner: Decimal.new(0),
owner: Decimal.new(95),
creator: Decimal.new(5),
},
sig
)
).rejectedWith('Media: Signature invalid');
});
it('should not mint a token for a different creator bid share', async () => {
const token = await tokenAs(otherWallet);
const sig = await signMintWithSig(
creatorWallet,
token.address,
creatorWallet.address,
tokenURI,
metadataURI,
Decimal.new(5).value.toString(),
1
);
await expect(
mintWithSig(
token,
creatorWallet.address,
tokenURI,
metadataURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(0),
owner: Decimal.new(100),
creator: Decimal.new(0),
},
sig
)
).rejectedWith('Media: Signature invalid');
});
it('should not mint a token with an invalid deadline', async () => {
const token = await tokenAs(otherWallet);
const sig = await signMintWithSig(
creatorWallet,
token.address,
creatorWallet.address,
tokenURI,
metadataURI,
Decimal.new(5).value.toString(),
1
);
await expect(
mintWithSig(
token,
creatorWallet.address,
tokenURI,
metadataURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(0),
owner: Decimal.new(95),
creator: Decimal.new(5),
},
{ ...sig, deadline: '1' }
)
).rejectedWith('Media: mintWithSig expired');
});
});
describe('#setAsk', () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
currencyAddr = await deployCurrency();
await setupAuction(currencyAddr);
});
it('should set the ask', async () => {
const token = await tokenAs(ownerWallet);
await expect(setAsk(token, 0, defaultAsk)).fulfilled;
});
it('should reject if the ask is 0', async () => {
const token = await tokenAs(ownerWallet);
await expect(setAsk(token, 0, { ...defaultAsk, amount: 0 })).rejectedWith(
'Market: Ask invalid for share splitting'
);
});
it('should reject if the ask amount is invalid and cannot be split', async () => {
const token = await tokenAs(ownerWallet);
await expect(
setAsk(token, 0, { ...defaultAsk, amount: 101 })
).rejectedWith('Market: Ask invalid for share splitting');
});
});
describe('#removeAsk', () => {
it('should remove the ask', async () => {
const token = await tokenAs(ownerWallet);
const market = await MarketFactory.connect(
auctionAddress,
deployerWallet
);
await setAsk(token, 0, defaultAsk);
await expect(removeAsk(token, 0)).fulfilled;
const ask = await market.currentAskForToken(0);
expect(toNumWei(ask.amount)).eq(0);
expect(ask.currency).eq(AddressZero);
});
it('should emit an Ask Removed event', async () => {
const token = await tokenAs(ownerWallet);
const auction = await MarketFactory.connect(
auctionAddress,
deployerWallet
);
await setAsk(token, 0, defaultAsk);
const block = await provider.getBlockNumber();
const tx = await removeAsk(token, 0);
const events = await auction.queryFilter(
auction.filters.AskRemoved(0, null),
block
);
expect(events.length).eq(1);
const logDescription = auction.interface.parseLog(events[0]);
expect(toNumWei(logDescription.args.tokenId)).to.eq(0);
expect(toNumWei(logDescription.args.ask.amount)).to.eq(defaultAsk.amount);
expect(logDescription.args.ask.currency).to.eq(defaultAsk.currency);
});
it('should not be callable by anyone that is not owner or approved', async () => {
const token = await tokenAs(ownerWallet);
const asOther = await tokenAs(otherWallet);
await setAsk(token, 0, defaultAsk);
expect(removeAsk(asOther, 0)).rejectedWith(
'Media: Only approved or owner'
);
});
});
describe('#setBid', () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
await mint(
await tokenAs(creatorWallet),
metadataURI,
'1111',
otherContentHashBytes,
metadataHashBytes,
defaultBidShares
);
currencyAddr = await deployCurrency();
});
it('should revert if the token bidder does not have a high enough allowance for their bidding currency', async () => {
const token = await tokenAs(bidderWallet);
await expect(
token.setBid(0, defaultBid(currencyAddr, bidderWallet.address))
).rejectedWith('SafeERC20: ERC20 operation did not succeed');
});
it('should revert if the token bidder does not have a high enough balance for their bidding currency', async () => {
const token = await tokenAs(bidderWallet);
await approveCurrency(currencyAddr, auctionAddress, bidderWallet);
await expect(
token.setBid(0, defaultBid(currencyAddr, bidderWallet.address))
).rejectedWith('SafeERC20: ERC20 operation did not succeed');
});
it('should set a bid', async () => {
const token = await tokenAs(bidderWallet);
await approveCurrency(currencyAddr, auctionAddress, bidderWallet);
await mintCurrency(currencyAddr, bidderWallet.address, 100000);
await expect(
token.setBid(0, defaultBid(currencyAddr, bidderWallet.address))
).fulfilled;
const balance = await getBalance(currencyAddr, bidderWallet.address);
expect(toNumWei(balance)).eq(100000 - 100);
});
it('should automatically transfer the token if the ask is set', async () => {
const token = await tokenAs(bidderWallet);
const asOwner = await tokenAs(ownerWallet);
await setupAuction(currencyAddr, 1);
await setAsk(asOwner, 1, { ...defaultAsk, currency: currencyAddr });
await expect(
token.setBid(1, defaultBid(currencyAddr, bidderWallet.address))
).fulfilled;
await expect(token.ownerOf(1)).eventually.eq(bidderWallet.address);
});
it('should refund a bid if one already exists for the bidder', async () => {
const token = await tokenAs(bidderWallet);
await setupAuction(currencyAddr, 1);
const beforeBalance = toNumWei(
await getBalance(currencyAddr, bidderWallet.address)
);
await setBid(
token,
{
currency: currencyAddr,
amount: 200,
bidder: bidderWallet.address,
recipient: otherWallet.address,
sellOnShare: Decimal.new(10),
},
1
);
const afterBalance = toNumWei(
await getBalance(currencyAddr, bidderWallet.address)
);
expect(afterBalance).eq(beforeBalance - 100);
});
});
describe('#removeBid', () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
currencyAddr = await deployCurrency();
await setupAuction(currencyAddr);
});
it('should revert if the bidder has not placed a bid', async () => {
const token = await tokenAs(nonBidderWallet);
await expect(removeBid(token, 0)).rejectedWith(
'Market: cannot remove bid amount of 0'
);
});
it('should revert if the tokenId has not yet ben created', async () => {
const token = await tokenAs(bidderWallet);
await expect(removeBid(token, 100)).rejectedWith(
'Media: token with that id does not exist'
);
});
it('should remove a bid and refund the bidder', async () => {
const token = await tokenAs(bidderWallet);
const beforeBalance = toNumWei(
await getBalance(currencyAddr, bidderWallet.address)
);
await expect(removeBid(token, 0)).fulfilled;
const afterBalance = toNumWei(
await getBalance(currencyAddr, bidderWallet.address)
);
expect(afterBalance).eq(beforeBalance + 100);
});
it('should not be able to remove a bid twice', async () => {
const token = await tokenAs(bidderWallet);
await removeBid(token, 0);
await expect(removeBid(token, 0)).rejectedWith(
'Market: cannot remove bid amount of 0'
);
});
it('should remove a bid, even if the token is burned', async () => {
const asOwner = await tokenAs(ownerWallet);
const asBidder = await tokenAs(bidderWallet);
const asCreator = await tokenAs(creatorWallet);
await asOwner.transferFrom(ownerWallet.address, creatorWallet.address, 0);
await asCreator.burn(0);
const beforeBalance = toNumWei(
await getBalance(currencyAddr, bidderWallet.address)
);
await expect(asBidder.removeBid(0)).fulfilled;
const afterBalance = toNumWei(
await getBalance(currencyAddr, bidderWallet.address)
);
expect(afterBalance).eq(beforeBalance + 100);
});
});
describe('#acceptBid', () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
currencyAddr = await deployCurrency();
await setupAuction(currencyAddr);
});
it('should accept a bid', async () => {
const token = await tokenAs(ownerWallet);
const auction = await MarketFactory.connect(auctionAddress, bidderWallet);
const asBidder = await tokenAs(bidderWallet);
const bid = {
...defaultBid(currencyAddr, bidderWallet.address, otherWallet.address),
sellOnShare: Decimal.new(15),
};
await setBid(asBidder, bid, 0);
const beforeOwnerBalance = toNumWei(
await getBalance(currencyAddr, ownerWallet.address)
);
const beforePrevOwnerBalance = toNumWei(
await getBalance(currencyAddr, prevOwnerWallet.address)
);
const beforeCreatorBalance = toNumWei(
await getBalance(currencyAddr, creatorWallet.address)
);
await expect(token.acceptBid(0, bid)).fulfilled;
const newOwner = await token.ownerOf(0);
const afterOwnerBalance = toNumWei(
await getBalance(currencyAddr, ownerWallet.address)
);
const afterPrevOwnerBalance = toNumWei(
await getBalance(currencyAddr, prevOwnerWallet.address)
);
const afterCreatorBalance = toNumWei(
await getBalance(currencyAddr, creatorWallet.address)
);
const bidShares = await auction.bidSharesForToken(0);
expect(afterOwnerBalance).eq(beforeOwnerBalance + 80);
expect(afterPrevOwnerBalance).eq(beforePrevOwnerBalance + 10);
expect(afterCreatorBalance).eq(beforeCreatorBalance + 10);
expect(newOwner).eq(otherWallet.address);
expect(toNumWei(bidShares.owner.value)).eq(75 * 10 ** 18);
expect(toNumWei(bidShares.prevOwner.value)).eq(15 * 10 ** 18);
expect(toNumWei(bidShares.creator.value)).eq(10 * 10 ** 18);
});
it('should emit a bid finalized event if the bid is accepted', async () => {
const asBidder = await tokenAs(bidderWallet);
const token = await tokenAs(ownerWallet);
const auction = await MarketFactory.connect(auctionAddress, bidderWallet);
const bid = defaultBid(currencyAddr, bidderWallet.address);
const block = await provider.getBlockNumber();
await setBid(asBidder, bid, 0);
await token.acceptBid(0, bid);
const events = await auction.queryFilter(
auction.filters.BidFinalized(null, null),
block
);
expect(events.length).eq(1);
const logDescription = auction.interface.parseLog(events[0]);
expect(toNumWei(logDescription.args.tokenId)).to.eq(0);
expect(toNumWei(logDescription.args.bid.amount)).to.eq(bid.amount);
expect(logDescription.args.bid.currency).to.eq(bid.currency);
expect(toNumWei(logDescription.args.bid.sellOnShare.value)).to.eq(
toNumWei(bid.sellOnShare.value)
);
expect(logDescription.args.bid.bidder).to.eq(bid.bidder);
});
it('should emit a bid shares updated event if the bid is accepted', async () => {
const asBidder = await tokenAs(bidderWallet);
const token = await tokenAs(ownerWallet);
const auction = await MarketFactory.connect(auctionAddress, bidderWallet);
const bid = defaultBid(currencyAddr, bidderWallet.address);
const block = await provider.getBlockNumber();
await setBid(asBidder, bid, 0);
await token.acceptBid(0, bid);
const events = await auction.queryFilter(
auction.filters.BidShareUpdated(null, null),
block
);
expect(events.length).eq(1);
const logDescription = auction.interface.parseLog(events[0]);
expect(toNumWei(logDescription.args.tokenId)).to.eq(0);
expect(toNumWei(logDescription.args.bidShares.prevOwner.value)).to.eq(
10000000000000000000
);
expect(toNumWei(logDescription.args.bidShares.owner.value)).to.eq(
80000000000000000000
);
expect(toNumWei(logDescription.args.bidShares.creator.value)).to.eq(
10000000000000000000
);
});
it('should revert if not called by the owner', async () => {
const token = await tokenAs(otherWallet);
await expect(
token.acceptBid(0, { ...defaultBid(currencyAddr, otherWallet.address) })
).rejectedWith('Media: Only approved or owner');
});
it('should revert if a non-existent bid is accepted', async () => {
const token = await tokenAs(ownerWallet);
await expect(
token.acceptBid(0, { ...defaultBid(currencyAddr, AddressZero) })
).rejectedWith('Market: cannot accept bid of 0');
});
it('should revert if an invalid bid is accepted', async () => {
const token = await tokenAs(ownerWallet);
const asBidder = await tokenAs(bidderWallet);
const bid = {
...defaultBid(currencyAddr, bidderWallet.address),
amount: 99,
};
await setBid(asBidder, bid, 0);
await expect(token.acceptBid(0, bid)).rejectedWith(
'Market: Bid invalid for share splitting'
);
});
// TODO: test the front running logic
});
describe('#transfer', () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
currencyAddr = await deployCurrency();
await setupAuction(currencyAddr);
});
it('should remove the ask after a transfer', async () => {
const token = await tokenAs(ownerWallet);
const auction = MarketFactory.connect(auctionAddress, deployerWallet);
await setAsk(token, 0, defaultAsk);
await expect(
token.transferFrom(ownerWallet.address, otherWallet.address, 0)
).fulfilled;
const ask = await auction.currentAskForToken(0);
await expect(toNumWei(ask.amount)).eq(0);
await expect(ask.currency).eq(AddressZero);
});
});
describe('#burn', () => {
beforeEach(async () => {
await deploy();
const token = await tokenAs(creatorWallet);
await mint(
token,
metadataURI,
tokenURI,
contentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
);
});
it('should revert when the caller is the owner, but not creator', async () => {
const creatorToken = await tokenAs(creatorWallet);
await creatorToken.transferFrom(
creatorWallet.address,
ownerWallet.address,
0
);
const token = await tokenAs(ownerWallet);
await expect(token.burn(0)).rejectedWith(
'Media: owner is not creator of media'
);
});
it('should revert when the caller is approved, but the owner is not the creator', async () => {
const creatorToken = await tokenAs(creatorWallet);
await creatorToken.transferFrom(
creatorWallet.address,
ownerWallet.address,
0
);
const token = await tokenAs(ownerWallet);
await token.approve(otherWallet.address, 0);
const otherToken = await tokenAs(otherWallet);
await expect(otherToken.burn(0)).rejectedWith(
'Media: owner is not creator of media'
);
});
it('should revert when the caller is not the owner or a creator', async () => {
const token = await tokenAs(otherWallet);
await expect(token.burn(0)).rejectedWith('Media: Only approved or owner');
});
it('should revert if the token id does not exist', async () => {
const token = await tokenAs(creatorWallet);
await expect(token.burn(100)).rejectedWith('Media: nonexistent token');
});
it('should clear approvals, set remove owner, but maintain tokenURI and contentHash when the owner is creator and caller', async () => {
const token = await tokenAs(creatorWallet);
await expect(token.approve(otherWallet.address, 0)).fulfilled;
await expect(token.burn(0)).fulfilled;
await expect(token.ownerOf(0)).rejectedWith(
'ERC721: owner query for nonexistent token'
);
const totalSupply = await token.totalSupply();
expect(toNumWei(totalSupply)).eq(0);
await expect(token.getApproved(0)).rejectedWith(
'ERC721: approved query for nonexistent token'
);
const tokenURI = await token.tokenURI(0);
expect(tokenURI).eq('www.example.com');
const contentHash = await token.tokenContentHashes(0);
expect(contentHash).eq(contentHash);
const previousOwner = await token.previousTokenOwners(0);
expect(previousOwner).eq(AddressZero);
});
it('should clear approvals, set remove owner, but maintain tokenURI and contentHash when the owner is creator and caller is approved', async () => {
const token = await tokenAs(creatorWallet);
await expect(token.approve(otherWallet.address, 0)).fulfilled;
const otherToken = await tokenAs(otherWallet);
await expect(otherToken.burn(0)).fulfilled;
await expect(token.ownerOf(0)).rejectedWith(
'ERC721: owner query for nonexistent token'
);
const totalSupply = await token.totalSupply();
expect(toNumWei(totalSupply)).eq(0);
await expect(token.getApproved(0)).rejectedWith(
'ERC721: approved query for nonexistent token'
);
const tokenURI = await token.tokenURI(0);
expect(tokenURI).eq('www.example.com');
const contentHash = await token.tokenContentHashes(0);
expect(contentHash).eq(contentHash);
const previousOwner = await token.previousTokenOwners(0);
expect(previousOwner).eq(AddressZero);
});
});
describe('#updateTokenURI', async () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
currencyAddr = await deployCurrency();
await setupAuction(currencyAddr);
});
it('should revert if the token does not exist', async () => {
const token = await tokenAs(creatorWallet);
await expect(token.updateTokenURI(1, 'blah blah')).rejectedWith(
'ERC721: operator query for nonexistent token'
);
});
it('should revert if the caller is not the owner of the token and does not have approval', async () => {
const token = await tokenAs(otherWallet);
await expect(token.updateTokenURI(0, 'blah blah')).rejectedWith(
'Media: Only approved or owner'
);
});
it('should revert if the uri is empty string', async () => {
const token = await tokenAs(ownerWallet);
await expect(token.updateTokenURI(0, '')).rejectedWith(
'Media: specified uri must be non-empty'
);
});
it('should revert if the token has been burned', async () => {
const token = await tokenAs(creatorWallet);
await mint(
token,
metadataURI,
tokenURI,
otherContentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
);
await expect(token.burn(1)).fulfilled;
await expect(token.updateTokenURI(1, 'blah')).rejectedWith(
'ERC721: operator query for nonexistent token'
);
});
it('should set the tokenURI to the URI passed if the msg.sender is the owner', async () => {
const token = await tokenAs(ownerWallet);
await expect(token.updateTokenURI(0, 'blah blah')).fulfilled;
const tokenURI = await token.tokenURI(0);
expect(tokenURI).eq('blah blah');
});
it('should set the tokenURI to the URI passed if the msg.sender is approved', async () => {
const token = await tokenAs(ownerWallet);
await token.approve(otherWallet.address, 0);
const otherToken = await tokenAs(otherWallet);
await expect(otherToken.updateTokenURI(0, 'blah blah')).fulfilled;
const tokenURI = await token.tokenURI(0);
expect(tokenURI).eq('blah blah');
});
});
describe('#updateMetadataURI', async () => {
let currencyAddr: string;
beforeEach(async () => {
await deploy();
currencyAddr = await deployCurrency();
await setupAuction(currencyAddr);
});
it('should revert if the token does not exist', async () => {
const token = await tokenAs(creatorWallet);
await expect(token.updateTokenMetadataURI(1, 'blah blah')).rejectedWith(
'ERC721: operator query for nonexistent token'
);
});
it('should revert if the caller is not the owner of the token or approved', async () => {
const token = await tokenAs(otherWallet);
await expect(token.updateTokenMetadataURI(0, 'blah blah')).rejectedWith(
'Media: Only approved or owner'
);
});
it('should revert if the uri is empty string', async () => {
const token = await tokenAs(ownerWallet);
await expect(token.updateTokenMetadataURI(0, '')).rejectedWith(
'Media: specified uri must be non-empty'
);
});
it('should revert if the token has been burned', async () => {
const token = await tokenAs(creatorWallet);
await mint(
token,
metadataURI,
tokenURI,
otherContentHashBytes,
metadataHashBytes,
{
prevOwner: Decimal.new(10),
creator: Decimal.new(90),
owner: Decimal.new(0),
}
);
await expect(token.burn(1)).fulfilled;
await expect(token.updateTokenMetadataURI(1, 'blah')).rejectedWith(
'ERC721: operator query for nonexistent token'
);
});
it('should set the tokenMetadataURI to the URI passed if msg.sender is the owner', async () => {
const token = await tokenAs(ownerWallet);
await expect(token.updateTokenMetadataURI(0, 'blah blah')).fulfilled;
const tokenURI = await token.tokenMetadataURI(0);
expect(tokenURI).eq('blah blah');
});
it('should set the tokenMetadataURI to the URI passed if the msg.sender is approved', async () => {
const token = await tokenAs(ownerWallet);
await token.approve(otherWallet.address, 0);
const otherToken = await tokenAs(otherWallet);
await expect(otherToken.updateTokenMetadataURI(0, 'blah blah')).fulfilled;
const tokenURI = await token.tokenMetadataURI(0);
expect(tokenURI).eq('blah blah');
});
});
describe('#permit', () => {
let currency: string;
beforeEach(async () => {
await deploy();
currency = await deployCurrency();
await setupAuction(currency);
});
it('should allow a wallet to set themselves to approved with a valid signature', async () => {
const token = await tokenAs(otherWallet);
const sig = await signPermit(
ownerWallet,
otherWallet.address,
token.address,
0,
// NOTE: We set the chain ID to 1 because of an error with ganache-core: https://github.com/trufflesuite/ganache-core/issues/515
1
);
await expect(token.permit(otherWallet.address, 0, sig)).fulfilled;
await expect(token.getApproved(0)).eventually.eq(otherWallet.address);
});
it('should not allow a wallet to set themselves to approved with an invalid signature', async () => {
const token = await tokenAs(otherWallet);
const sig = await signPermit(
ownerWallet,
bidderWallet.address,
token.address,
0,
1
);
await expect(token.permit(otherWallet.address, 0, sig)).rejectedWith(
'Media: Signature invalid'
);
await expect(token.getApproved(0)).eventually.eq(AddressZero);
});
});
describe('#supportsInterface', async () => {
beforeEach(async () => {
await deploy();
});
it('should return true to supporting new metadata interface', async () => {
const token = await tokenAs(otherWallet);
const interfaceId = ethers.utils.arrayify('0x4e222e66');
const supportsId = await token.supportsInterface(interfaceId);
expect(supportsId).eq(true);
});
it('should return false to supporting the old metadata interface', async () => {
const token = await tokenAs(otherWallet);
const interfaceId = ethers.utils.arrayify('0x5b5e139f');
const supportsId = await token.supportsInterface(interfaceId);
expect(supportsId).eq(false);
});
});
describe('#revokeApproval', async () => {
let currency: string;
beforeEach(async () => {
await deploy();
currency = await deployCurrency();
await setupAuction(currency);
});
it('should revert if the caller is the owner', async () => {
const token = await tokenAs(ownerWallet);
await expect(token.revokeApproval(0)).rejectedWith(
'Media: caller not approved address'
);
});
it('should revert if the caller is the creator', async () => {
const token = await tokenAs(creatorWallet);
await expect(token.revokeApproval(0)).rejectedWith(
'Media: caller not approved address'
);
});
it('should revert if the caller is neither owner, creator, or approver', async () => {
const token = await tokenAs(otherWallet);
await expect(token.revokeApproval(0)).rejectedWith(
'Media: caller not approved address'
);
});
it('should revoke the approval for token id if caller is approved address', async () => {
const token = await tokenAs(ownerWallet);
await token.approve(otherWallet.address, 0);
const otherToken = await tokenAs(otherWallet);
await expect(otherToken.revokeApproval(0)).fulfilled;
const approved = await token.getApproved(0);
expect(approved).eq(ethers.constants.AddressZero);
});
});
});
Example #15
Source File: index.ts From vvs-ui with GNU General Public License v3.0 | 4 votes |
fetchNodeHistory = createAsyncThunk<
{ bets: Bet[]; claimableStatuses: PredictionsState['claimableStatuses']; page?: number; totalHistory: number },
{ account: string; page?: number }
>('predictions/fetchNodeHistory', async ({ account, page = 1 }) => {
const userRoundsLength = await fetchUsersRoundsLength(account)
const emptyResult = { bets: [], claimableStatuses: {}, totalHistory: userRoundsLength.toNumber() }
const maxPages = userRoundsLength.lte(ROUNDS_PER_PAGE) ? 1 : Math.ceil(userRoundsLength.toNumber() / ROUNDS_PER_PAGE)
if (userRoundsLength.eq(0)) {
return emptyResult
}
if (page > maxPages) {
return emptyResult
}
const cursor = userRoundsLength.sub(ROUNDS_PER_PAGE * page)
// If the page request is the final one we only want to retrieve the amount of rounds up to the next cursor.
const size =
maxPages === page
? userRoundsLength
.sub(ROUNDS_PER_PAGE * (page - 1)) // Previous page's cursor
.toNumber()
: ROUNDS_PER_PAGE
const userRounds = await fetchUserRounds(account, cursor.lt(0) ? 0 : cursor.toNumber(), size)
if (!userRounds) {
return emptyResult
}
const epochs = Object.keys(userRounds).map((epochStr) => Number(epochStr))
const roundData = await getRoundsData(epochs)
const claimableStatuses = await getClaimStatuses(account, epochs)
// Turn the data from the node into an Bet object that comes from the graph
const bets: Bet[] = roundData.reduce((accum, round) => {
const reduxRound = serializePredictionsRoundsResponse(round)
const ledger = userRounds[reduxRound.epoch]
const ledgerAmount = ethers.BigNumber.from(ledger.amount)
const closePrice = round.closePrice ? parseFloat(formatUnits(round.closePrice, 8)) : null
const lockPrice = round.lockPrice ? parseFloat(formatUnits(round.lockPrice, 8)) : null
const getRoundPosition = () => {
if (!closePrice) {
return null
}
if (round.closePrice.eq(round.lockPrice)) {
return BetPosition.HOUSE
}
return round.closePrice.gt(round.lockPrice) ? BetPosition.BULL : BetPosition.BEAR
}
return [
...accum,
{
id: null,
hash: null,
amount: parseFloat(formatUnits(ledgerAmount)),
position: ledger.position,
claimed: ledger.claimed,
claimedAt: null,
claimedHash: null,
claimedCRO: 0,
claimedNetCRO: 0,
createdAt: null,
updatedAt: null,
block: 0,
round: {
id: null,
epoch: round.epoch.toNumber(),
failed: false,
startBlock: null,
startAt: round.startTimestamp ? round.startTimestamp.toNumber() : null,
startHash: null,
lockAt: round.lockTimestamp ? round.lockTimestamp.toNumber() : null,
lockBlock: null,
lockPrice,
lockHash: null,
lockRoundId: round.lockOracleId ? round.lockOracleId.toString() : null,
closeRoundId: round.closeOracleId ? round.closeOracleId.toString() : null,
closeHash: null,
closeAt: null,
closePrice,
closeBlock: null,
totalBets: 0,
totalAmount: parseFloat(formatUnits(round.totalAmount)),
bullBets: 0,
bullAmount: parseFloat(formatUnits(round.bullAmount)),
bearBets: 0,
bearAmount: parseFloat(formatUnits(round.bearAmount)),
position: getRoundPosition(),
},
},
]
}, [])
return { bets, claimableStatuses, page, totalHistory: userRoundsLength.toNumber() }
})
Example #16
Source File: index.tsx From vvs-ui with GNU General Public License v3.0 | 4 votes |
Pools: React.FC = () => {
const location = useLocation()
const { t } = useTranslation()
const { account } = useWeb3React()
const { pools: poolsWithoutAutoVault, userDataLoaded } = usePools()
const [stakedOnly, setStakedOnly] = useUserPoolStakedOnly()
const [viewMode, setViewMode] = useUserPoolsViewMode()
const [numberOfPoolsVisible, setNumberOfPoolsVisible] = useState(NUMBER_OF_POOLS_VISIBLE)
const { observerRef, isIntersecting } = useIntersectionObserver()
const [searchQuery, setSearchQuery] = useState('')
const [sortOption, setSortOption] = useState('hot')
const chosenPoolsLength = useRef(0)
const {
userData: { vvsAtLastUserAction, userShares },
fees: { performanceFee },
pricePerFullShare,
totalVvsInVault,
} = useVvsVault()
const accountHasVaultShares = userShares && userShares.gt(0)
const performanceFeeAsDecimal = performanceFee && performanceFee / 100
const pools = useMemo(() => {
const vvsPool = poolsWithoutAutoVault.find((pool) => pool.sousId === 0)
const vvsAutoVault = { ...vvsPool, isAutoVault: true }
return [vvsAutoVault, ...poolsWithoutAutoVault]
}, [poolsWithoutAutoVault])
// TODO aren't arrays in dep array checked just by reference, i.e. it will rerender every time reference changes?
const [finishedPools, openPools] = useMemo(() => partition(pools, (pool) => pool.isFinished), [pools])
const stakedOnlyFinishedPools = useMemo(
() =>
finishedPools.filter((pool) => {
if (pool.isAutoVault) {
return accountHasVaultShares
}
return pool.userData && new BigNumber(pool.userData.stakedBalance).isGreaterThan(0)
}),
[finishedPools, accountHasVaultShares],
)
const stakedOnlyOpenPools = useMemo(
() =>
openPools.filter((pool) => {
if (pool.isAutoVault) {
return accountHasVaultShares
}
return pool.userData && new BigNumber(pool.userData.stakedBalance).isGreaterThan(0)
}),
[openPools, accountHasVaultShares],
)
const hasStakeInFinishedPools = stakedOnlyFinishedPools.length > 0
usePollFarmsPublicData()
useFetchVvsVault()
useFetchPublicPoolsData()
useFetchUserPools(account)
useEffect(() => {
if (isIntersecting) {
setNumberOfPoolsVisible((poolsCurrentlyVisible) => {
if (poolsCurrentlyVisible <= chosenPoolsLength.current) {
return poolsCurrentlyVisible + NUMBER_OF_POOLS_VISIBLE
}
return poolsCurrentlyVisible
})
}
}, [isIntersecting])
const showFinishedPools = location.pathname.includes('history')
const handleChangeSearchQuery = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchQuery(event.target.value)
}
const handleSortOptionChange = (option: OptionProps): void => {
setSortOption(option.value)
}
const sortPools = (poolsToSort: DeserializedPool[]) => {
switch (sortOption) {
case 'apr':
// Ternary is needed to prevent pools without APR (like MIX) getting top spot
return orderBy(
poolsToSort,
(pool: DeserializedPool) => (pool.apr ? getAprData(pool, performanceFeeAsDecimal).apr : 0),
'desc',
)
case 'earned':
return orderBy(
poolsToSort,
(pool: DeserializedPool) => {
if (!pool.userData || !pool.earningTokenPrice) {
return 0
}
return pool.isAutoVault
? getVvsVaultEarnings(
account,
vvsAtLastUserAction,
userShares,
pricePerFullShare,
pool.earningTokenPrice,
).autoUsdToDisplay
: pool.userData.pendingReward.times(pool.earningTokenPrice).toNumber()
},
'desc',
)
case 'totalStaked':
return orderBy(
poolsToSort,
(pool: DeserializedPool) => {
let totalStaked = Number.NaN
if (pool.isAutoVault) {
if (totalVvsInVault.isFinite()) {
totalStaked = +formatUnits(
ethers.BigNumber.from(totalVvsInVault.toString()),
pool.stakingToken.decimals,
)
}
} else if (pool.sousId === 0) {
if (pool.totalStaked?.isFinite() && totalVvsInVault.isFinite()) {
const manualVvsTotalMinusAutoVault = ethers.BigNumber.from(pool.totalStaked.toString()).sub(
totalVvsInVault.toString(),
)
totalStaked = +formatUnits(manualVvsTotalMinusAutoVault, pool.stakingToken.decimals)
}
} else if (pool.totalStaked?.isFinite()) {
totalStaked = +formatUnits(ethers.BigNumber.from(pool.totalStaked.toString()), pool.stakingToken.decimals)
}
return Number.isFinite(totalStaked) ? totalStaked : 0
},
'desc',
)
default:
return poolsToSort
}
}
let chosenPools
if (showFinishedPools) {
chosenPools = stakedOnly ? stakedOnlyFinishedPools : finishedPools
} else {
chosenPools = stakedOnly ? stakedOnlyOpenPools : openPools
}
if (searchQuery) {
const lowercaseQuery = latinise(searchQuery.toLowerCase())
chosenPools = chosenPools.filter((pool) =>
latinise(pool.earningToken.symbol.toLowerCase()).includes(lowercaseQuery),
)
}
chosenPools = sortPools(chosenPools).slice(0, numberOfPoolsVisible)
chosenPoolsLength.current = chosenPools.length
const cardLayout = (
<CardLayout>
{chosenPools.map((pool) =>
pool.isAutoVault ? (
<VvsVaultCard key="auto-vvs" pool={pool} showStakedOnly={stakedOnly} />
) : (
<PoolCard key={pool.sousId} pool={pool} account={account} />
),
)}
</CardLayout>
)
const tableLayout = <PoolsTable pools={chosenPools} account={account} userDataLoaded={userDataLoaded} />
return (
<>
<StyledPageHeader>
<MineBackgroundWrapper>
<MineBackground />
</MineBackgroundWrapper>
<Flex
style={{ position: 'relative' }}
justifyContent="space-between"
flexDirection={['column', null, null, 'row']}
>
<Flex flex="1" flexDirection="column" mr={['8px', 0]}>
<StyledHeading as="h1" scale="xxl" mb="24px" weight={500}>
{t('Glitter Mines')}
</StyledHeading>
<StyledHeading scale="md">{t('Carts full of bling bling')}</StyledHeading>
<StyledHeading scale="md" mb="36px" mt="14px">
{t('Stake VVS for more VVS')}
</StyledHeading>
</Flex>
{/* <Flex flex="1" height="fit-content" justifyContent="center" alignItems="center" mt={['24px', null, '0']}>
<HelpButton />
<BountyCard />
</Flex> */}
</Flex>
{/* <PoolControls>
<PoolTabButtons
stakedOnly={stakedOnly}
setStakedOnly={setStakedOnly}
hasStakeInFinishedPools={hasStakeInFinishedPools}
viewMode={viewMode}
setViewMode={setViewMode}
/>
<FilterContainer>
<LabelWrapper>
<ControlStretch>
<Select
options={[
{
label: t('Hot'),
value: 'hot',
},
{
label: t('APR'),
value: 'apr',
},
{
label: t('Earned'),
value: 'earned',
},
{
label: t('Total staked'),
value: 'totalStaked',
},
]}
onOptionChange={handleSortOptionChange}
/>
</ControlStretch>
</LabelWrapper>
<LabelWrapper style={{ marginLeft: 16 }}>
<SearchInput onChange={handleChangeSearchQuery} placeholder="Search Pools" />
</LabelWrapper>
</FilterContainer>
</PoolControls> */}
</StyledPageHeader>
<Page style={{ marginTop: '-90px', position: 'relative' }}>
{showFinishedPools && (
<Text fontSize="20px" color="failure" pb="32px">
{t('These pools are no longer distributing rewards. Please unstake your tokens.')}
</Text>
)}
{account && !userDataLoaded && stakedOnly && (
<Flex justifyContent="center" mb="4px">
<Loading />
</Flex>
)}
{viewMode === ViewMode.CARD ? cardLayout : tableLayout}
<div ref={observerRef} />
{/* <Image
mx="auto"
mt="12px"
src="/images/decorations/3d-syrup-bunnies.png"
alt="VVS illustration"
width={192}
height={184.5}
/> */}
</Page>
</>
)
}