/ src / eventHandlers / TransferEventHandler.ts
TransferEventHandler.ts
  1  import { ZeroAddress } from 'ethers';
  2  import type { TransferEvent } from '../../contracts/CURRENT_NETWORK/NftDriver';
  3  import EventHandlerBase from '../events/EventHandlerBase';
  4  import ScopedLogger from '../core/ScopedLogger';
  5  import { convertToNftDriverId } from '../utils/accountIdUtils';
  6  import type EventHandlerRequest from '../events/EventHandlerRequest';
  7  import {
  8    DripListModel,
  9    EcosystemMainAccountModel,
 10    TransferEventModel,
 11  } from '../models';
 12  import { dbConnection } from '../db/database';
 13  import RecoverableError from '../utils/recoverableError';
 14  import type { Address, AddressDriverId } from '../core/types';
 15  import {
 16    addressDriverContract,
 17    nftDriverContract,
 18  } from '../core/contractClients';
 19  import unreachableError from '../utils/unreachableError';
 20  import appSettings from '../config/appSettings';
 21  import { makeVersion } from '../utils/lastProcessedVersion';
 22  
 23  export default class TransferEventHandler extends EventHandlerBase<'Transfer(address,address,uint256)'> {
 24    public eventSignatures = ['Transfer(address,address,uint256)' as const];
 25  
 26    protected async _handle({
 27      id: requestId,
 28      event: {
 29        args,
 30        logIndex,
 31        blockNumber,
 32        blockTimestamp,
 33        transactionHash,
 34        eventSignature,
 35      },
 36    }: EventHandlerRequest<'Transfer(address,address,uint256)'>): Promise<void> {
 37      const [from, to, rawTokenId] = args as TransferEvent.OutputTuple;
 38      const tokenId = convertToNftDriverId(rawTokenId);
 39  
 40      const isMint = from === ZeroAddress;
 41  
 42      const scopedLogger = new ScopedLogger(this.name, requestId);
 43  
 44      scopedLogger.log(
 45        [
 46          `📥 ${this.name} is processing ${eventSignature}:`,
 47          `  - from:       ${from}`,
 48          `  - to:         ${to}`,
 49          `  - tokenId:    ${rawTokenId}`,
 50          `  - logIndex:   ${logIndex}`,
 51          `  - txHash:     ${transactionHash}`,
 52        ].join('\n'),
 53      );
 54  
 55      await dbConnection.transaction(async (transaction) => {
 56        const transferEvent = await TransferEventModel.create(
 57          {
 58            tokenId,
 59            to: to as Address,
 60            from: from as Address,
 61            logIndex,
 62            blockNumber,
 63            blockTimestamp,
 64            transactionHash,
 65          },
 66          { transaction },
 67        );
 68  
 69        scopedLogger.bufferCreation({
 70          type: TransferEventModel,
 71          id: `${transactionHash}-${logIndex}`,
 72          input: transferEvent,
 73        });
 74  
 75        const dripList = await DripListModel.findByPk(tokenId, {
 76          transaction,
 77          lock: transaction.LOCK.UPDATE,
 78        });
 79  
 80        const ecosystemMainAccount = await EcosystemMainAccountModel.findByPk(
 81          tokenId,
 82          {
 83            transaction,
 84            lock: transaction.LOCK.UPDATE,
 85          },
 86        );
 87  
 88        const entity = dripList ?? ecosystemMainAccount;
 89        const Model = dripList ? DripListModel : EcosystemMainAccountModel;
 90  
 91        if (!entity) {
 92          scopedLogger.flush();
 93  
 94          throw new RecoverableError(
 95            `Cannot process '${eventSignature}' event for Drip List or Ecosystem Main Account ${tokenId}: entity not found. Likely waiting on 'AccountMetadata' event to be processed. Retrying, but if this persists, it is a real error.`,
 96          );
 97        }
 98  
 99        if (dripList && ecosystemMainAccount) {
100          unreachableError(
101            `Invariant violation: both Drip List and Ecosystem Main Account found for token '${tokenId}'.`,
102          );
103        }
104  
105        const newVersion = makeVersion(blockNumber, logIndex);
106        const storedVersion = BigInt(entity.lastProcessedVersion);
107  
108        if (isMint) {
109          entity.creator = to as Address;
110  
111          scopedLogger.bufferUpdate({
112            type: Model,
113            id: entity.accountId,
114            input: entity,
115          });
116  
117          await entity.save({ transaction });
118        }
119  
120        const onChainOwner = (await nftDriverContract.ownerOf(
121          tokenId,
122        )) as Address;
123  
124        if (to !== onChainOwner) {
125          scopedLogger.bufferMessage(
126            `Skipped Drip List or Ecosystem Main Account ${tokenId} '${eventSignature}' event processing: event is not the latest (on-chain owner '${onChainOwner}' does not match 'to' '${to}').`,
127          );
128  
129          scopedLogger.flush();
130  
131          return;
132        }
133  
134        // Update to the latest on-chain state.
135        entity.ownerAddress = onChainOwner; // Equal to `to`.
136        entity.ownerAccountId = (
137          await addressDriverContract.calcAccountId(onChainOwner)
138        ).toString() as AddressDriverId; // Equal to `to`.
139        entity.previousOwnerAddress = from as Address;
140  
141        // Safely update fields that another event handler could also modify.
142        if (newVersion > storedVersion) {
143          entity.isVisible =
144            blockNumber > appSettings.visibilityThresholdBlockNumber
145              ? from === ZeroAddress // If it's a mint, then the Drip List will be visible. If it's a real transfer, then it's not.
146              : true; // If the block number is less than the visibility threshold, then the Drip List is visible by default.
147        }
148  
149        entity.lastProcessedVersion = newVersion.toString();
150  
151        scopedLogger.bufferUpdate({
152          type: Model,
153          id: entity.accountId,
154          input: entity,
155        });
156  
157        await entity.save({ transaction });
158  
159        scopedLogger.flush();
160      });
161    }
162  
163    override async afterHandle(context: {
164      args: [from: string, to: string, tokenId: bigint];
165      blockTimestamp: Date;
166      requestId: string;
167    }): Promise<void> {
168      const [from, to, tokenId] = context.args;
169      await super.afterHandle({
170        args: [
171          tokenId,
172          (
173            await addressDriverContract.calcAccountId(from)
174          ).toString() as AddressDriverId,
175          (
176            await addressDriverContract.calcAccountId(to)
177          ).toString() as AddressDriverId,
178        ],
179        blockTimestamp: context.blockTimestamp,
180        requestId: context.requestId,
181      });
182    }
183  }