feat: support Renew events from the name registry contract (#574)

* feat: support Renew events from the name registry contract

* represent renews as NameRegistryEvents

* log both success and errors in eth events provider

* fix import syntax
This commit is contained in:
Paul Fletcher-Hill
2023-02-15 11:29:34 -08:00
committed by GitHub
parent a31de82054
commit 8068ec61d6
4 changed files with 156 additions and 98 deletions

View File

@@ -1,10 +1,11 @@
import { Event } from '@ethersproject/contracts';
import { BaseProvider, Block, TransactionReceipt, TransactionResponse } from '@ethersproject/providers';
import * as protobufs from '@farcaster/protobufs';
import { Factories, bytesToHexString, hexStringToBytes } from '@farcaster/utils';
import { Factories, hexStringToBytes } from '@farcaster/utils';
import { BigNumber, Contract, utils } from 'ethers';
import { IdRegistry, NameRegistry } from '~/eth/abis';
import { EthEventsProvider } from '~/eth/ethEventsProvider';
import { bytesToBytes32 } from '~/eth/utils';
import { getIdRegistryEvent } from '~/storage/db/idRegistryEvent';
import { jestRocksDB } from '~/storage/db/jestUtils';
import { getNameRegistryEvent } from '~/storage/db/nameRegistryEvent';
@@ -163,7 +164,7 @@ describe('process events', () => {
'Transfer',
'0x000001',
'0x000002',
BigNumber.from(bytesToHexString(fname)._unsafeUnwrap()),
bytesToBytes32(fname)._unsafeUnwrap(),
new MockEvent(blockNumber++, '0xb00001', '0x400001', 0)
);
expect(rTransfer).toBeTruthy();
@@ -177,25 +178,25 @@ describe('process events', () => {
// The event is now available
expect((await getNameRegistryEvent(db, fname)).fname).toEqual(fname);
// // Renew the fname
// mockNameRegistry.emit(
// 'Renew',
// BigNumber.from(bytesToHexString(fname)._unsafeUnwrap()),
// BigNumber.from(1000),
// new MockEvent(blockNumber++, '0xb00002', '0x400002', 0)
// );
// // The event is not immediately available, since it has to wait for confirmations. We should still get the Transfer event
// expect((await getNameRegistryEvent(db, fname)).type).toEqual(
// protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_TRANSFER
// );
// Renew the fname
mockNameRegistry.emit(
'Renew',
bytesToBytes32(fname)._unsafeUnwrap(),
BigNumber.from(Date.now() + 1000),
new MockEvent(blockNumber++, '0xb00002', '0x400002', 0)
);
// The event is not immediately available, since it has to wait for confirmations. We should still get the Transfer event
expect(await getNameRegistryEvent(db, fname)).toMatchObject({
type: protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_TRANSFER,
});
// // Add 6 confirmations
// blockNumber = await addBlocks(blockNumber, 6);
// Add 6 confirmations
blockNumber = await addBlocks(blockNumber, 6);
// // The renew event is now available
// expect((await getNameRegistryEvent(db, fname)).fname).toEqual(fname);
// expect((await getNameRegistryEvent(db, fname)).type).toEqual(
// protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_RENEW
// );
// The renew event is now available
expect(await getNameRegistryEvent(db, fname)).toMatchObject({
fname,
type: protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_RENEW,
});
});
});

View File

@@ -1,7 +1,7 @@
import * as protobufs from '@farcaster/protobufs';
import { hexStringToBytes, HubAsyncResult } from '@farcaster/utils';
import { bytesToUtf8String, hexStringToBytes, HubAsyncResult, toFarcasterTime } from '@farcaster/utils';
import { BigNumber, Contract, Event, providers } from 'ethers';
import { err, ok, ResultAsync } from 'neverthrow';
import { err, ok, Result, ResultAsync } from 'neverthrow';
import { IdRegistry, NameRegistry } from '~/eth/abis';
import { bytes32ToBytes, bytesToBytes32 } from '~/eth/utils';
import { HubInterface } from '~/hubble';
@@ -18,6 +18,8 @@ export class GoerliEthConstants {
public static ChunkSize = 10000;
}
type NameRegistryRenewEvent = Omit<protobufs.NameRegistryEvent, 'to' | 'from'>;
/**
* Class that follows the Ethereum chain to handle on-chain events from the ID Registry and Name Registry contracts.
*/
@@ -32,6 +34,7 @@ export class EthEventsProvider {
private _idEventsByBlock: Map<number, Array<protobufs.IdRegistryEvent>>;
private _nameEventsByBlock: Map<number, Array<protobufs.NameRegistryEvent>>;
private _renewEventsByBlock: Map<number, Array<NameRegistryRenewEvent>>;
private _lastBlockNumber: number;
@@ -60,6 +63,7 @@ export class EthEventsProvider {
// numConfirmations blocks have been mined.
this._nameEventsByBlock = new Map();
this._idEventsByBlock = new Map();
this._renewEventsByBlock = new Map();
// Setup IdRegistry contract
this._idRegistryContract.on('Register', (to: string, id: BigNumber, _recovery, _url, event: Event) => {
@@ -71,19 +75,12 @@ export class EthEventsProvider {
// Setup NameRegistry contract
this._nameRegistryContract.on('Transfer', (from: string, to: string, tokenId: BigNumber, event: Event) => {
this.cacheNameRegistryEvent(
from,
to,
tokenId,
protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_TRANSFER,
event
);
this.cacheNameRegistryEvent(from, to, tokenId, event);
});
// TODO: handle Renew events separately from Transfer events
// this._nameRegistryContract.on('Renew', (tokenId: BigNumber, expiry: BigNumber, event: Event) => {
// this.cacheNameRegistryEvent('', '', tokenId, flatbuffers.NameRegistryEventType.NameRegistryRenew, expiry, event);
// });
this._nameRegistryContract.on('Renew', (tokenId: BigNumber, expiry: BigNumber, event: Event) => {
this.cacheRenewEvent(tokenId, expiry, event);
});
// Set up block listener to confirm blocks
this._jsonRpcProvider.on('block', (blockNumber: number) => this.handleNewBlock(blockNumber));
@@ -194,7 +191,9 @@ export class EthEventsProvider {
toBlock,
GoerliEthConstants.ChunkSize
);
// TODO: sync old Name Renew events
// We don't need to sync historical Renew events because the expiry
// is pulled when NameRegistryEvents are merged
this._isHistoricalSyncDone = true;
}
@@ -346,12 +345,10 @@ export class EthEventsProvider {
const from: string = event.args?.at(0);
const to: string = event.args?.at(1);
const tokenId: BigNumber = BigNumber.from(event.args?.at(2));
await this.cacheNameRegistryEvent(from, to, tokenId, type, event);
await this.cacheNameRegistryEvent(from, to, tokenId, event);
} catch (e) {
log.error({ event }, 'failed to parse event args');
}
} else if (type === protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_RENEW) {
// TODO: create NameRegistryEvent using attributes of Renew events and previous Transfer event
}
}
}
@@ -362,7 +359,11 @@ export class EthEventsProvider {
log.info({ blockNumber }, `new block: ${blockNumber}`);
// Get all blocks that have been confirmed into a single array and sort.
const cachedBlocksSet = new Set([...this._nameEventsByBlock.keys(), ...this._idEventsByBlock.keys()]);
const cachedBlocksSet = new Set([
...this._nameEventsByBlock.keys(),
...this._idEventsByBlock.keys(),
...this._renewEventsByBlock.keys(),
]);
const cachedBlocks = Array.from(cachedBlocksSet);
cachedBlocks.sort();
@@ -385,6 +386,31 @@ export class EthEventsProvider {
await this._hub.submitNameRegistryEvent(nameEvent, 'eth-provider');
}
}
const renewEvents = this._renewEventsByBlock.get(cachedBlock);
this._renewEventsByBlock.delete(cachedBlock);
if (renewEvents) {
for (const renewEvent of renewEvents) {
const nameRegistryEvent = await this._hub.engine.getNameRegistryEvent(renewEvent.fname);
if (nameRegistryEvent.isErr()) {
log.error(
{ blockNumber, errCode: nameRegistryEvent.error.errCode },
`failed to get event for fname ${bytesToUtf8String(
renewEvent.fname
)._unsafeUnwrap()} from renew event: ${nameRegistryEvent.error.message}`
);
continue;
}
const updatedEvent: protobufs.NameRegistryEvent = {
...nameRegistryEvent.value,
...renewEvent,
};
await this._hub.submitNameRegistryEvent(updatedEvent);
}
}
}
}
@@ -405,42 +431,33 @@ export class EthEventsProvider {
event: Event
): HubAsyncResult<void> {
const { blockNumber, blockHash, transactionHash, logIndex } = event;
log.info(
{ event: { to, id: id.toString(), blockNumber } },
`cacheIdRegistryEvent: fid ${id.toString()} assigned to ${to} in block ${blockNumber}`
);
// Convert id registry datatypes to bytes
const fromBytes = from && from.length > 0 ? hexStringToBytes(from) : ok(undefined);
if (fromBytes.isErr()) {
return err(fromBytes.error);
const logEvent = log.child({ event: { to, id: id.toString(), blockNumber } });
const serialized = Result.combine([
from && from.length > 0 ? hexStringToBytes(from) : ok(new Uint8Array()),
hexStringToBytes(blockHash),
hexStringToBytes(transactionHash),
hexStringToBytes(to),
]);
if (serialized.isErr()) {
logEvent.error({ errCode: serialized.error.errCode }, `cacheIdRegistryEvent error: ${serialized.error.message}`);
return err(serialized.error);
}
const blockHashBytes = hexStringToBytes(blockHash);
if (blockHashBytes.isErr()) {
return err(blockHashBytes.error);
}
const transactionHashBytes = hexStringToBytes(transactionHash);
if (transactionHashBytes.isErr()) {
return err(transactionHashBytes.error);
}
const toBytes = hexStringToBytes(to);
if (toBytes.isErr()) {
return err(toBytes.error);
}
const [fromBytes, blockHashBytes, transactionHashBytes, toBytes] = serialized.value;
// Construct the protobuf
const idRegistryEvent = protobufs.IdRegistryEvent.create({
blockNumber,
blockHash: blockHashBytes.value,
blockHash: blockHashBytes,
logIndex,
fid: id.toNumber(),
to: toBytes.value,
transactionHash: transactionHashBytes.value,
to: toBytes,
transactionHash: transactionHashBytes,
type,
from: fromBytes.value ?? new Uint8Array(),
from: fromBytes,
});
// Add it to the cache
@@ -451,6 +468,11 @@ export class EthEventsProvider {
}
idEvents.push(idRegistryEvent);
log.info(
{ event: { to, id: id.toString(), blockNumber } },
`cacheIdRegistryEvent: fid ${id.toString()} assigned to ${to} in block ${blockNumber}`
);
return ok(undefined);
}
@@ -458,49 +480,39 @@ export class EthEventsProvider {
from: string,
to: string,
tokenId: BigNumber,
type: protobufs.NameRegistryEventType,
event: Event
): HubAsyncResult<void> {
const { blockNumber, blockHash, transactionHash, logIndex } = event;
log.info(
{ event: { to, blockNumber } },
`cacheNameRegistryEvent: token id ${tokenId.toString()} assigned to ${to} in block ${blockNumber}`
);
const blockHashBytes = hexStringToBytes(blockHash);
if (blockHashBytes.isErr()) {
return err(blockHashBytes.error);
const logEvent = log.child({ event: { to, blockNumber } });
const serialized = Result.combine([
hexStringToBytes(blockHash),
hexStringToBytes(transactionHash),
from && from.length > 0 ? hexStringToBytes(from) : ok(new Uint8Array()),
hexStringToBytes(to),
bytes32ToBytes(tokenId),
]);
if (serialized.isErr()) {
logEvent.error(
{ errCode: serialized.error.errCode },
`cacheNameRegistryEvent error: ${serialized.error.message}`
);
return err(serialized.error);
}
const transactionHashBytes = hexStringToBytes(transactionHash);
if (transactionHashBytes.isErr()) {
return err(transactionHashBytes.error);
}
const fromBytes = from.length > 0 ? hexStringToBytes(from) : ok(undefined);
if (fromBytes.isErr()) {
return err(fromBytes.error);
}
const toBytes = hexStringToBytes(to);
if (toBytes.isErr()) {
return err(toBytes.error);
}
const fnameBytes = bytes32ToBytes(tokenId);
if (fnameBytes.isErr()) {
return err(fnameBytes.error);
}
const [blockHashBytes, transactionHashBytes, fromBytes, toBytes, fnameBytes] = serialized.value;
const nameRegistryEvent = protobufs.NameRegistryEvent.create({
blockNumber,
blockHash: blockHashBytes.value,
transactionHash: transactionHashBytes.value,
blockHash: blockHashBytes,
transactionHash: transactionHashBytes,
logIndex,
fname: fnameBytes.value,
from: fromBytes.value ?? new Uint8Array(),
to: toBytes.value,
type,
fname: fnameBytes,
from: fromBytes,
to: toBytes,
type: protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_TRANSFER,
});
// Add it to the cache
@@ -511,6 +523,50 @@ export class EthEventsProvider {
}
nameEvents.push(nameRegistryEvent);
logEvent.info(`cacheNameRegistryEvent: token id ${tokenId.toString()} assigned to ${to} in block ${blockNumber}`);
return ok(undefined);
}
private async cacheRenewEvent(tokenId: BigNumber, expiry: BigNumber, event: Event): HubAsyncResult<void> {
const { blockHash, transactionHash, blockNumber, logIndex } = event;
const logEvent = log.child({ event: { blockNumber } });
const serialized = Result.combine([
hexStringToBytes(blockHash),
hexStringToBytes(transactionHash),
bytes32ToBytes(tokenId),
toFarcasterTime(expiry.toNumber()),
]);
if (serialized.isErr()) {
logEvent.error({ errCode: serialized.error.errCode }, `cacheRenewEvent error: ${serialized.error.message}`);
return err(serialized.error);
}
const [blockHashBytes, transactionHashBytes, fnameBytes, farcasterTimeExpiry] = serialized.value;
const renewEvent: NameRegistryRenewEvent = {
blockNumber,
blockHash: blockHashBytes,
transactionHash: transactionHashBytes,
logIndex,
fname: fnameBytes,
type: protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_RENEW,
expiry: farcasterTimeExpiry,
};
// Add it to the cache
let renewEvents = this._renewEventsByBlock.get(blockNumber);
if (!renewEvents) {
renewEvents = [];
this._renewEventsByBlock.set(blockNumber, renewEvents);
}
renewEvents.push(renewEvent);
logEvent.info(`cacheRenewEvent: token id ${tokenId.toString()} renewed in block ${blockNumber}`);
return ok(undefined);
}
}

View File

@@ -53,6 +53,7 @@ export const APP_VERSION = process.env['npm_package_version'] ?? '1.0.0';
export const APP_NICKNAME = 'Farcaster Hub';
export interface HubInterface {
engine: Engine;
submitMessage(message: protobufs.Message, source?: HubSubmitSource): HubAsyncResult<void>;
submitIdRegistryEvent(event: protobufs.IdRegistryEvent, source?: HubSubmitSource): HubAsyncResult<void>;
submitNameRegistryEvent(event: protobufs.NameRegistryEvent, source?: HubSubmitSource): HubAsyncResult<void>;

View File

@@ -117,7 +117,7 @@ class UserDataStore {
// When there is a NameRegistryEvent, we need to check if we need to revoke UserDataAdd messages from the
// previous owner of the name.
if (event.from) {
if (event.type === protobufs.NameRegistryEventType.NAME_REGISTRY_EVENT_TYPE_TRANSFER && event.from) {
// Check to see if the from address has an fid
const idRegistryEvent = await ResultAsync.fromPromise(
getIdRegistryEventByCustodyAddress(this._db, event.from),