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 }