Skip to content

Commit

Permalink
tests: rework eth events handling, allow to skip finalization
Browse files Browse the repository at this point in the history
  • Loading branch information
vklachkov committed Feb 19, 2025
1 parent dd88ae5 commit a2f4ceb
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 169 deletions.
29 changes: 1 addition & 28 deletions js-packages/test-utils/eth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ class CreateCollectionTransaction {

const collectionAddress = this.helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId);
const collectionId = this.helper.ethAddress.extractCollectionId(collectionAddress);
const events = this.helper.eth.normalizeEvents(result.events);
const events = result.events.normalized;
const collection = await this.helper.ethNativeContract.collectionById(collectionId, this.data.collectionMode, this.signer, this.mergeDeprecated);

return {collectionId, collectionAddress, events, collection};
Expand Down Expand Up @@ -421,33 +421,6 @@ class EthGroup extends EthGroupBase {
return before - after;
}

normalizeEvents(events: any): NormalizedEvent[] {
const output = [];
for(const key of Object.keys(events)) {
if(key.match(/^[0-9]+$/)) {
output.push(events[key]);
} else if(Array.isArray(events[key])) {
output.push(...events[key]);
} else {
output.push(events[key]);
}
}
output.sort((a, b) => a.logIndex - b.logIndex);
return output.map(({address, event, returnValues}) => {
const args: { [key: string]: string } = {};
for(const key of Object.keys(returnValues)) {
if(!key.match(/^[0-9]+$/)) {
args[key] = returnValues[key];
}
}
return {
address,
event,
args,
};
});
}

async calculateFee(address: ICrossAccountId, code: () => Promise<any>): Promise<bigint> {
const wrappedCode = async () => {
await code();
Expand Down
145 changes: 97 additions & 48 deletions js-packages/test-utils/eth/tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,47 +5,59 @@

import * as web3 from 'web3';

const RECEIPT_TIMEOUT: number = 48 * 6;
const REQUIRED_CONFIRMATION: number = 12;

type Contract = any; /* web3contract.Contract */
type Transaction = any; /* web3contract.ContractSendMethod */
type TransactionOptions = any; /* web3contract.SendOptions */
type Receipt = any; /* web3eth.TransactionReceipt */
type Event = any; /* web3core.EventLog */

const RECEIPT_TIMEOUT: number = 48 * 6;
const REQUIRED_CONFIRMATION: number = 12;
const SKIP_FINALIZATION_WAIT: boolean = ['1', 'true'].includes(process.env.SKIP_FINALIZATION_WAIT || '');

export async function sendAndWait(
web3: web3.default,
tx: any /* web3contract.ContractSendMethod */,
options?: any /* web3contract.SendOptions */,
tx: Transaction,
options?: TransactionOptions,
): Promise<Receipt> {
// The contract is needed to get the list of events in addition to the receipt.
// Usually, the contract can be taken from the transaction itself,
// but if some proxy is used, getPastEvents('allEvent') will not work.
// In this case, user must pass the original contract in the options.
const contract = options?.contract || tx._parent;

// Since web3.js does not validate the options and simply serializes them "as is",
// we must remove the additional fields manually
if(options?.contract) options.contract = undefined;

try {
return await sendAndWaitInternal(web3, tx, options);
return await new Promise((resolve, reject) => {
tx.send(options)
.on('transactionHash', (hash: string) => {
// We manually wait recept and finalization because
// in this version of web3.js receiving receipt
// and finalization waiting is broken.
//
// With default web3.js implementation we regullary got
// 'transaction not mined in 50 blocks' errors
// and have issues with waiting for finalization.
waitForTransaction(web3, contract, hash)
.then((receipt: Receipt) => resolve(receipt))
.catch((error: Error) => reject(error));
})
.on('error', (error: any) => {
reject(error);
});
});
} catch (error: any) {
console.error('sendAndWait failed with error:', error);
console.error('ethereum transaction failed:', error);
throw error;
}
}

function sendAndWaitInternal(
web3: web3.default,
tx: any /* web3contract.ContractSendMethod */,
options?: any /* web3contract.SendOptions */,
): Promise<Receipt> {
return new Promise((resolve, reject) => {
tx.send(options as any)
.on('transactionHash', (hash: string) => {
const contract = tx._parent;
waitForTransaction(web3, contract, hash)
.then((receipt: Receipt) => resolve(receipt))
.catch((error: Error) => reject(error));
})
.on('error', (error: any) => {
reject(error);
});
});
}

async function waitForTransaction(
web3: web3.default,
contract: any,
contract: Contract,
txHash: string,
): Promise<Receipt> {
if(!txHash) {
Expand All @@ -54,19 +66,21 @@ async function waitForTransaction(

const receipt: Receipt = await getReceipt(web3, txHash);

while(await web3.eth.getBlockNumber() - receipt.blockNumber < REQUIRED_CONFIRMATION) {
await new Promise(resolve => setTimeout(resolve, 1000));
if(!SKIP_FINALIZATION_WAIT) {
while(await web3.eth.getBlockNumber() - receipt.blockNumber < REQUIRED_CONFIRMATION) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

const eventsOptions = {fromBlock: receipt.blockNumber, toBlock: await web3.eth.getBlockNumber()};
const events = await contract.getPastEvents('allEvents', eventsOptions);
const eventsOptions = {fromBlock: receipt.blockNumber, toBlock: receipt.blockNumber};
const events: Event[] = await contract.getPastEvents('allEvents', eventsOptions);
if(!events) {
return new Error(`getPastEvents('allEvents', ${JSON.stringify(eventsOptions)}) returns ${events} instead of list of events`);
return new Error(`getPastEvents returns ${events} instead of list of events`);
}

return {
...receipt,
events: repackEvents(txHash, events),
events: new Events(events.filter(ev => ev.transactionHash == txHash)),
};
}

Expand All @@ -87,24 +101,59 @@ async function getReceipt(
throw new Error(`timeout ${RECEIPT_TIMEOUT} seconds waiting for getTransactionReceipt(${txHash})`);
}

function repackEvents(
transactionHash: string,
events: Event[],
) {
const hackyEvents = [];
class Events {
events: Event[];
normalizedEvents: any[] | undefined;

constructor(events: Event[]) {
console.log('events:', events);

for(const event of events) {
if(event.transactionHash !== transactionHash) {
continue;
}
this.events = events;
this.normalizedEvents = undefined;

return new Proxy(this, {
get: (target, key, _receiver) => target.get(key),
});
}

get(key: string | symbol): Event | undefined {
key = key.toString();

hackyEvents.push(event);
if(key === 'normalized') {
return this.normalizeEvents();
} else if(+key) {
return this.events[+key];
} else {
return this.findEvent(key);
}
}

// Events is array, but our tests expect they can access events by name.
if(!hackyEvents[event.event]) {
hackyEvents[event.event] = event;
normalizeEvents(): any[] {
if(this.normalizedEvents) {
return this.normalizedEvents;
}

this.normalizedEvents = structuredClone(this.events)
.sort((a, b) => a.logIndex - b.logIndex)
.map(({address, event, returnValues}) => {
const args: { [key: string]: string } = {};
for(const key of Object.keys(returnValues)) {
if(!key.match(/^[0-9]+$/)) {
args[key] = returnValues[key];
}
}

return {
address,
event,
args,
};
});

return this.normalizedEvents;
}

return hackyEvents;
}
findEvent(key: string): Event | undefined {
return this.events.find(ev => ev.event === key);
}
}
42 changes: 17 additions & 25 deletions js-packages/tests/eth/collectionSponsoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ describe('evm nft collection sponsoring', () => {

const result = await sendAndWait(helper.getWeb3(), contract.methods.mint(minter));

const events = helper.eth.normalizeEvents(result.events);
expect(events).to.be.deep.equal([
expect(result.events.normalized).to.be.deep.equal([
{
address: collectionAddress,
event: 'Transfer',
Expand Down Expand Up @@ -173,8 +172,8 @@ describe('evm nft collection sponsoring', () => {
// User can mint token without balance:
{
const result = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintCross(user, [{key: 'key', value: Buffer.from('Value')}]), {from: user.eth});
const event = helper.eth.normalizeEvents(result.events)
.find(event => event.event === 'Transfer');
const event = result.events.normalized
.find((event: any) => event.event === 'Transfer');

expect(event).to.be.deep.equal({
address: collectionAddress,
Expand Down Expand Up @@ -245,8 +244,8 @@ describe('evm nft collection sponsoring', () => {
// User can mint token without balance:
{
const result = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintCross(user, []), {from: user.eth});
const event = helper.eth.normalizeEvents(result.events)
.find(event => event.event === 'Transfer');
const event = result.events.normalized
.find((event: any) => event.event === 'Transfer');

expect(event).to.be.deep.equal({
address: collectionAddress,
Expand Down Expand Up @@ -366,8 +365,8 @@ describe('evm nft collection sponsoring', () => {
const mintingResult = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintWithTokenURI(user, 'Test URI'), {from: user});
const tokenId = mintingResult.events.Transfer.returnValues.tokenId;

const event = helper.eth.normalizeEvents(mintingResult.events)
.find(event => event.event === 'Transfer');
const event = mintingResult.events.normalized
.find((event: any) => event.event === 'Transfer');
const address = helper.ethAddress.fromCollectionId(collectionId);

expect(event).to.be.deep.equal({
Expand Down Expand Up @@ -462,8 +461,7 @@ describe('evm RFT collection sponsoring', () => {
break;
}

const events = helper.eth.normalizeEvents(mintingResult.events);
expect(events).to.deep.include({
expect(mintingResult.events.normalized).to.deep.include({
address: collectionAddress,
event: 'Transfer',
args: {
Expand Down Expand Up @@ -562,9 +560,8 @@ describe('evm RFT collection sponsoring', () => {
// User can mint token without balance:
{
const result = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintWithTokenURI(user, 'Test URI'), {from: user});
const events = helper.eth.normalizeEvents(result.events);

expect(events).to.deep.include({
expect(result.events.normalized).to.deep.include({
address: collectionAddress,
event: 'Transfer',
args: {
Expand Down Expand Up @@ -626,10 +623,9 @@ describe('evm RFT collection sponsoring', () => {
const mintingResult = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintWithTokenURI(user, 'Test URI'), {from: user});
const tokenId = mintingResult.events.Transfer.returnValues.tokenId;

const events = helper.eth.normalizeEvents(mintingResult.events);
const address = helper.ethAddress.fromCollectionId(collectionId);

expect(events).to.deep.include({
expect(mintingResult.events.normalized).to.deep.include({
address,
event: 'Transfer',
args: {
Expand Down Expand Up @@ -680,9 +676,7 @@ describe('evm RFT collection sponsoring', () => {
const mintingResult = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintWithTokenURI(user, 'Test URI'), {from: user});
const tokenId = mintingResult.events.Transfer.returnValues.tokenId;

const events = helper.eth.normalizeEvents(mintingResult.events);

expect(events).to.deep.include({
expect(mintingResult.events.normalized).to.deep.include({
address: collectionAddress,
event: 'Transfer',
args: {
Expand Down Expand Up @@ -738,15 +732,13 @@ describe('evm RFT collection sponsoring', () => {
{
const nextTokenId = await collectionEvm.methods.nextTokenId().call();
expect(nextTokenId).to.be.equal('1');
const mintingResult = await sendAndWait(helper.getWeb3(), collectionEvm.methods.mintWithTokenURI(
user,
'Test URI',
), {from: user});

const events = helper.eth.normalizeEvents(mintingResult.events);

const mintingResult = await sendAndWait(
helper.getWeb3(),
collectionEvm.methods.mintWithTokenURI(user, 'Test URI'),
{from: user},
);

expect(events).to.deep.include({
expect(mintingResult.events.normalized).to.deep.include({
address: collectionAddress,
event: 'Transfer',
args: {
Expand Down
13 changes: 5 additions & 8 deletions js-packages/tests/eth/contractSponsoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,7 @@ describe('Sponsoring EVM contracts', () => {
expect(actualSponsor.sub).to.eq('0');

// 2. Events should be:
const ethEvents = helper.eth.helper.eth.normalizeEvents(result.events);
expect(ethEvents).to.be.deep.equal([
expect(result.events.normalized).to.be.deep.equal([
{
address: flipper.options.address,
event: 'ContractSponsorSet',
Expand Down Expand Up @@ -115,8 +114,7 @@ describe('Sponsoring EVM contracts', () => {
expect(await helpers.methods.hasPendingSponsor(flipper.options.address).call()).to.be.true;

// 2. Events should be:
const events = helper.eth.normalizeEvents(result.events);
expect(events).to.be.deep.equal([
expect(result.events.normalized).to.be.deep.equal([
{
address: flipper.options.address,
event: 'ContractSponsorSet',
Expand Down Expand Up @@ -161,8 +159,7 @@ describe('Sponsoring EVM contracts', () => {
expect(actualSponsor.sub).to.eq('0');

// 2. Events should be:
const events = helper.eth.normalizeEvents(result.events);
expect(events).to.be.deep.equal([
expect(result.events.normalized).to.be.deep.equal([
{
address: flipper.options.address,
event: 'ContractSponsorshipConfirmed',
Expand Down Expand Up @@ -208,13 +205,13 @@ describe('Sponsoring EVM contracts', () => {
await sendAndWait(helper.getWeb3(), helpers.methods.setSponsor(flipper.options.address, sponsor));
await sendAndWait(helper.getWeb3(), helpers.methods.confirmSponsorship(flipper.options.address), {from: sponsor});
expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.true;

// 1. Can remove sponsor:
const result = await sendAndWait(helper.getWeb3(), helpers.methods.removeSponsor(flipper.options.address));
expect(await helpers.methods.hasSponsor(flipper.options.address).call()).to.be.false;

// 2. Events should be:
const events = helper.eth.normalizeEvents(result.events);
expect(events).to.be.deep.equal([
expect(result.events.normalized).to.be.deep.equal([
{
address: flipper.options.address,
event: 'ContractSponsorRemoved',
Expand Down
Loading

0 comments on commit a2f4ceb

Please sign in to comment.