From 3d9853e49c23df5fe0ec0f5ee9e116334f5d7523 Mon Sep 17 00:00:00 2001 From: ponyjackal Date: Thu, 20 Jun 2024 03:53:21 -0700 Subject: [PATCH] feat: add rodeo contextualizer --- src/contextualizers/protocol/rodeo/README.md | 5 + .../rodeo/abis/MultiTokenDropMarket.ts | 452 ++++++++++++++++++ .../protocol/rodeo/constants.ts | 6 + src/contextualizers/protocol/rodeo/index.ts | 11 + .../protocol/rodeo/rodeo.spec.ts | 55 +++ src/contextualizers/protocol/rodeo/rodeo.ts | 103 ++++ 6 files changed, 632 insertions(+) create mode 100644 src/contextualizers/protocol/rodeo/README.md create mode 100644 src/contextualizers/protocol/rodeo/abis/MultiTokenDropMarket.ts create mode 100644 src/contextualizers/protocol/rodeo/constants.ts create mode 100644 src/contextualizers/protocol/rodeo/index.ts create mode 100644 src/contextualizers/protocol/rodeo/rodeo.spec.ts create mode 100644 src/contextualizers/protocol/rodeo/rodeo.ts diff --git a/src/contextualizers/protocol/rodeo/README.md b/src/contextualizers/protocol/rodeo/README.md new file mode 100644 index 00000000..bbe20922 --- /dev/null +++ b/src/contextualizers/protocol/rodeo/README.md @@ -0,0 +1,5 @@ +### MultiTokenDropMarket contract + +| Contract | Address | Chain ID | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------- | +| MultiTokenDropMarket | [0x132363a3bbf47e06cf642dd18e9173e364546c99](https://www.onceupon.xyz/0x132363a3bbf47e06cf642dd18e9173e364546c99:8453) | Base | diff --git a/src/contextualizers/protocol/rodeo/abis/MultiTokenDropMarket.ts b/src/contextualizers/protocol/rodeo/abis/MultiTokenDropMarket.ts new file mode 100644 index 00000000..652c7e01 --- /dev/null +++ b/src/contextualizers/protocol/rodeo/abis/MultiTokenDropMarket.ts @@ -0,0 +1,452 @@ +const abi = [ + { + inputs: [ + { internalType: 'address payable', name: '_treasury', type: 'address' }, + { internalType: 'address', name: '_feth', type: 'address' }, + { internalType: 'address', name: '_worldsNft', type: 'address' }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [], + name: 'DropMarketLibrary_General_Availability_Start_Time_Has_Expired', + type: 'error', + }, + { + inputs: [], + name: 'DropMarketLibrary_Mint_Permission_Required', + type: 'error', + }, + { + inputs: [], + name: 'DropMarketLibrary_Only_Callable_By_Collection_Admin', + type: 'error', + }, + { + inputs: [{ internalType: 'uint256', name: 'maxTime', type: 'uint256' }], + name: 'DropMarketLibrary_Time_Too_Far_In_The_Future', + type: 'error', + }, + { + inputs: [], + name: 'FETHNode_FETH_Address_Is_Not_A_Contract', + type: 'error', + }, + { inputs: [], name: 'FETHNode_Only_FETH_Can_Transfer_ETH', type: 'error' }, + { + inputs: [ + { internalType: 'uint256', name: 'expectedValueAmount', type: 'uint256' }, + ], + name: 'FethNode_Too_Much_Value_Provided', + type: 'error', + }, + { + inputs: [], + name: 'FoundationTreasuryNode_Address_Is_Not_A_Contract', + type: 'error', + }, + { + inputs: [], + name: 'MultiTokenDropMarketFixedPriceSale_Collection_Must_Be_ERC_1155', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint256', + name: 'generalAvailabilityStartTime', + type: 'uint256', + }, + ], + name: 'MultiTokenDropMarketFixedPriceSale_General_Access_Not_Open', + type: 'error', + }, + { + inputs: [], + name: 'MultiTokenDropMarketFixedPriceSale_Must_Buy_At_Least_One_Token', + type: 'error', + }, + { + inputs: [], + name: 'MultiTokenDropMarketFixedPriceSale_Must_Have_Available_Supply', + type: 'error', + }, + { + inputs: [], + name: 'MultiTokenDropMarketFixedPriceSale_Payment_Address_Required', + type: 'error', + }, + { + inputs: [], + name: 'MultiTokenDropMarketFixedPriceSale_Recipient_Cannot_Be_Zero_Address', + type: 'error', + }, + { + inputs: [], + name: 'MultiTokenDropMarketFixedPriceSale_Sale_Terms_Not_Found', + type: 'error', + }, + { + inputs: [{ internalType: 'uint256', name: 'mintEndTime', type: 'uint256' }], + name: 'MultiTokenDropMarketFixedPriceSale_Start_Time_Is_After_Mint_End_Time', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint256', name: 'existingSaleTermsId', type: 'uint256' }, + ], + name: 'MultiTokenDropMarketFixedPriceSale_Token_Already_Listed_For_Sale', + type: 'error', + }, + { + inputs: [ + { internalType: 'uint8', name: 'bits', type: 'uint8' }, + { internalType: 'uint256', name: 'value', type: 'uint256' }, + ], + name: 'SafeCastOverflowedUintDowncast', + type: 'error', + }, + { + inputs: [], + name: 'WorldsNftNode_Worlds_NFT_Is_Not_A_Contract', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'saleTermsId', + type: 'uint256', + }, + ], + name: 'CancelSaleTerms', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'multiTokenContract', + type: 'address', + }, + { + indexed: true, + internalType: 'uint256', + name: 'tokenId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'saleTermsId', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'pricePerQuantity', + type: 'uint256', + }, + { + indexed: false, + internalType: 'address payable', + name: 'creatorPaymentAddress', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'generalAvailabilityStartTime', + type: 'uint256', + }, + ], + name: 'ConfigureFixedPriceSale', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'invalidatedSaleTermsId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'uint256', + name: 'newSaleTermsId', + type: 'uint256', + }, + ], + name: 'InvalidateSaleTerms', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'uint256', + name: 'saleTermsId', + type: 'uint256', + }, + { + indexed: true, + internalType: 'address', + name: 'buyer', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'referrer', + type: 'address', + }, + { + indexed: false, + internalType: 'uint256', + name: 'tokenQuantity', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'pricePerQuantity', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'creatorRevenue', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'worldCuratorRevenue', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'protocolFee', + type: 'uint256', + }, + { + indexed: false, + internalType: 'uint256', + name: 'referrerReward', + type: 'uint256', + }, + ], + name: 'MintFromFixedPriceSale', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: 'address', name: 'user', type: 'address' }, + { + indexed: false, + internalType: 'uint256', + name: 'amount', + type: 'uint256', + }, + ], + name: 'WithdrawalToFETH', + type: 'event', + }, + { + inputs: [{ internalType: 'uint256', name: 'saleTermsId', type: 'uint256' }], + name: 'cancelFixedPriceSale', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'multiTokenContract', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'uint256', name: 'pricePerQuantity', type: 'uint256' }, + { + internalType: 'address payable', + name: 'creatorPaymentAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'generalAvailabilityStartTime', + type: 'uint256', + }, + ], + name: 'createFixedPriceSale', + outputs: [ + { internalType: 'uint256', name: 'saleTermsId', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'getFethAddress', + outputs: [ + { internalType: 'address', name: 'fethAddress', type: 'address' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'saleTermsId', type: 'uint256' }, + { internalType: 'address payable', name: 'referrer', type: 'address' }, + ], + name: 'getFixedPriceSale', + outputs: [ + { + components: [ + { + internalType: 'address', + name: 'multiTokenContract', + type: 'address', + }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { + internalType: 'uint256', + name: 'pricePerQuantity', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'quantityAvailableToMint', + type: 'uint256', + }, + { + internalType: 'address payable', + name: 'creatorPaymentAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'generalAvailabilityStartTime', + type: 'uint256', + }, + { internalType: 'uint256', name: 'mintEndTime', type: 'uint256' }, + { + internalType: 'uint256', + name: 'creatorRevenuePerQuantity', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'referrerRewardPerQuantity', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'worldCuratorRevenuePerQuantity', + type: 'uint256', + }, + { + internalType: 'uint256', + name: 'protocolFeePerQuantity', + type: 'uint256', + }, + ], + internalType: + 'struct MultiTokenDropMarketFixedPriceSale.GetFixedPriceSaleResults', + name: 'results', + type: 'tuple', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getFoundationTreasury', + outputs: [ + { + internalType: 'address payable', + name: 'treasuryAddress', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'pricePerQuantity', type: 'uint256' }, + { internalType: 'uint256', name: 'tokenQuantity', type: 'uint256' }, + ], + name: 'getRevenueDistributionForMint', + outputs: [ + { internalType: 'uint256', name: 'creatorRevenue', type: 'uint256' }, + { internalType: 'uint256', name: 'protocolFee', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'address', name: 'nftContract', type: 'address' }, + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + ], + name: 'getSaleTermsForToken', + outputs: [ + { internalType: 'uint256', name: 'saleTermsId', type: 'uint256' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'getWorldsNftAddress', + outputs: [{ internalType: 'address', name: 'worldsNft', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'saleTermsId', type: 'uint256' }, + { internalType: 'uint256', name: 'tokenQuantity', type: 'uint256' }, + { internalType: 'address', name: 'tokenRecipient', type: 'address' }, + { internalType: 'address payable', name: 'referrer', type: 'address' }, + ], + name: 'mintFromFixedPriceSale', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [ + { internalType: 'uint256', name: 'saleTermsId', type: 'uint256' }, + { internalType: 'uint256', name: 'pricePerQuantity', type: 'uint256' }, + { + internalType: 'address payable', + name: 'creatorPaymentAddress', + type: 'address', + }, + { + internalType: 'uint256', + name: 'generalAvailabilityStartTime', + type: 'uint256', + }, + ], + name: 'updateFixedPriceSale', + outputs: [ + { internalType: 'uint256', name: 'newSaleTermsId', type: 'uint256' }, + ], + stateMutability: 'nonpayable', + type: 'function', + }, + { stateMutability: 'payable', type: 'receive' }, +] as const; + +export default abi; diff --git a/src/contextualizers/protocol/rodeo/constants.ts b/src/contextualizers/protocol/rodeo/constants.ts new file mode 100644 index 00000000..8177e1b6 --- /dev/null +++ b/src/contextualizers/protocol/rodeo/constants.ts @@ -0,0 +1,6 @@ +import multiTokenDropMarketAbi from './abis/MultiTokenDropMarket'; + +export const MULTI_TOKEN_DROP_MARKET_CONTRACT = + '0x132363a3bbf47e06cf642dd18e9173e364546c99'; + +export const MULTI_TOKEN_DROP_MARKET_ABI = multiTokenDropMarketAbi; diff --git a/src/contextualizers/protocol/rodeo/index.ts b/src/contextualizers/protocol/rodeo/index.ts new file mode 100644 index 00000000..d0d1d5b6 --- /dev/null +++ b/src/contextualizers/protocol/rodeo/index.ts @@ -0,0 +1,11 @@ +import { contextualize as rodeo } from './rodeo'; +import { makeContextualize } from '../../../helpers/utils'; + +const children = { rodeo }; + +const contextualize = makeContextualize(children); + +export const rodeoContextualizer = { + contextualize, + children, +}; diff --git a/src/contextualizers/protocol/rodeo/rodeo.spec.ts b/src/contextualizers/protocol/rodeo/rodeo.spec.ts new file mode 100644 index 00000000..9cebf8d1 --- /dev/null +++ b/src/contextualizers/protocol/rodeo/rodeo.spec.ts @@ -0,0 +1,55 @@ +import { Transaction } from '../../../types'; +import { detect, generate } from './rodeo'; +import { containsBigInt, contextSummary } from '../../../helpers/utils'; +import mintWithRewards0x6ccb3140 from '../../test/transactions/mintWithRewards-0x6ccb3140.json'; +import zoraMintWithRewards0x837a9a69 from '../../test/transactions/zoraMintWithRewards-0x837a9a69.json'; +import catchall0xc35c01ac from '../../test/transactions/catchall-0xc35c01ac.json'; + +describe('Zora Mint', () => { + it('Should detect zora mint with rewards transaction', () => { + const zoraMintWithRewards1 = detect( + mintWithRewards0x6ccb3140 as unknown as Transaction, + ); + expect(zoraMintWithRewards1).toBe(true); + + const zoraMintWithRewards2 = detect( + zoraMintWithRewards0x837a9a69 as unknown as Transaction, + ); + expect(zoraMintWithRewards2).toBe(true); + }); + + it('Should generate context for mintWithRewards transaction', () => { + const zoraMintWithRewards1 = generate( + mintWithRewards0x6ccb3140 as unknown as Transaction, + ); + expect(zoraMintWithRewards1.context?.summaries?.category).toBe( + 'PROTOCOL_1', + ); + expect(zoraMintWithRewards1.context?.summaries?.en.title).toBe('Zora'); + const desc1 = contextSummary(zoraMintWithRewards1.context); + expect(desc1).toBe( + '0x6bd6d5f98c5ad557eed0df2fd73b9666188cac96 MINTED 1 0xf41a3e3033d4e878943194b729aec993a4ea2045 #26 for 0.000777 ETH with 0.000111 ETH in rewards for 0xecfc2ee50409e459c554a2b0376f882ce916d853', + ); + expect(containsBigInt(zoraMintWithRewards1.context)).toBe(false); + + const zoraMintWithRewards2 = generate( + zoraMintWithRewards0x837a9a69 as unknown as Transaction, + ); + expect(zoraMintWithRewards2.context?.summaries?.category).toBe( + 'PROTOCOL_1', + ); + expect(zoraMintWithRewards2.context?.summaries?.en.title).toBe('Zora'); + const desc2 = contextSummary(zoraMintWithRewards2.context); + expect(desc2).toBe( + '0xf70da97812cb96acdf810712aa562db8dfa3dbef MINTED 1 0xf41a3e3033d4e878943194b729aec993a4ea2045 #29 to 0xd97622b57112f82a0db8b1aee08e37aa6b4b2a03 for 0.000777 ETH with 0.000111 ETH in rewards for 0xecfc2ee50409e459c554a2b0376f882ce916d853', + ); + expect(containsBigInt(zoraMintWithRewards2.context)).toBe(false); + }); + + it('Should not detect as zora creator', () => { + const zoraMintWithRewards1 = detect( + catchall0xc35c01ac as unknown as Transaction, + ); + expect(zoraMintWithRewards1).toBe(false); + }); +}); diff --git a/src/contextualizers/protocol/rodeo/rodeo.ts b/src/contextualizers/protocol/rodeo/rodeo.ts new file mode 100644 index 00000000..4b5081da --- /dev/null +++ b/src/contextualizers/protocol/rodeo/rodeo.ts @@ -0,0 +1,103 @@ +import { Hex } from 'viem'; +import { AssetType, EventLogTopics, Transaction, Log } from '../../../types'; +import { + MULTI_TOKEN_DROP_MARKET_ABI, + MULTI_TOKEN_DROP_MARKET_CONTRACT, +} from './constants'; +import { decodeLog } from '../../../helpers/utils'; +import { generate as erc721Generate } from '../../heuristics/erc721Mint/erc721Mint'; +import { generate as erc1155Generate } from '../../heuristics/erc1155Mint/erc1155Mint'; + +export const contextualize = (transaction: Transaction): Transaction => { + const isEnjoy = detect(transaction); + if (!isEnjoy) return transaction; + + return generate(transaction); +}; + +export const detect = (transaction: Transaction): boolean => { + // check if there is 'MintFromFixedPriceSale' log emitted + const logs = + transaction.logs && transaction.logs.length > 0 ? transaction.logs : []; + const mintFromFixedPriceSaleLog = findMintFromFixedPriceSaleLog(logs); + + if (!mintFromFixedPriceSaleLog) return false; + + return true; +}; + +// Contextualize for mined txs +export const generate = (transaction: Transaction): Transaction => { + // detect as heuristic erc721 or erc1155 mint + transaction = erc721Generate(transaction); + if (transaction.context?.summaries?.en.title !== 'NFT Mint') { + transaction = erc1155Generate(transaction); + if (transaction.context?.summaries?.en.title !== 'NFT Mint') { + return transaction; + } + } + // update category and title + transaction.context.summaries.category = 'PROTOCOL_1'; + transaction.context.summaries.en.title = 'Rodeo'; + + const logs = + transaction.logs && transaction.logs.length > 0 ? transaction.logs : []; + const mintFromFixedPriceSaleLog = findMintFromFixedPriceSaleLog(logs); + if (!mintFromFixedPriceSaleLog) return transaction; + const decodedLog = decodeLog( + MULTI_TOKEN_DROP_MARKET_ABI, + mintFromFixedPriceSaleLog.data as Hex, + [ + mintFromFixedPriceSaleLog.topic0, + mintFromFixedPriceSaleLog.topic1, + mintFromFixedPriceSaleLog.topic2, + mintFromFixedPriceSaleLog.topic3, + ] as EventLogTopics, + ); + if ( + !decodedLog || + !decodedLog.args['referrer'] || + !decodedLog.args['referrerReward'] + ) + return transaction; + + transaction.context.variables = { + ...transaction.context.variables, + numOfEth: { + type: AssetType.ETH, + value: decodedLog.args['referrerReward'].toString(), + unit: 'wei', + }, + mintReferral: { + type: 'address', + value: decodedLog.args['referrer'].toLowerCase(), + }, + }; + + transaction.context.summaries['en'].default += + 'with[[numOfEth]]in rewards for[[mintReferral]]'; + + return transaction; +}; + +const findMintFromFixedPriceSaleLog = (logs: Log[]): Log | undefined => { + const mintFromFixedPriceSaleLog = logs.find((log) => { + if (log.address !== MULTI_TOKEN_DROP_MARKET_CONTRACT) { + return false; + } + const decodedLog = decodeLog( + MULTI_TOKEN_DROP_MARKET_ABI, + log.data as Hex, + [log.topic0, log.topic1, log.topic2, log.topic3] as EventLogTopics, + ); + if (!decodedLog) return false; + + if (decodedLog.eventName !== 'MintFromFixedPriceSale') { + return false; + } + + return true; + }); + + return mintFromFixedPriceSaleLog; +};