-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for changeTokenBalance(s) matchers
- Loading branch information
Showing
5 changed files
with
819 additions
and
0 deletions.
There are no files selected for viewing
177 changes: 177 additions & 0 deletions
177
packages/hardhat-chai-matchers/src/changeTokenBalance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
import { BigNumber, BigNumberish, Contract, providers } from "ethers"; | ||
import { ensure } from "./calledOnContract/utils"; | ||
import { Account, getAddressOf } from "./misc/account"; | ||
|
||
type TransactionResponse = providers.TransactionResponse; | ||
|
||
interface Token extends Contract { | ||
balanceOf(address: string, overrides?: any): Promise<BigNumber>; | ||
} | ||
|
||
export function supportChangeTokenBalance(Assertion: Chai.AssertionStatic) { | ||
Assertion.addMethod( | ||
"changeTokenBalance", | ||
function ( | ||
this: any, | ||
token: Token, | ||
account: Account | string, | ||
balanceChange: BigNumberish | ||
) { | ||
const subject = this._obj; | ||
|
||
checkToken(token, "changeTokenBalance"); | ||
|
||
const derivedPromise = Promise.all([ | ||
getBalanceChange(subject, token, account), | ||
getAddressOf(account), | ||
getTokenDescription(token), | ||
]).then(([actualChange, address, tokenDescription]) => { | ||
this.assert( | ||
actualChange.eq(BigNumber.from(balanceChange)), | ||
`Expected "${address}" to change its balance of ${tokenDescription} by ${balanceChange}, ` + | ||
`but it has changed by ${actualChange}`, | ||
`Expected "${address}" to not change its balance of ${tokenDescription} by ${balanceChange}, but it did`, | ||
balanceChange, | ||
actualChange | ||
); | ||
}); | ||
|
||
this.then = derivedPromise.then.bind(derivedPromise); | ||
this.catch = derivedPromise.catch.bind(derivedPromise); | ||
|
||
return this; | ||
} | ||
); | ||
|
||
Assertion.addMethod( | ||
"changeTokenBalances", | ||
function ( | ||
this: any, | ||
token: Token, | ||
accounts: Array<Account | string>, | ||
balanceChanges: BigNumberish[] | ||
) { | ||
const subject = this._obj; | ||
|
||
checkToken(token, "changeTokenBalances"); | ||
|
||
if (accounts.length !== balanceChanges.length) { | ||
throw new Error( | ||
`The number of accounts (${accounts.length}) is different than the number of expected balance changes (${balanceChanges.length})` | ||
); | ||
} | ||
|
||
const balanceChangesPromise = Promise.all( | ||
accounts.map((account) => getBalanceChange(subject, token, account)) | ||
); | ||
const addressesPromise = Promise.all(accounts.map(getAddressOf)); | ||
|
||
const derivedPromise = Promise.all([ | ||
balanceChangesPromise, | ||
addressesPromise, | ||
getTokenDescription(token), | ||
]).then(([actualChanges, addresses, tokenDescription]) => { | ||
this.assert( | ||
actualChanges.every((change, ind) => | ||
change.eq(BigNumber.from(balanceChanges[ind])) | ||
), | ||
`Expected ${addresses} to change their balance of ${tokenDescription} by ${balanceChanges}, ` + | ||
`but it has changed by ${actualChanges}`, | ||
`Expected ${addresses} to not change their balance of ${tokenDescription} by ${balanceChanges}, but they did`, | ||
balanceChanges.map((balanceChange) => balanceChange.toString()), | ||
actualChanges.map((actualChange) => actualChange.toString()) | ||
); | ||
}); | ||
|
||
this.then = derivedPromise.then.bind(derivedPromise); | ||
this.catch = derivedPromise.catch.bind(derivedPromise); | ||
|
||
return this; | ||
} | ||
); | ||
} | ||
|
||
function checkToken(token: unknown, method: string) { | ||
if (typeof token !== "object" || token === null || !("functions" in token)) { | ||
throw new Error( | ||
`The first argument of ${method} must be the contract instance of the token` | ||
); | ||
} else if ((token as any).functions.balanceOf === undefined) { | ||
throw new Error("The given contract instance is not an ERC20 token"); | ||
} | ||
} | ||
|
||
export async function getBalanceChange( | ||
transaction: | ||
| TransactionResponse | ||
| Promise<TransactionResponse> | ||
| (() => TransactionResponse) | ||
| (() => Promise<TransactionResponse>), | ||
token: Token, | ||
account: Account | string | ||
) { | ||
const hre = await import("hardhat"); | ||
const provider = hre.network.provider; | ||
|
||
let txResponse: TransactionResponse; | ||
|
||
if (typeof transaction === "function") { | ||
txResponse = await transaction(); | ||
} else { | ||
txResponse = await transaction; | ||
} | ||
|
||
const txReceipt = await txResponse.wait(); | ||
const txBlockNumber = txReceipt.blockNumber; | ||
|
||
const block = await provider.send("eth_getBlockByHash", [ | ||
txReceipt.blockHash, | ||
false, | ||
]); | ||
|
||
ensure( | ||
block.transactions.length === 1, | ||
Error, | ||
"Multiple transactions found in block" | ||
); | ||
|
||
const address = await getAddressOf(account); | ||
|
||
const balanceAfter = await token.balanceOf(address, { | ||
blockTag: txBlockNumber, | ||
}); | ||
|
||
const balanceBefore = await token.balanceOf(address, { | ||
blockTag: txBlockNumber - 1, | ||
}); | ||
|
||
return BigNumber.from(balanceAfter).sub(balanceBefore); | ||
} | ||
|
||
let tokenDescriptionsCache: Record<string, string> = {}; | ||
/** | ||
* Get a description for the given token. Use the symbol of the token if | ||
* possible; if it doesn't exist, the name is used; if the name doesn't | ||
* exist, the address of the token is used. | ||
*/ | ||
async function getTokenDescription(token: Token): Promise<string> { | ||
if (tokenDescriptionsCache[token.address] === undefined) { | ||
let tokenDescription = `<token at ${token.address}>`; | ||
try { | ||
tokenDescription = await token.symbol(); | ||
} catch (e) { | ||
try { | ||
tokenDescription = await token.name(); | ||
} catch (e2) {} | ||
} | ||
|
||
tokenDescriptionsCache[token.address] = tokenDescription; | ||
} | ||
|
||
return tokenDescriptionsCache[token.address]; | ||
} | ||
|
||
// only used by tests | ||
export function clearTokenDescriptionsCache() { | ||
tokenDescriptionsCache = {}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.