diff --git a/contracts/package.json b/contracts/package.json index 131fc7d74..8bad5cc62 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -24,7 +24,6 @@ }, "dependencies": { "@chainlink/contracts": "^0.4.2", - "@chainlink/starknet": "^1.0.0", "@openzeppelin/contracts": "^4.7.3", "axios": "^0.24.0" } diff --git a/contracts/test/account.ts b/contracts/test/account.ts new file mode 100644 index 000000000..5da60b364 --- /dev/null +++ b/contracts/test/account.ts @@ -0,0 +1,100 @@ +import { Account, RpcProvider, ec, uint256, constants } from 'starknet' + +export const ERC20_ADDRESS = '0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7' + +export const DEVNET_URL = 'http://127.0.0.1:5050' +const DEVNET_NAME = 'devnet' +// This function loads options from the environment. +// It returns options for Devnet as default when nothing is configured in the environment. +export const makeFunderOptsFromEnv = () => { + const network = process.env.NETWORK || DEVNET_NAME + const gateway = process.env.NODE_URL || DEVNET_URL + const accountAddr = process.env.ACCOUNT?.toLowerCase() + const keyPair = ec.starkCurve.utils.randomPrivateKey() + + return { network, gateway, accountAddr, keyPair } +} + +interface FundAccounts { + account: string + amount: number +} + +interface FunderOptions { + network?: string + gateway?: string + accountAddr?: string + keyPair?: Uint8Array +} + +// Define the Strategy to use depending on the network. +export class Funder { + private opts: FunderOptions + private strategy: IFundingStrategy + + constructor(opts: FunderOptions) { + this.opts = opts + if (this.opts.network === DEVNET_NAME) { + this.strategy = new DevnetFundingStrategy() + return + } + this.strategy = new AllowanceFundingStrategy() + } + + // This function adds some funds to pre-deployed account that we are using in our test. + public async fund(accounts: FundAccounts[]) { + await this.strategy.fund(accounts, this.opts) + } +} + +interface IFundingStrategy { + fund(accounts: FundAccounts[], opts: FunderOptions): Promise +} + +// Fund the Account on Devnet +class DevnetFundingStrategy implements IFundingStrategy { + public async fund(accounts: FundAccounts[], opts: FunderOptions) { + accounts.forEach(async (account) => { + const body = { + address: account.account, + amount: account.amount, + lite: true, + } + await fetch(`${opts.gateway}/mint`, { + method: 'post', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/json' }, + }) + }) + } +} + +// Fund the Account on Testnet +class AllowanceFundingStrategy implements IFundingStrategy { + public async fund(accounts: FundAccounts[], opts: FunderOptions) { + const provider = new RpcProvider({ + nodeUrl: constants.NetworkName.SN_GOERLI, + }) + + const operator = new Account(provider, opts.accountAddr, opts.keyPair) + + for (const account of accounts) { + const data = [ + account.account, + uint256.bnToUint256(account.amount).low.toString(), + uint256.bnToUint256(account.amount).high.toString(), + ] + const nonce = await operator.getNonce() + const hash = await operator.execute( + { + contractAddress: ERC20_ADDRESS, + entrypoint: 'transfer', + calldata: data, + }, + undefined, + { nonce }, + ) + await provider.waitForTransaction(hash.transaction_hash) + } + } +} diff --git a/contracts/test/emergency/StarknetValidator.test.ts b/contracts/test/emergency/StarknetValidator.test.ts index 54385109f..5eedc51fa 100644 --- a/contracts/test/emergency/StarknetValidator.test.ts +++ b/contracts/test/emergency/StarknetValidator.test.ts @@ -1,6 +1,6 @@ import { ethers, starknet, network } from 'hardhat' import { BigNumber, Contract, ContractFactory } from 'ethers' -import { hash, number } from 'starknet' +import { hash } from 'starknet' import { Account, StarknetContractFactory, @@ -13,7 +13,8 @@ import { abi as aggregatorAbi } from '../../artifacts/@chainlink/contracts/src/v import { abi as accessControllerAbi } from '../../artifacts/@chainlink/contracts/src/v0.8/interfaces/AccessControllerInterface.sol/AccessControllerInterface.json' import { abi as starknetMessagingAbi } from '../../artifacts/vendor/starkware-libs/cairo-lang/src/starkware/starknet/solidity/IStarknetMessaging.sol/IStarknetMessaging.json' import { deployMockContract, MockContract } from '@ethereum-waffle/mock-contract' -import { account, addCompilationToNetwork, expectSuccessOrDeclared } from '@chainlink/starknet' +import * as account from '../account' +import { addCompilationToNetwork, expectSuccessOrDeclared } from '../utils' describe('StarknetValidator', () => { /** Fake L2 target */ diff --git a/contracts/test/ocr2/aggregator.test.ts b/contracts/test/ocr2/aggregator.test.ts index 3d53e7ccd..2b07e55d0 100644 --- a/contracts/test/ocr2/aggregator.test.ts +++ b/contracts/test/ocr2/aggregator.test.ts @@ -3,7 +3,8 @@ import { starknet } from 'hardhat' import { ec, hash, num } from 'starknet' import { Account, StarknetContract, StarknetContractFactory } from 'hardhat/types/runtime' import { TIMEOUT } from '../constants' -import { account, expectInvokeError, expectSuccessOrDeclared } from '@chainlink/starknet' +import { expectInvokeError, expectSuccessOrDeclared } from '../utils' +import * as account from '../account' import { bytesToFelts } from '@chainlink/starknet-gauntlet' interface Oracle { diff --git a/contracts/test/utils.ts b/contracts/test/utils.ts new file mode 100644 index 000000000..6bf736f85 --- /dev/null +++ b/contracts/test/utils.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai' +import { artifacts, network } from 'hardhat' + +// This function adds the build info to the test network so that the network knows +// how to handle custom errors. It is automatically done when testing +// against the default hardhat network. +export const addCompilationToNetwork = async (fullyQualifiedName: string) => { + if (network.name !== 'hardhat') { + // This is so that the network can know about custom errors. + // Running against the provided hardhat node does this automatically. + + const buildInfo = await artifacts.getBuildInfo(fullyQualifiedName) + if (!buildInfo) { + throw Error('Cannot find build info') + } + const { solcVersion, input, output } = buildInfo + console.log('Sending compilation result for StarknetValidator test') + await network.provider.request({ + method: 'hardhat_addCompilationResult', + params: [solcVersion, input, output], + }) + console.log('Successfully sent compilation result for StarknetValidator test') + } +} + +export const expectInvokeError = async (invoke: Promise, expected?: string) => { + try { + await invoke + } catch (err: any) { + expectInvokeErrorMsg(err?.message, expected) + return // force + } + expect.fail("Unexpected! Invoke didn't error!?") +} + +export const expectInvokeErrorMsg = (actual: string, expected?: string) => { + // Match transaction error + expect(actual).to.deep.contain('TRANSACTION_FAILED') + // Match specific error + if (expected) expectSpecificMsg(actual, expected) +} + +export const expectCallError = async (call: Promise, expected?: string) => { + try { + await call + } catch (err: any) { + expectCallErrorMsg(err?.message, expected) + return // force + } + expect.fail("Unexpected! Call didn't error!?") +} + +export const expectCallErrorMsg = (actual: string, expected?: string) => { + // Match call error + expect(actual).to.deep.contain('Could not perform call') + // Match specific error + if (expected) expectSpecificMsg(actual, expected) +} + +export const expectSpecificMsg = (actual: string, expected: string) => { + // The error message is displayed as a felt hex string, so we need to convert the text. + // ref: https://github.com/starkware-libs/cairo-lang/blob/c954f154bbab04c3fb27f7598b015a9475fc628e/src/starkware/starknet/business_logic/execution/execute_entry_point.py#L223 + const expectedHex = '0x' + Buffer.from(expected, 'utf8').toString('hex') + const errorMessage = `Execution was reverted; failure reason: [${expectedHex}]` + if (!actual.includes(errorMessage)) { + expect.fail(`\nActual: ${actual}\n\nExpected:\n\tFelt hex: ${expectedHex}\n\tText: ${expected}`) + } +} + +// Starknet v0.11.0 and higher only allow declaring a class once: +// https://github.com/starkware-libs/starknet-specs/pull/85 +export const expectSuccessOrDeclared = async (declareContractPromise: Promise) => { + try { + await declareContractPromise + } catch (err: any) { + if (/Class with hash 0x[0-9a-f]+ is already declared\./.test(err?.message)) { + return // force + } + expect.fail(err) + } +}