- 
                Notifications
    You must be signed in to change notification settings 
- Fork 28
Make an option to make addresses lower case #736
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
6ec463b
              7754b27
              cd975df
              811c533
              16c6c5d
              5c9c3ad
              23ab76b
              64d5725
              5b69ca6
              60437cc
              1bb9ce5
              45a3c10
              cc84556
              017e612
              f31f3a5
              a001786
              b594d9b
              6f7033c
              c1dc3d1
              7803752
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -10,6 +10,29 @@ external unsafeFromString: string => t = "%identity" | |
| module Evm = { | ||
| @module("viem") | ||
| external fromStringOrThrow: string => t = "getAddress" | ||
|  | ||
| // NOTE: the function is named to be overshadowed by the one below, so that we don't have to import viem in the handler code | ||
| @module("viem") | ||
| external fromStringLowercaseOrThrow: string => bool = "isAddress" | ||
|  | ||
| // Reassign since the function might be used in the handler code | ||
| // and we don't want to have a "viem" import there. It's needed to keep "viem" a dependency | ||
| // of generated code instead of adding it to the indexer project dependencies. | ||
| // Also, we want a custom error message, which is searchable in our codebase. | ||
| // Validate that the string is a proper address but return a lowercased value | ||
| let fromStringLowercaseOrThrow = string => { | ||
| if fromStringLowercaseOrThrow(string) { | ||
| unsafeFromString(string->Js.String2.toLowerCase) | ||
| } else { | ||
| Js.Exn.raiseError( | ||
| `Address "${string}" is invalid. Expected a 20-byte hex string starting with 0x.`, | ||
| ) | ||
| } | ||
| } | ||
| 
      Comment on lines
    
      +23
     to 
      +31
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainVerify viem's isAddress accepts both checksummed and lowercase inputs. The wrapper validates using  Based on the PR discussion, this validation path may be more expensive than a simple regex but provides better UX by accepting mixed-case inputs. 🌐 Web query: 💡 Result: Short answer: Yes — but with options. Details: isAddress(address, { strict?: boolean }) defaults to strict: true, which enforces EIP‑55 checksums. An all-lowercase address will be considered valid only if you call isAddress(..., { strict: false }). [1] Sources: 
 Use non-strict validation to accept lowercase addresses let fromStringLowercaseOrThrow = string => {
  if isAddress(string, { strict: false }) {
    unsafeFromString(string->Js.String2.toLowerCase)
  } else {
    Js.Exn.raiseError(
      `Address "${string}" is invalid. Expected a 20-byte hex string starting with 0x.`,
    )
  }
}This ensures both EIP-55 checksummed and all-lowercase addresses are accepted. 🤖 Prompt for AI Agents | ||
|  | ||
| let fromAddressLowercaseOrThrow = address => | ||
| address->toString->Js.String2.toLowerCase->(Utils.magic: string => t) | ||
|  | ||
| // Reassign since the function might be used in the handler code | ||
| // and we don't want to have a "viem" import there. It's needed to keep "viem" a dependency | ||
| // of generated code instead of adding it to the indexer project dependencies. | ||
|         
                  DZakh marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -147,19 +147,43 @@ module JsonRpcProvider = { | |
| @send | ||
| external getTransaction: (t, ~transactionHash: string) => promise<transaction> = "getTransaction" | ||
|  | ||
| let makeGetTransactionFields = (~getTransactionByHash) => | ||
| async (log: log): promise<unknown> => { | ||
| let transaction = await getTransactionByHash(log.transactionHash) | ||
| // Mutating should be fine, since the transaction isn't used anywhere else outside the function | ||
| let fields: {..} = transaction->Obj.magic | ||
|  | ||
| // Make it compatible with HyperSync transaction fields | ||
| fields["transactionIndex"] = log.transactionIndex | ||
| fields["input"] = fields["data"] | ||
|  | ||
| fields->Obj.magic | ||
| let makeGetTransactionFields = (~getTransactionByHash, ~lowercaseAddresses: bool) => async ( | ||
| log: log, | ||
| ): promise<unknown> => { | ||
| let transaction = await getTransactionByHash(log.transactionHash) | ||
| // Mutating should be fine, since the transaction isn't used anywhere else outside the function | ||
| 
      Comment on lines
    
      147
     to 
      +154
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle nullable getTransaction results (ethers may return null). Provider.getTransaction can return null; current type/signature assumes non-null and may crash. Apply: -  @send
-  external getTransaction: (t, ~transactionHash: string) => promise<transaction> = "getTransaction"
+  @send
+  external getTransaction: (t, ~transactionHash: string) => promise<Js.nullable<transaction>> = "getTransaction"
@@
-  let makeGetTransactionFields = (~getTransactionByHash, ~lowercaseAddresses: bool) => async (
+  let makeGetTransactionFields = (~getTransactionByHash, ~lowercaseAddresses: bool) => async (
     log: log,
   ): promise<unknown> => {
-    let transaction = await getTransactionByHash(log.transactionHash)
+    let transaction = await getTransactionByHash(log.transactionHash)
+    let transaction =
+      switch transaction {
+      | Js.Nullable.Value(tx) => tx
+      | Js.Nullable.Null
+      | Js.Nullable.Undefined =>
+        Js.Exn.raiseError("getTransaction returned null for " ++ log.transactionHash)
+      }Also applies to: 150-156 | ||
| let fields: {..} = transaction->Obj.magic | ||
|  | ||
| // Make it compatible with HyperSync transaction fields | ||
| fields["transactionIndex"] = log.transactionIndex | ||
| fields["input"] = fields["data"] | ||
|  | ||
| // NOTE: this is wasteful if these fields are not selected in the users config. | ||
| // There might be a better way to do this in the `makeThrowingGetEventTransaction` function rather based on the schema. | ||
| // However this is not extremely expensive and good enough for now (only on rpc sync also). | ||
| if lowercaseAddresses { | ||
| open Js.Nullable | ||
| switch fields["from"] { | ||
| | Value(from) => fields["from"] = from->Js.String2.toLowerCase | ||
| | Undefined => () | ||
| | Null => () | ||
| } | ||
| switch fields["to"] { | ||
| | Value(to) => fields["to"] = to->Js.String2.toLowerCase | ||
| | Undefined => () | ||
| | Null => () | ||
| } | ||
| switch fields["contractAddress"] { | ||
| | Value(contractAddress) => | ||
| fields["contractAddress"] = contractAddress->Js.String2.toLowerCase | ||
| | Undefined => () | ||
| | Null => () | ||
| } | ||
| } | ||
|  | ||
| fields->Obj.magic | ||
| } | ||
|  | ||
| type block = { | ||
| _difficulty: bigint, | ||
| difficulty: int, | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -22,6 +22,7 @@ let rec getKnownBlockWithBackoff = async ( | |
| ~chain, | ||
| ~blockNumber, | ||
| ~backoffMsOnFailure, | ||
| ~lowercaseAddresses: bool, | ||
| ) => | ||
| switch await getKnownBlock(provider, blockNumber) { | ||
| | exception err => | ||
|  | @@ -39,8 +40,17 @@ let rec getKnownBlockWithBackoff = async ( | |
| ~chain, | ||
| ~blockNumber, | ||
| ~backoffMsOnFailure=backoffMsOnFailure * 2, | ||
| ~lowercaseAddresses, | ||
| ) | ||
| | result => result | ||
| | result => | ||
| if lowercaseAddresses { | ||
| // NOTE: this is wasteful if these fields are not selected in the users config. | ||
| // There might be a better way to do this based on the block schema. | ||
| // However this is not extremely expensive and good enough for now (only on rpc sync also). | ||
| // The in place mutation is cheapest. | ||
| (result->Obj.magic)["miner"] = result.miner->Address.Evm.fromAddressLowercaseOrThrow | ||
| } | ||
| result | ||
| 
      Comment on lines
    
      +45
     to 
      +53
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chainVerify: in-place mutation may affect cached blocks and happens on every fetch. The miner address is mutated in-place on every successful block fetch (not just after backoff). Since  Consider returning a transformed copy instead of mutating in-place, or ensure the loader never caches when  Run this script to check if the blockLoader cache could be accessed from multiple locations: 🏁 Script executed: #!/bin/bash
# Check for other usages of blockLoader that might expect unmutated blocks
rg -nC3 'blockLoader->LazyLoader\.get' --type=resLength of output: 27 Let's rerun the search without the  🏁 Script executed: #!/bin/bash
rg -nC3 'blockLoader' -g '**/*.res'
rg -nC3 'LazyLoader' -g '**/*.res'
rg -nC3 'getKnownBlockWithBackoff' -g '**/*.res'Length of output: 9687 🏁 Script executed: #!/bin/bash
sed -n '1,100p' codegenerator/cli/npm/envio/src/sources/RpcSource.resLength of output: 3708 🏁 Script executed: #!/bin/bash
# Find LazyLoader implementation
fd LazyLoader.resLength of output: 46 🏁 Script executed: #!/bin/bash
sed -n '1,200p' codegenerator/cli/npm/envio/src/LazyLoader.resLength of output: 4422 🏁 Script executed: #!/bin/bash
sed -n '400,650p' codegenerator/cli/npm/envio/src/sources/RpcSource.resLength of output: 8842 Return a new block object instead of mutating the cached record let block =
  if lowercaseAddresses {
    {...result, miner: Address.Evm.fromAddressLowercaseOrThrow(result.miner)}
  } else {
    result
  }
block—or disable caching when  🤖 Prompt for AI Agents | ||
| } | ||
| let getSuggestedBlockIntervalFromExn = { | ||
| // Unknown provider: "retry with the range 123-456" | ||
|  | @@ -453,9 +463,24 @@ type options = { | |
| chain: ChainMap.Chain.t, | ||
| contracts: array<Internal.evmContractConfig>, | ||
| eventRouter: EventRouter.t<Internal.evmEventConfig>, | ||
| allEventSignatures: array<string>, | ||
| shouldUseHypersyncClientDecoder: bool, | ||
| lowercaseAddresses: bool, | ||
| } | ||
|  | ||
| let make = ({sourceFor, syncConfig, url, chain, contracts, eventRouter}: options): t => { | ||
| let make = ( | ||
| { | ||
| sourceFor, | ||
| syncConfig, | ||
| url, | ||
| chain, | ||
| contracts, | ||
| eventRouter, | ||
| allEventSignatures, | ||
| shouldUseHypersyncClientDecoder, | ||
| lowercaseAddresses, | ||
| }: options, | ||
| ): t => { | ||
| let urlHost = switch sanitizeUrl(url) { | ||
| | None => | ||
| Js.Exn.raiseError( | ||
|  | @@ -498,6 +523,7 @@ let make = ({sourceFor, syncConfig, url, chain, contracts, eventRouter}: options | |
| ~chain, | ||
| ~backoffMsOnFailure=1000, | ||
| ~blockNumber, | ||
| ~lowercaseAddresses, | ||
| ), | ||
| ~onError=(am, ~exn) => { | ||
| Logging.error({ | ||
|  | @@ -522,6 +548,7 @@ let make = ({sourceFor, syncConfig, url, chain, contracts, eventRouter}: options | |
| let getEventTransactionOrThrow = makeThrowingGetEventTransaction( | ||
| ~getTransactionFields=Ethers.JsonRpcProvider.makeGetTransactionFields( | ||
| ~getTransactionByHash=LazyLoader.get(transactionLoader, _), | ||
| ~lowercaseAddresses, | ||
| ), | ||
| ) | ||
|  | ||
|  | @@ -530,6 +557,32 @@ let make = ({sourceFor, syncConfig, url, chain, contracts, eventRouter}: options | |
| contractNameAbiMapping->Js.Dict.set(contract.name, contract.abi) | ||
| }) | ||
|  | ||
| let convertEthersLogToHyperSyncEvent = (log: Ethers.log): HyperSyncClient.ResponseTypes.event => { | ||
| let hyperSyncLog: HyperSyncClient.ResponseTypes.log = { | ||
| removed: log.removed->Option.getWithDefault(false), | ||
| index: log.logIndex, | ||
| transactionIndex: log.transactionIndex, | ||
| transactionHash: log.transactionHash, | ||
| blockHash: log.blockHash, | ||
| blockNumber: log.blockNumber, | ||
| address: log.address, | ||
| data: log.data, | ||
| topics: log.topics->Array.map(topic => Js.Nullable.return(topic)), | ||
| } | ||
| {log: hyperSyncLog} | ||
| } | ||
|  | ||
| let hscDecoder: ref<option<HyperSyncClient.Decoder.t>> = ref(None) | ||
| let getHscDecoder = () => { | ||
| switch hscDecoder.contents { | ||
| | Some(decoder) => decoder | ||
| | None => { | ||
| let decoder = HyperSyncClient.Decoder.fromSignatures(allEventSignatures) | ||
| decoder | ||
| } | ||
| } | ||
| } | ||
| 
      Comment on lines
    
      +575
     to 
      +584
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cache the HyperSync decoder and set address mode like HyperSyncSource getHscDecoder() creates a new decoder every time and doesn’t set address casing behavior. Mirror HyperSyncSource: cache once and toggle checksummed vs lowercase to keep decoded address fields consistent. Apply this diff:  let hscDecoder: ref<option<HyperSyncClient.Decoder.t>> = ref(None)
 let getHscDecoder = () => {
   switch hscDecoder.contents {
   | Some(decoder) => decoder
-  | None => {
-      let decoder = HyperSyncClient.Decoder.fromSignatures(allEventSignatures)
-      decoder
-    }
+  | None => {
+      let decoder = HyperSyncClient.Decoder.fromSignatures(allEventSignatures)
+      // Align decoded address handling with lowercaseAddresses
+      if lowercaseAddresses {
+        decoder.disableChecksummedAddresses()
+      } else {
+        decoder.enableChecksummedAddresses()
+      };
+      hscDecoder := Some(decoder);
+      decoder
+    }
   }
 }🤖 Prompt for AI Agents | ||
|  | ||
| let getItemsOrThrow = async ( | ||
| ~fromBlock, | ||
| ~toBlock, | ||
|  | @@ -603,10 +656,106 @@ let make = ({sourceFor, syncConfig, url, chain, contracts, eventRouter}: options | |
| ) | ||
| } | ||
|  | ||
| let parsedQueueItems = | ||
| let parsedQueueItems = if shouldUseHypersyncClientDecoder { | ||
| // Convert Ethers logs to HyperSync events | ||
| let hyperSyncEvents = logs->Belt.Array.map(convertEthersLogToHyperSyncEvent) | ||
|  | ||
| // Decode using HyperSyncClient decoder | ||
| let parsedEvents = try await getHscDecoder().decodeEvents(hyperSyncEvents) catch { | ||
| | exn => | ||
| raise( | ||
| Source.GetItemsError( | ||
| FailedParsingItems({ | ||
| message: "Failed to parse events using hypersync client decoder. Please double-check your ABI.", | ||
| exn, | ||
| blockNumber: fromBlock, | ||
| logIndex: 0, | ||
| }), | ||
| ), | ||
| ) | ||
| } | ||
|  | ||
| await logs | ||
| ->Array.zip(parsedEvents) | ||
| ->Array.keepMap((( | ||
| log: Ethers.log, | ||
| maybeDecodedEvent: Js.Nullable.t<HyperSyncClient.Decoder.decodedEvent>, | ||
| )) => { | ||
| let topic0 = log.topics[0]->Option.getWithDefault("0x0"->EvmTypes.Hex.fromStringUnsafe) | ||
| let routedAddress = if lowercaseAddresses { | ||
| log.address->Address.Evm.fromAddressLowercaseOrThrow | ||
| } else { | ||
| log.address | ||
| } | ||
|  | ||
| switch eventRouter->EventRouter.get( | ||
| ~tag=EventRouter.getEvmEventId( | ||
| ~sighash=topic0->EvmTypes.Hex.toString, | ||
| ~topicCount=log.topics->Array.length, | ||
| ), | ||
| ~indexingContracts, | ||
| ~contractAddress=routedAddress, | ||
| ~blockNumber=log.blockNumber, | ||
| ) { | ||
| 
      Comment on lines
    
      +678
     to 
      +699
    
   There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: topic0 extraction uses Option on non-option; also normalize routedAddress here log.topics is an array; indexing doesn’t return option. Use Js.Array2.unsafe_get(0) like the Viem branch, and keep routedAddress normalization. Apply this diff: -      await logs
-      ->Array.zip(parsedEvents)
+      logs
+      ->Array.zip(parsedEvents)
       ->Array.keepMap(((
         log: Ethers.log,
         maybeDecodedEvent: Js.Nullable.t<HyperSyncClient.Decoder.decodedEvent>,
       )) => {
-        let topic0 = log.topics[0]->Option.getWithDefault("0x0"->EvmTypes.Hex.fromStringUnsafe)
+        let topic0 = log.topics->Js.Array2.unsafe_get(0)
         let routedAddress = if lowercaseAddresses {
           log.address->Address.Evm.fromAddressLowercaseOrThrow
         } else {
           log.address
         }
 🤖 Prompt for AI Agents | ||
| | None => None | ||
| | Some(eventConfig) => | ||
| switch maybeDecodedEvent { | ||
| | Js.Nullable.Value(decoded) => | ||
| Some( | ||
| ( | ||
| async () => { | ||
| let (block, transaction) = try await Promise.all2(( | ||
| log->getEventBlockOrThrow, | ||
| log->getEventTransactionOrThrow( | ||
| ~transactionSchema=eventConfig.transactionSchema, | ||
| ), | ||
| )) catch { | ||
| | exn => | ||
| raise( | ||
| Source.GetItemsError( | ||
| FailedGettingFieldSelection({ | ||
| message: "Failed getting selected fields. Please double-check your RPC provider returns correct data.", | ||
| exn, | ||
| blockNumber: log.blockNumber, | ||
| logIndex: log.logIndex, | ||
| }), | ||
| ), | ||
| ) | ||
| } | ||
|  | ||
| Internal.Event({ | ||
| eventConfig: (eventConfig :> Internal.eventConfig), | ||
| timestamp: block.timestamp, | ||
| blockNumber: block.number, | ||
| chain, | ||
| logIndex: log.logIndex, | ||
| event: { | ||
| chainId: chain->ChainMap.Chain.toChainId, | ||
| params: decoded->eventConfig.convertHyperSyncEventArgs, | ||
| transaction, | ||
| block: block->( | ||
| Utils.magic: Ethers.JsonRpcProvider.block => Internal.eventBlock | ||
| ), | ||
| srcAddress: routedAddress, | ||
| logIndex: log.logIndex, | ||
| }->Internal.fromGenericEvent, | ||
| }) | ||
| } | ||
| )(), | ||
| ) | ||
| | Js.Nullable.Null | ||
| | Js.Nullable.Undefined => | ||
| None | ||
| } | ||
| } | ||
| }) | ||
| ->Promise.all | ||
| } else { | ||
| // Decode using Viem | ||
| await logs | ||
| ->Belt.Array.keepMap(log => { | ||
| let topic0 = log.topics->Js.Array2.unsafe_get(0) | ||
|  | ||
| switch eventRouter->EventRouter.get( | ||
| ~tag=EventRouter.getEvmEventId( | ||
| ~sighash=topic0->EvmTypes.Hex.toString, | ||
|  | @@ -685,6 +834,7 @@ let make = ({sourceFor, syncConfig, url, chain, contracts, eventRouter}: options | |
| } | ||
| }) | ||
| ->Promise.all | ||
| } | ||
|  | ||
| let optFirstBlockParent = await firstBlockParentPromise | ||
|  | ||
|  | ||
Uh oh!
There was an error while loading. Please reload this page.