From 62992bc09971d3799d644c585070c08f10b76b4c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= <wiktor.tkaczynski@gmail.com>
Date: Tue, 14 Jan 2025 17:50:38 +0100
Subject: [PATCH 1/6] feat: index both mainnet and sepolia

---
 apps/api/src/config.json      |   94 ---
 apps/api/src/config.ts        |  164 +++++
 apps/api/src/currentConfig.ts |   16 -
 apps/api/src/index.ts         |   27 +-
 apps/api/src/ipfs.ts          |   77 ++-
 apps/api/src/overrides.json   |    8 +
 apps/api/src/overrrides.ts    |   53 --
 apps/api/src/utils.ts         |   66 +-
 apps/api/src/writer.ts        | 1214 +++++++++++++++++----------------
 9 files changed, 915 insertions(+), 804 deletions(-)
 delete mode 100644 apps/api/src/config.json
 create mode 100644 apps/api/src/config.ts
 delete mode 100644 apps/api/src/currentConfig.ts
 create mode 100644 apps/api/src/overrides.json
 delete mode 100644 apps/api/src/overrrides.ts

diff --git a/apps/api/src/config.json b/apps/api/src/config.json
deleted file mode 100644
index d99527172..000000000
--- a/apps/api/src/config.json
+++ /dev/null
@@ -1,94 +0,0 @@
-{
-  "network_node_url": "https://starknet-sepolia.infura.io/v3/46a5dd9727bf48d4a132672d3f376146",
-  "optimistic_indexing": true,
-  "decimal_types": {
-    "BigDecimalVP": {
-      "p": 60,
-      "d": 0
-    }
-  },
-  "sources": [
-    {
-      "contract": "0x302d332e9aceb184e5f301cb62c85181e7fc3b30559935c5736e987de579f6e",
-      "start": 17960,
-      "abi": "SpaceFactory",
-      "events": [
-        {
-          "name": "NewContractDeployed",
-          "fn": "handleContractDeployed"
-        }
-      ]
-    }
-  ],
-  "templates": {
-    "Space": {
-      "abi": "Space",
-      "events": [
-        {
-          "name": "SpaceCreated",
-          "fn": "handleSpaceCreated"
-        },
-        {
-          "name": "ProposalCreated",
-          "fn": "handlePropose"
-        },
-        {
-          "name": "ProposalCancelled",
-          "fn": "handleCancel"
-        },
-        {
-          "name": "ProposalUpdated",
-          "fn": "handleUpdate"
-        },
-        {
-          "name": "ProposalExecuted",
-          "fn": "handleExecute"
-        },
-        {
-          "name": "VoteCast",
-          "fn": "handleVote"
-        },
-        {
-          "name": "MetadataUriUpdated",
-          "fn": "handleMetadataUriUpdated"
-        },
-        {
-          "name": "MinVotingDurationUpdated",
-          "fn": "handleMinVotingDurationUpdated"
-        },
-        {
-          "name": "MaxVotingDurationUpdated",
-          "fn": "handleMaxVotingDurationUpdated"
-        },
-        {
-          "name": "VotingDelayUpdated",
-          "fn": "handleVotingDelayUpdated"
-        },
-        {
-          "name": "OwnershipTransferred",
-          "fn": "handleOwnershipTransferred"
-        },
-        {
-          "name": "AuthenticatorsAdded",
-          "fn": "handleAuthenticatorsAdded"
-        },
-        {
-          "name": "AuthenticatorsRemoved",
-          "fn": "handleAuthenticatorsRemoved"
-        },
-        {
-          "name": "VotingStrategiesAdded",
-          "fn": "handleVotingStrategiesAdded"
-        },
-        {
-          "name": "VotingStrategiesRemoved",
-          "fn": "handleVotingStrategiesRemoved"
-        },
-        {
-          "name": "ProposalValidationStrategyUpdated",
-          "fn": "handleProposalValidationStrategyUpdated"
-        }
-      ]
-    }
-  }
-}
diff --git a/apps/api/src/config.ts b/apps/api/src/config.ts
new file mode 100644
index 000000000..cc3622cdc
--- /dev/null
+++ b/apps/api/src/config.ts
@@ -0,0 +1,164 @@
+import { CheckpointConfig } from '@snapshot-labs/checkpoint';
+import { starknetNetworks } from '@snapshot-labs/sx';
+import { validateAndParseAddress } from 'starknet';
+import spaceAbi from './abis/space.json';
+import spaceFactoryAbi from './abis/spaceFactory.json';
+
+export type FullConfig = {
+  indexerName: 'sn' | 'sn-sep';
+  overrides: ReturnType<typeof createOverrides>;
+} & CheckpointConfig;
+
+const CONFIG = {
+  sn: {
+    indexerName: 'sn',
+    networkNodeUrl:
+      'https://starknet-mainnet.infura.io/v3/46a5dd9727bf48d4a132672d3f376146',
+    l1NetworkNodeUrl: 'https://rpc.brovider.xyz/1',
+    contract: starknetNetworks['sn'].Meta.spaceFactory,
+    start: 445498,
+    verifiedSpaces: [
+      '0x009fedaf0d7a480d21a27683b0965c0f8ded35b3f1cac39827a25a06a8a682a4',
+      '0x05ea5ef0c54c84dc7382629684c6e536c0b06246b3b0981c426b42372e3ef263',
+      '0x07c251045154318a2376a3bb65be47d3c90df1740d8e35c9b9d943aa3f240e50',
+      '0x07bd3419669f9f0cc8f19e9e2457089cdd4804a4c41a5729ee9c7fd02ab8ab62'
+    ]
+  },
+  'sn-sep': {
+    indexerName: 'sn-sep',
+    networkNodeUrl:
+      'https://starknet-sepolia.infura.io/v3/46a5dd9727bf48d4a132672d3f376146',
+    l1NetworkNodeUrl: 'https://rpc.brovider.xyz/11155111',
+    contract: starknetNetworks['sn-sep'].Meta.spaceFactory,
+    start: 17960,
+    verifiedSpaces: [
+      '0x0141464688e48ae5b7c83045edb10ecc242ce0e1ad4ff44aca3402f7f47c1ab9'
+    ]
+  }
+};
+
+const manaRpcUrl = process.env.VITE_MANA_URL || 'https://mana.box';
+
+function createOverrides(networkId: keyof typeof CONFIG) {
+  const config = starknetNetworks[networkId];
+
+  return {
+    networkNodeUrl: CONFIG[networkId].networkNodeUrl,
+    l1NetworkNodeUrl: CONFIG[networkId].l1NetworkNodeUrl,
+    manaRpcUrl: `${manaRpcUrl}/stark_rpc/${config.Meta.eip712ChainId}`,
+    baseChainId: config.Meta.herodotusAccumulatesChainId,
+    erc20VotesStrategy: config.Strategies.ERC20Votes,
+    propositionPowerValidationStrategyAddress:
+      config.ProposalValidations.VotingPower,
+    spaceClassHash: config.Meta.masterSpace,
+    verifiedSpaces: CONFIG[networkId].verifiedSpaces,
+    herodotusStrategies: [
+      config.Strategies.OZVotesStorageProof,
+      config.Strategies.OZVotesTrace208StorageProof,
+      config.Strategies.EVMSlotValue
+    ]
+      .filter(address => !!address)
+      .map(strategy => validateAndParseAddress(strategy))
+  };
+}
+
+export function createConfig(indexerName: keyof typeof CONFIG): FullConfig {
+  const { networkNodeUrl, contract, start } = CONFIG[indexerName];
+
+  const overrides = createOverrides(indexerName);
+
+  return {
+    indexerName,
+    overrides,
+    network_node_url: networkNodeUrl,
+    optimistic_indexing: true,
+    sources: [
+      {
+        contract,
+        start,
+        abi: 'SpaceFactory',
+        events: [
+          {
+            name: 'NewContractDeployed',
+            fn: 'handleContractDeployed'
+          }
+        ]
+      }
+    ],
+    templates: {
+      Space: {
+        abi: 'Space',
+        events: [
+          {
+            name: 'SpaceCreated',
+            fn: 'handleSpaceCreated'
+          },
+          {
+            name: 'ProposalCreated',
+            fn: 'handlePropose'
+          },
+          {
+            name: 'ProposalCancelled',
+            fn: 'handleCancel'
+          },
+          {
+            name: 'ProposalUpdated',
+            fn: 'handleUpdate'
+          },
+          {
+            name: 'ProposalExecuted',
+            fn: 'handleExecute'
+          },
+          {
+            name: 'VoteCast',
+            fn: 'handleVote'
+          },
+          {
+            name: 'MetadataUriUpdated',
+            fn: 'handleMetadataUriUpdated'
+          },
+          {
+            name: 'MinVotingDurationUpdated',
+            fn: 'handleMinVotingDurationUpdated'
+          },
+          {
+            name: 'MaxVotingDurationUpdated',
+            fn: 'handleMaxVotingDurationUpdated'
+          },
+          {
+            name: 'VotingDelayUpdated',
+            fn: 'handleVotingDelayUpdated'
+          },
+          {
+            name: 'OwnershipTransferred',
+            fn: 'handleOwnershipTransferred'
+          },
+          {
+            name: 'AuthenticatorsAdded',
+            fn: 'handleAuthenticatorsAdded'
+          },
+          {
+            name: 'AuthenticatorsRemoved',
+            fn: 'handleAuthenticatorsRemoved'
+          },
+          {
+            name: 'VotingStrategiesAdded',
+            fn: 'handleVotingStrategiesAdded'
+          },
+          {
+            name: 'VotingStrategiesRemoved',
+            fn: 'handleVotingStrategiesRemoved'
+          },
+          {
+            name: 'ProposalValidationStrategyUpdated',
+            fn: 'handleProposalValidationStrategyUpdated'
+          }
+        ]
+      }
+    },
+    abis: {
+      SpaceFactory: spaceFactoryAbi,
+      Space: spaceAbi
+    }
+  };
+}
diff --git a/apps/api/src/currentConfig.ts b/apps/api/src/currentConfig.ts
deleted file mode 100644
index 06a431238..000000000
--- a/apps/api/src/currentConfig.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import baseConfig from './config.json';
-import { networkNodeUrl, networkProperties } from './overrrides';
-
-export default {
-  ...baseConfig,
-  network_node_url: networkNodeUrl,
-  sources: baseConfig.sources.map((source, i) => {
-    if (i !== 0) return source;
-
-    return {
-      ...source,
-      contract: networkProperties.factoryAddress,
-      start: networkProperties.startBlock
-    };
-  })
-};
diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
index d1bbf2655..eb84f5f05 100644
--- a/apps/api/src/index.ts
+++ b/apps/api/src/index.ts
@@ -9,10 +9,9 @@ import Checkpoint, {
   LogLevel,
   starknet
 } from '@snapshot-labs/checkpoint';
-import spaceAbi from './abis/space.json';
-import spaceFactoryAbi from './abis/spaceFactory.json';
-import config from './currentConfig';
-import * as writer from './writer';
+import { createConfig } from './config';
+import overrides from './overrides.json';
+import { createWriters } from './writer';
 
 const dir = __dirname.endsWith('dist/src') ? '../' : '';
 const schemaFile = path.join(__dirname, `${dir}../src/schema.gql`);
@@ -25,17 +24,22 @@ if (process.env.CA_CERT) {
   process.env.CA_CERT = process.env.CA_CERT.replace(/\\n/g, '\n');
 }
 
-const indexer = new starknet.StarknetIndexer(writer);
-const checkpoint = new Checkpoint(config, indexer, schema, {
+const snConfig = createConfig('sn');
+const snSepConfig = createConfig('sn-sep');
+
+const snIndexer = new starknet.StarknetIndexer(createWriters(snConfig));
+const snSepIndexer = new starknet.StarknetIndexer(createWriters(snSepConfig));
+
+const checkpoint = new Checkpoint(schema, {
   logLevel: LogLevel.Info,
   resetOnConfigChange: true,
   prettifyLogs: process.env.NODE_ENV !== 'production',
-  abis: {
-    SpaceFactory: spaceFactoryAbi,
-    Space: spaceAbi
-  }
+  overridesConfig: overrides
 });
 
+checkpoint.addIndexer(snConfig.indexerName, snConfig, snIndexer);
+checkpoint.addIndexer(snSepConfig.indexerName, snSepConfig, snSepIndexer);
+
 const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
 
 async function run() {
@@ -65,7 +69,8 @@ async function run() {
     await sleep(PRODUCTION_INDEXER_DELAY);
   }
 
-  // await checkpoint.reset();
+  await checkpoint.resetMetadata();
+  await checkpoint.reset();
   checkpoint.start();
 }
 
diff --git a/apps/api/src/ipfs.ts b/apps/api/src/ipfs.ts
index 7ab97bbfb..5d3bcaf27 100644
--- a/apps/api/src/ipfs.ts
+++ b/apps/api/src/ipfs.ts
@@ -1,9 +1,10 @@
 import { getAddress } from '@ethersproject/address';
 import { Contract as EthContract } from '@ethersproject/contracts';
+import { StaticJsonRpcProvider } from '@ethersproject/providers';
 import { validateAndParseAddress } from 'starknet';
 import L1AvatarExectionStrategyAbi from './abis/l1/L1AvatarExectionStrategy.json';
-import { networkProperties } from './overrrides';
-import { dropIpfs, ethProvider, getJSON, getSpaceName } from './utils';
+import { FullConfig } from './config';
+import { dropIpfs, getJSON, getSpaceName } from './utils';
 import {
   ExecutionStrategy,
   ProposalMetadataItem,
@@ -12,11 +13,21 @@ import {
   VoteMetadataItem
 } from '../.checkpoint/models';
 
-export async function handleSpaceMetadata(space: string, metadataUri: string) {
-  const exists = await SpaceMetadataItem.loadEntity(dropIpfs(metadataUri));
+export async function handleSpaceMetadata(
+  space: string,
+  metadataUri: string,
+  config: FullConfig
+) {
+  const exists = await SpaceMetadataItem.loadEntity(
+    dropIpfs(metadataUri),
+    config.indexerName
+  );
   if (exists) return;
 
-  const spaceMetadataItem = new SpaceMetadataItem(dropIpfs(metadataUri));
+  const spaceMetadataItem = new SpaceMetadataItem(
+    dropIpfs(metadataUri),
+    config.indexerName
+  );
   spaceMetadataItem.name = getSpaceName(space);
   spaceMetadataItem.about = '';
   spaceMetadataItem.avatar = '';
@@ -92,8 +103,12 @@ export async function handleSpaceMetadata(space: string, metadataUri: string) {
         destinations.push(destination);
         uniqueExecutors.push(id);
 
-        let executionStrategy = await ExecutionStrategy.loadEntity(id);
-        if (!executionStrategy) executionStrategy = new ExecutionStrategy(id);
+        let executionStrategy = await ExecutionStrategy.loadEntity(
+          id,
+          config.indexerName
+        );
+        if (!executionStrategy)
+          executionStrategy = new ExecutionStrategy(id, config.indexerName);
 
         executionStrategy.type =
           metadata.properties.execution_strategies_types[i];
@@ -106,6 +121,11 @@ export async function handleSpaceMetadata(space: string, metadataUri: string) {
         if (executionStrategy.type === 'EthRelayer') {
           const l1Destination = getAddress(destination);
 
+          const ethProvider = new StaticJsonRpcProvider(
+            config.overrides.l1NetworkNodeUrl,
+            config.overrides.baseChainId
+          );
+
           const l1AvatarExecutionStrategyContract = new EthContract(
             l1Destination,
             L1AvatarExectionStrategyAbi,
@@ -120,7 +140,7 @@ export async function handleSpaceMetadata(space: string, metadataUri: string) {
           executionStrategy.destination_address = l1Destination;
           executionStrategy.quorum = quorum;
           executionStrategy.treasury = treasury;
-          executionStrategy.treasury_chain = networkProperties.baseChainId;
+          executionStrategy.treasury_chain = config.overrides.baseChainId;
         }
 
         await executionStrategy.save();
@@ -140,11 +160,20 @@ export async function handleSpaceMetadata(space: string, metadataUri: string) {
   await spaceMetadataItem.save();
 }
 
-export async function handleProposalMetadata(metadataUri: string) {
-  const exists = await ProposalMetadataItem.loadEntity(dropIpfs(metadataUri));
+export async function handleProposalMetadata(
+  metadataUri: string,
+  config: FullConfig
+) {
+  const exists = await ProposalMetadataItem.loadEntity(
+    dropIpfs(metadataUri),
+    config.indexerName
+  );
   if (exists) return;
 
-  const proposalMetadataItem = new ProposalMetadataItem(dropIpfs(metadataUri));
+  const proposalMetadataItem = new ProposalMetadataItem(
+    dropIpfs(metadataUri),
+    config.indexerName
+  );
   proposalMetadataItem.choices = ['For', 'Against', 'Abstain'];
   proposalMetadataItem.labels = [];
 
@@ -172,11 +201,20 @@ export async function handleProposalMetadata(metadataUri: string) {
   await proposalMetadataItem.save();
 }
 
-export async function handleVoteMetadata(metadataUri: string) {
-  const exists = await VoteMetadataItem.loadEntity(dropIpfs(metadataUri));
+export async function handleVoteMetadata(
+  metadataUri: string,
+  config: FullConfig
+) {
+  const exists = await VoteMetadataItem.loadEntity(
+    dropIpfs(metadataUri),
+    config.indexerName
+  );
   if (exists) return;
 
-  const voteMetadataItem = new VoteMetadataItem(dropIpfs(metadataUri));
+  const voteMetadataItem = new VoteMetadataItem(
+    dropIpfs(metadataUri),
+    config.indexerName
+  );
 
   const metadata: any = await getJSON(metadataUri);
   voteMetadataItem.reason = metadata.reason ?? '';
@@ -184,14 +222,19 @@ export async function handleVoteMetadata(metadataUri: string) {
   await voteMetadataItem.save();
 }
 
-export async function handleStrategiesParsedMetadata(metadataUri: string) {
+export async function handleStrategiesParsedMetadata(
+  metadataUri: string,
+  config: FullConfig
+) {
   const exists = await StrategiesParsedMetadataDataItem.loadEntity(
-    dropIpfs(metadataUri)
+    dropIpfs(metadataUri),
+    config.indexerName
   );
   if (exists) return;
 
   const strategiesParsedMetadataItem = new StrategiesParsedMetadataDataItem(
-    dropIpfs(metadataUri)
+    dropIpfs(metadataUri),
+    config.indexerName
   );
 
   const metadata: any = await getJSON(metadataUri);
diff --git a/apps/api/src/overrides.json b/apps/api/src/overrides.json
new file mode 100644
index 000000000..74a7b36e1
--- /dev/null
+++ b/apps/api/src/overrides.json
@@ -0,0 +1,8 @@
+{
+  "decimal_types": {
+    "BigDecimalVP": {
+      "p": 60,
+      "d": 0
+    }
+  }
+}
diff --git a/apps/api/src/overrrides.ts b/apps/api/src/overrrides.ts
deleted file mode 100644
index 5c150684b..000000000
--- a/apps/api/src/overrrides.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { starknetNetworks } from '@snapshot-labs/sx';
-import { validateAndParseAddress } from 'starknet';
-
-export const networkNodeUrl =
-  process.env.NETWORK_NODE_URL ||
-  'https://starknet-sepolia.infura.io/v3/46a5dd9727bf48d4a132672d3f376146';
-
-export const manaRpcUrl = process.env.VITE_MANA_URL || 'https://mana.box';
-
-const verifiedSpaces = {
-  sn: [
-    '0x009fedaf0d7a480d21a27683b0965c0f8ded35b3f1cac39827a25a06a8a682a4',
-    '0x05ea5ef0c54c84dc7382629684c6e536c0b06246b3b0981c426b42372e3ef263',
-    '0x07c251045154318a2376a3bb65be47d3c90df1740d8e35c9b9d943aa3f240e50',
-    '0x07bd3419669f9f0cc8f19e9e2457089cdd4804a4c41a5729ee9c7fd02ab8ab62'
-  ],
-  'sn-sep': [
-    '0x0141464688e48ae5b7c83045edb10ecc242ce0e1ad4ff44aca3402f7f47c1ab9'
-  ]
-};
-
-const createConfig = (
-  networkId: keyof typeof starknetNetworks,
-  { startBlock }: { startBlock: number }
-) => {
-  const config = starknetNetworks[networkId];
-
-  return {
-    manaRpcUrl: `${manaRpcUrl}/stark_rpc/${config.Meta.eip712ChainId}`,
-    baseChainId: config.Meta.herodotusAccumulatesChainId,
-    factoryAddress: config.Meta.spaceFactory,
-    erc20VotesStrategy: config.Strategies.ERC20Votes,
-    propositionPowerValidationStrategyAddress:
-      config.ProposalValidations.VotingPower,
-    spaceClassHash: config.Meta.masterSpace,
-    verifiedSpaces: verifiedSpaces[networkId],
-    herodotusStrategies: [
-      config.Strategies.OZVotesStorageProof,
-      config.Strategies.OZVotesTrace208StorageProof,
-      config.Strategies.EVMSlotValue
-    ]
-      .filter(address => !!address)
-      .map(strategy => validateAndParseAddress(strategy)),
-    startBlock
-  };
-};
-
-let networkProperties = createConfig('sn-sep', { startBlock: 17960 });
-if (process.env.NETWORK === 'SN_MAIN') {
-  networkProperties = createConfig('sn', { startBlock: 445498 });
-}
-
-export { networkProperties };
diff --git a/apps/api/src/utils.ts b/apps/api/src/utils.ts
index dd6d50ae6..5714b98e4 100644
--- a/apps/api/src/utils.ts
+++ b/apps/api/src/utils.ts
@@ -1,7 +1,7 @@
 import { getAddress } from '@ethersproject/address';
 import { BigNumber } from '@ethersproject/bignumber';
 import { Contract as EthContract } from '@ethersproject/contracts';
-import { JsonRpcProvider } from '@ethersproject/providers';
+import { StaticJsonRpcProvider } from '@ethersproject/providers';
 import { faker } from '@faker-js/faker';
 import { utils } from '@snapshot-labs/sx';
 import fetch from 'cross-fetch';
@@ -17,8 +17,8 @@ import {
 import EncodersAbi from './abis/encoders.json';
 import ExecutionStrategyAbi from './abis/executionStrategy.json';
 import SimpleQuorumExecutionStrategyAbi from './abis/l1/SimpleQuorumExecutionStrategy.json';
+import { FullConfig } from './config';
 import { handleStrategiesParsedMetadata } from './ipfs';
-import { networkNodeUrl, networkProperties } from './overrrides';
 import {
   Space,
   StrategiesParsedMetadataItem,
@@ -30,14 +30,6 @@ type StrategyConfig = {
   params: BigNumberish[];
 };
 
-export const ethProvider = new JsonRpcProvider(
-  process.env.L1_NETWORK_NODE_URL ??
-    `https://rpc.brovider.xyz/${networkProperties.baseChainId}`
-);
-const starkProvider = new RpcProvider({
-  nodeUrl: networkNodeUrl
-});
-
 const encodersAbi = new CallData(EncodersAbi);
 
 export function getCurrentTimestamp() {
@@ -145,11 +137,16 @@ export function getVoteValue(label: string) {
 
 export async function handleExecutionStrategy(
   address: string,
-  payload: string[]
+  payload: string[],
+  config: FullConfig
 ) {
   try {
     if (address === '0x0') return null;
 
+    const starkProvider = new RpcProvider({
+      nodeUrl: config.overrides.networkNodeUrl
+    });
+
     const executionContract = new Contract(
       ExecutionStrategyAbi,
       address,
@@ -170,6 +167,11 @@ export async function handleExecutionStrategy(
         throw new Error('Invalid payload for EthRelayer execution strategy');
       destinationAddress = formatAddress('Ethereum', l1Destination);
 
+      const ethProvider = new StaticJsonRpcProvider(
+        config.overrides.l1NetworkNodeUrl,
+        config.overrides.baseChainId
+      );
+
       const SimpleQuorumExecutionStrategyContract = new EthContract(
         destinationAddress,
         SimpleQuorumExecutionStrategyAbi,
@@ -197,7 +199,8 @@ export async function updateProposaValidationStrategy(
   space: Space,
   validationStrategyAddress: string,
   validationStrategyParams: string[],
-  metadataUri: string[]
+  metadataUri: string[],
+  config: FullConfig
 ) {
   space.validation_strategy = validationStrategyAddress;
   space.validation_strategy_params = validationStrategyParams.join(',');
@@ -209,7 +212,7 @@ export async function updateProposaValidationStrategy(
   if (
     utils.encoding.hexPadLeft(validationStrategyAddress) ===
     utils.encoding.hexPadLeft(
-      networkProperties.propositionPowerValidationStrategyAddress
+      config.overrides.propositionPowerValidationStrategyAddress
     )
   ) {
     const parsed = encodersAbi.parse(
@@ -232,7 +235,8 @@ export async function updateProposaValidationStrategy(
     try {
       await handleVotingPowerValidationMetadata(
         space.id,
-        space.voting_power_validation_strategy_metadata
+        space.voting_power_validation_strategy_metadata,
+        config
       );
     } catch (e) {
       console.log('failed to handle voting power strategies metadata', e);
@@ -244,6 +248,7 @@ export async function handleStrategiesMetadata(
   spaceId: string,
   metadataUris: string[],
   startingIndex: number,
+  config: FullConfig,
   type:
     | typeof StrategiesParsedMetadataItem
     | typeof VotingPowerValidationStrategiesParsedMetadataItem = StrategiesParsedMetadataItem
@@ -255,17 +260,17 @@ export async function handleStrategiesMetadata(
     const index = startingIndex + i;
     const uniqueId = `${spaceId}/${index}/${dropIpfs(metadataUri)}`;
 
-    const exists = await type.loadEntity(uniqueId);
+    const exists = await type.loadEntity(uniqueId, config.indexerName);
     if (exists) continue;
 
-    const strategiesParsedMetadataItem = new type(uniqueId);
+    const strategiesParsedMetadataItem = new type(uniqueId, config.indexerName);
     strategiesParsedMetadataItem.space = spaceId;
     strategiesParsedMetadataItem.index = index;
 
     if (metadataUri.startsWith('ipfs://')) {
       strategiesParsedMetadataItem.data = dropIpfs(metadataUri);
 
-      await handleStrategiesParsedMetadata(metadataUri);
+      await handleStrategiesParsedMetadata(metadataUri, config);
     }
 
     await strategiesParsedMetadataItem.save();
@@ -274,7 +279,8 @@ export async function handleStrategiesMetadata(
 
 export async function handleVotingPowerValidationMetadata(
   spaceId: string,
-  metadataUri: string
+  metadataUri: string,
+  config: FullConfig
 ) {
   if (!metadataUri) return;
 
@@ -285,20 +291,24 @@ export async function handleVotingPowerValidationMetadata(
     spaceId,
     metadata.strategies_metadata,
     0,
+    config,
     VotingPowerValidationStrategiesParsedMetadataItem
   );
 }
 
-export async function registerProposal({
-  l1TokenAddress,
-  strategyAddress,
-  snapshotTimestamp
-}: {
-  l1TokenAddress: string;
-  strategyAddress: string;
-  snapshotTimestamp: number;
-}) {
-  const res = await fetch(networkProperties.manaRpcUrl, {
+export async function registerProposal(
+  {
+    l1TokenAddress,
+    strategyAddress,
+    snapshotTimestamp
+  }: {
+    l1TokenAddress: string;
+    strategyAddress: string;
+    snapshotTimestamp: number;
+  },
+  config: FullConfig
+) {
+  const res = await fetch(config.overrides.manaRpcUrl, {
     method: 'POST',
     headers: {
       'Content-Type': 'application/json'
diff --git a/apps/api/src/writer.ts b/apps/api/src/writer.ts
index 6cbda47c5..dd11456b6 100644
--- a/apps/api/src/writer.ts
+++ b/apps/api/src/writer.ts
@@ -1,11 +1,11 @@
 import { starknet } from '@snapshot-labs/checkpoint';
 import { validateAndParseAddress } from 'starknet';
+import { FullConfig } from './config';
 import {
   handleProposalMetadata,
   handleSpaceMetadata,
   handleVoteMetadata
 } from './ipfs';
-import { networkProperties } from './overrrides';
 import {
   dropIpfs,
   findVariant,
@@ -31,673 +31,717 @@ type Strategy = {
   params: string[];
 };
 
-export const handleContractDeployed: starknet.Writer = async ({
-  blockNumber,
-  event,
-  instance
-}) => {
-  console.log('Handle contract deployed');
-
-  if (!event) return;
-
-  const paddedClassHash = validateAndParseAddress(event.class_hash);
-
-  if (paddedClassHash === networkProperties.spaceClassHash) {
-    await instance.executeTemplate('Space', {
-      contract: event.contract_address,
-      start: blockNumber
-    });
-  } else {
-    console.log('Unknown class hash', paddedClassHash);
-  }
-};
+export function createWriters(config: FullConfig) {
+  const handleContractDeployed: starknet.Writer = async ({
+    blockNumber,
+    event,
+    helpers: { executeTemplate }
+  }) => {
+    console.log('Handle contract deployed');
+
+    if (!event) return;
+
+    const paddedClassHash = validateAndParseAddress(event.class_hash);
+
+    if (paddedClassHash === config.overrides.spaceClassHash) {
+      await executeTemplate('Space', {
+        contract: event.contract_address,
+        start: blockNumber
+      });
+    } else {
+      console.log('Unknown class hash', paddedClassHash);
+    }
+  };
+
+  const handleSpaceCreated: starknet.Writer = async ({ block, tx, event }) => {
+    console.log('Handle space created');
+
+    if (!event || !tx.transaction_hash) return;
 
-export const handleSpaceCreated: starknet.Writer = async ({
-  block,
-  tx,
-  event
-}) => {
-  console.log('Handle space created');
-
-  if (!event || !tx.transaction_hash) return;
-
-  const strategies: string[] = event.voting_strategies.map(
-    (strategy: Strategy) => strategy.address
-  );
-  const strategiesParams = event.voting_strategies.map((strategy: Strategy) =>
-    strategy.params.join(',')
-  ); // different format than sx-evm
-  const strategiesMetadataUris = event.voting_strategy_metadata_uris.map(
-    (array: string[]) => longStringToText(array)
-  );
-
-  const id = validateAndParseAddress(event.space);
-
-  const space = new Space(id);
-  space.verified = networkProperties.verifiedSpaces.includes(id);
-  space.turbo = false;
-  space.metadata = null;
-  space.controller = validateAndParseAddress(event.owner);
-  space.voting_delay = Number(BigInt(event.voting_delay).toString());
-  space.min_voting_period = Number(
-    BigInt(event.min_voting_duration).toString()
-  );
-  space.max_voting_period = Number(
-    BigInt(event.max_voting_duration).toString()
-  );
-  space.proposal_threshold = '0';
-  space.strategies_indices = strategies.map((_, i) => i);
-  // NOTE: deprecated
-  space.strategies_indicies = space.strategies_indices;
-  space.strategies = strategies;
-  space.next_strategy_index = strategies.length;
-  space.strategies_params = strategiesParams;
-  space.strategies_metadata = strategiesMetadataUris;
-  space.authenticators = event.authenticators;
-  space.proposal_count = 0;
-  space.vote_count = 0;
-  space.proposer_count = 0;
-  space.voter_count = 0;
-  space.created = block?.timestamp ?? getCurrentTimestamp();
-  space.tx = tx.transaction_hash;
-
-  await updateProposaValidationStrategy(
-    space,
-    event.proposal_validation_strategy.address,
-    event.proposal_validation_strategy.params,
-    event.proposal_validation_strategy_metadata_uri
-  );
-
-  try {
-    const metadataUri = longStringToText(event.metadata_uri || []).replaceAll(
-      '\x00',
-      ''
+    const strategies: string[] = event.voting_strategies.map(
+      (strategy: Strategy) => strategy.address
+    );
+    const strategiesParams = event.voting_strategies.map((strategy: Strategy) =>
+      strategy.params.join(',')
+    ); // different format than sx-evm
+    const strategiesMetadataUris = event.voting_strategy_metadata_uris.map(
+      (array: string[]) => longStringToText(array)
     );
-    await handleSpaceMetadata(space.id, metadataUri);
 
-    space.metadata = dropIpfs(metadataUri);
-  } catch (e) {
-    console.log('failed to parse space metadata', e);
-  }
+    const id = validateAndParseAddress(event.space);
 
-  try {
-    await handleStrategiesMetadata(space.id, strategiesMetadataUris, 0);
-  } catch (e) {
-    console.log('failed to handle strategies metadata', e);
-  }
+    const space = new Space(id, config.indexerName);
+    space.verified = config.overrides.verifiedSpaces.includes(id);
+    space.turbo = false;
+    space.metadata = null;
+    space.controller = validateAndParseAddress(event.owner);
+    space.voting_delay = Number(BigInt(event.voting_delay).toString());
+    space.min_voting_period = Number(
+      BigInt(event.min_voting_duration).toString()
+    );
+    space.max_voting_period = Number(
+      BigInt(event.max_voting_duration).toString()
+    );
+    space.proposal_threshold = '0';
+    space.strategies_indices = strategies.map((_, i) => i);
+    // NOTE: deprecated
+    space.strategies_indicies = space.strategies_indices;
+    space.strategies = strategies;
+    space.next_strategy_index = strategies.length;
+    space.strategies_params = strategiesParams;
+    space.strategies_metadata = strategiesMetadataUris;
+    space.authenticators = event.authenticators;
+    space.proposal_count = 0;
+    space.vote_count = 0;
+    space.proposer_count = 0;
+    space.voter_count = 0;
+    space.created = block?.timestamp ?? getCurrentTimestamp();
+    space.tx = tx.transaction_hash;
+
+    await updateProposaValidationStrategy(
+      space,
+      event.proposal_validation_strategy.address,
+      event.proposal_validation_strategy.params,
+      event.proposal_validation_strategy_metadata_uri,
+      config
+    );
 
-  await space.save();
-};
+    try {
+      const metadataUri = longStringToText(event.metadata_uri || []).replaceAll(
+        '\x00',
+        ''
+      );
+      await handleSpaceMetadata(space.id, metadataUri, config);
 
-export const handleMetadataUriUpdated: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+      space.metadata = dropIpfs(metadataUri);
+    } catch (e) {
+      console.log('failed to parse space metadata', e);
+    }
 
-  console.log('Handle space metadata uri updated');
+    try {
+      await handleStrategiesMetadata(
+        space.id,
+        strategiesMetadataUris,
+        0,
+        config
+      );
+    } catch (e) {
+      console.log('failed to handle strategies metadata', e);
+    }
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    await space.save();
+  };
+
+  const handleMetadataUriUpdated: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
+
+    console.log('Handle space metadata uri updated');
+
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+
+    try {
+      const metadataUri = longStringToText(event.metadata_uri).replaceAll(
+        '\x00',
+        ''
+      );
+      await handleSpaceMetadata(spaceId, metadataUri, config);
+
+      const space = await Space.loadEntity(spaceId, config.indexerName);
+      if (!space) return;
+
+      space.metadata = dropIpfs(metadataUri);
+
+      await space.save();
+    } catch (e) {
+      console.log('failed to update space metadata', e);
+    }
+  };
+
+  const handleMinVotingDurationUpdated: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
+
+    console.log('Handle space min voting duration updated');
+
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  try {
-    const metadataUri = longStringToText(event.metadata_uri).replaceAll(
-      '\x00',
-      ''
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
+
+    space.min_voting_period = Number(
+      BigInt(event.min_voting_duration).toString()
     );
-    await handleSpaceMetadata(spaceId, metadataUri);
 
-    const space = await Space.loadEntity(spaceId);
+    await space.save();
+  };
+
+  const handleMaxVotingDurationUpdated: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
+
+    console.log('Handle space max voting duration updated');
+
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+
+    const space = await Space.loadEntity(spaceId, config.indexerName);
     if (!space) return;
 
-    space.metadata = dropIpfs(metadataUri);
+    space.max_voting_period = Number(
+      BigInt(event.max_voting_duration).toString()
+    );
 
     await space.save();
-  } catch (e) {
-    console.log('failed to update space metadata', e);
-  }
-};
+  };
 
-export const handleMinVotingDurationUpdated: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+  const handleOwnershipTransferred: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  console.log('Handle space min voting duration updated');
+    console.log('Handle space ownership transferred');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
 
-  space.min_voting_period = Number(
-    BigInt(event.min_voting_duration).toString()
-  );
+    space.controller = validateAndParseAddress(event.new_owner);
 
-  await space.save();
-};
+    await space.save();
+  };
 
-export const handleMaxVotingDurationUpdated: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+  const handleVotingDelayUpdated: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  console.log('Handle space max voting duration updated');
+    console.log('Handle space voting delay updated');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
 
-  space.max_voting_period = Number(
-    BigInt(event.max_voting_duration).toString()
-  );
+    space.voting_delay = Number(BigInt(event.voting_delay).toString());
 
-  await space.save();
-};
+    await space.save();
+  };
 
-export const handleOwnershipTransferred: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+  const handleAuthenticatorsAdded: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  console.log('Handle space ownership transferred');
+    console.log('Handle space authenticators added');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
 
-  space.controller = validateAndParseAddress(event.new_owner);
+    space.authenticators = [
+      ...new Set([...space.authenticators, ...event.authenticators])
+    ];
 
-  await space.save();
-};
+    await space.save();
+  };
 
-export const handleVotingDelayUpdated: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+  const handleAuthenticatorsRemoved: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  console.log('Handle space voting delay updated');
+    console.log('Handle space authenticators removed');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
 
-  space.voting_delay = Number(BigInt(event.voting_delay).toString());
+    space.authenticators = space.authenticators.filter(
+      authenticator => !event.authenticators.includes(authenticator)
+    );
 
-  await space.save();
-};
+    await space.save();
+  };
 
-export const handleAuthenticatorsAdded: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+  const handleVotingStrategiesAdded: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  console.log('Handle space authenticators added');
+    console.log('Handle space voting strategies added');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
 
-  space.authenticators = [
-    ...new Set([...space.authenticators, ...event.authenticators])
-  ];
+    const initialNextStrategy = space.next_strategy_index;
 
-  await space.save();
-};
+    const strategies: string[] = event.voting_strategies.map(
+      (strategy: Strategy) => strategy.address
+    );
+    const strategiesParams = event.voting_strategies.map((strategy: Strategy) =>
+      strategy.params.join(',')
+    );
+    const strategiesMetadataUris = event.voting_strategy_metadata_uris.map(
+      (array: string[]) => longStringToText(array)
+    );
 
-export const handleAuthenticatorsRemoved: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+    space.strategies_indices = [
+      ...space.strategies_indices,
+      ...strategies.map((_, i) => space.next_strategy_index + i)
+    ];
+    // NOTE: deprecated
+    space.strategies_indicies = space.strategies_indices;
+    space.strategies = [...space.strategies, ...strategies];
+    space.next_strategy_index += strategies.length;
+    space.strategies_params = [...space.strategies_params, ...strategiesParams];
+    space.strategies_metadata = [
+      ...space.strategies_metadata,
+      ...strategiesMetadataUris
+    ];
 
-  console.log('Handle space authenticators removed');
+    try {
+      await handleStrategiesMetadata(
+        space.id,
+        strategiesMetadataUris,
+        initialNextStrategy,
+        config
+      );
+    } catch (e) {
+      console.log('failed to handle strategies metadata', e);
+    }
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    await space.save();
+  };
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+  const handleVotingStrategiesRemoved: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  space.authenticators = space.authenticators.filter(
-    authenticator => !event.authenticators.includes(authenticator)
-  );
+    console.log('Handle space voting strategies removed');
 
-  await space.save();
-};
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-export const handleVotingStrategiesAdded: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
-
-  console.log('Handle space voting strategies added');
-
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
-
-  const initialNextStrategy = space.next_strategy_index;
-
-  const strategies: string[] = event.voting_strategies.map(
-    (strategy: Strategy) => strategy.address
-  );
-  const strategiesParams = event.voting_strategies.map((strategy: Strategy) =>
-    strategy.params.join(',')
-  );
-  const strategiesMetadataUris = event.voting_strategy_metadata_uris.map(
-    (array: string[]) => longStringToText(array)
-  );
-
-  space.strategies_indices = [
-    ...space.strategies_indices,
-    ...strategies.map((_, i) => space.next_strategy_index + i)
-  ];
-  // NOTE: deprecated
-  space.strategies_indicies = space.strategies_indices;
-  space.strategies = [...space.strategies, ...strategies];
-  space.next_strategy_index += strategies.length;
-  space.strategies_params = [...space.strategies_params, ...strategiesParams];
-  space.strategies_metadata = [
-    ...space.strategies_metadata,
-    ...strategiesMetadataUris
-  ];
-
-  try {
-    await handleStrategiesMetadata(
-      space.id,
-      strategiesMetadataUris,
-      initialNextStrategy
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
+
+    const indicesToRemove = event.voting_strategy_indices.map((index: string) =>
+      space.strategies_indices.indexOf(parseInt(index))
     );
-  } catch (e) {
-    console.log('failed to handle strategies metadata', e);
-  }
 
-  await space.save();
-};
+    space.strategies_indices = space.strategies_indices.filter(
+      (_, i) => !indicesToRemove.includes(i)
+    );
+    // NOTE: deprecated
+    space.strategies_indicies = space.strategies_indices;
+    space.strategies = space.strategies.filter(
+      (_, i) => !indicesToRemove.includes(i)
+    );
+    space.strategies_params = space.strategies_params.filter(
+      (_, i) => !indicesToRemove.includes(i)
+    );
+    space.strategies_metadata = space.strategies_metadata.filter(
+      (_, i) => !indicesToRemove.includes(i)
+    );
 
-export const handleVotingStrategiesRemoved: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
-
-  console.log('Handle space voting strategies removed');
-
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
-
-  const indicesToRemove = event.voting_strategy_indices.map((index: string) =>
-    space.strategies_indices.indexOf(parseInt(index))
-  );
-
-  space.strategies_indices = space.strategies_indices.filter(
-    (_, i) => !indicesToRemove.includes(i)
-  );
-  // NOTE: deprecated
-  space.strategies_indicies = space.strategies_indices;
-  space.strategies = space.strategies.filter(
-    (_, i) => !indicesToRemove.includes(i)
-  );
-  space.strategies_params = space.strategies_params.filter(
-    (_, i) => !indicesToRemove.includes(i)
-  );
-  space.strategies_metadata = space.strategies_metadata.filter(
-    (_, i) => !indicesToRemove.includes(i)
-  );
-
-  await space.save();
-};
+    await space.save();
+  };
 
-export const handleProposalValidationStrategyUpdated: starknet.Writer = async ({
-  rawEvent,
-  event
-}) => {
-  if (!event || !rawEvent) return;
+  const handleProposalValidationStrategyUpdated: starknet.Writer = async ({
+    rawEvent,
+    event
+  }) => {
+    if (!event || !rawEvent) return;
 
-  console.log('Handle space proposal validation strategy updated');
+    console.log('Handle space proposal validation strategy updated');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
 
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
 
-  await updateProposaValidationStrategy(
-    space,
-    event.proposal_validation_strategy.address,
-    event.proposal_validation_strategy.params,
-    event.proposal_validation_strategy_metadata_uri
-  );
+    await updateProposaValidationStrategy(
+      space,
+      event.proposal_validation_strategy.address,
+      event.proposal_validation_strategy.params,
+      event.proposal_validation_strategy_metadata_uri,
+      config
+    );
 
-  await space.save();
-};
+    await space.save();
+  };
 
-export const handlePropose: starknet.Writer = async ({
-  tx,
-  rawEvent,
-  event
-}) => {
-  if (!rawEvent || !event || !tx.transaction_hash) return;
-
-  console.log('Handle propose');
-
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-
-  const space = await Space.loadEntity(spaceId);
-  if (!space) return;
-
-  const proposalId = parseInt(BigInt(event.proposal_id).toString());
-  const author = formatAddressVariant(findVariant(event.author));
-
-  const created =
-    BigInt(event.proposal.start_timestamp) - BigInt(space.voting_delay);
-
-  // for erc20votes strategies we have to add artificial delay to prevent voting within same block
-  // snapshot needs to remain the same as we need real timestamp to compute VP
-  let startTimestamp = BigInt(event.proposal.start_timestamp);
-  let minEnd = BigInt(event.proposal.min_end_timestamp);
-  if (
-    space.strategies.some(
-      strategy => strategy === networkProperties.erc20VotesStrategy
-    )
-  ) {
-    const minimumDelay = 10n * 60n;
-    startTimestamp =
-      startTimestamp > created + minimumDelay
-        ? startTimestamp
-        : created + minimumDelay;
-    minEnd = minEnd > startTimestamp ? minEnd : startTimestamp;
-  }
-
-  const proposal = new Proposal(`${spaceId}/${proposalId}`);
-  proposal.proposal_id = proposalId;
-  proposal.space = spaceId;
-  proposal.author = author.address;
-  proposal.metadata = null;
-  proposal.execution_hash = event.proposal.execution_payload_hash;
-  proposal.start = parseInt(startTimestamp.toString());
-  proposal.min_end = parseInt(minEnd.toString());
-  proposal.max_end = parseInt(
-    BigInt(event.proposal.max_end_timestamp).toString()
-  );
-  proposal.snapshot = parseInt(
-    BigInt(event.proposal.start_timestamp).toString()
-  );
-  proposal.execution_time = 0;
-  proposal.execution_strategy = validateAndParseAddress(
-    event.proposal.execution_strategy
-  );
-  proposal.execution_strategy_type = 'none';
-  proposal.type = 'basic';
-  proposal.scores_1 = '0';
-  proposal.scores_2 = '0';
-  proposal.scores_3 = '0';
-  proposal.scores_total = '0';
-  proposal.quorum = 0n;
-  proposal.strategies_indices = space.strategies_indices;
-  // NOTE: deprecated
-  proposal.strategies_indicies = proposal.strategies_indices;
-  proposal.strategies = space.strategies;
-  proposal.strategies_params = space.strategies_params;
-  proposal.created = parseInt(created.toString());
-  proposal.tx = tx.transaction_hash;
-  proposal.execution_tx = null;
-  proposal.veto_tx = null;
-  proposal.vote_count = 0;
-  proposal.execution_ready = true;
-  proposal.executed = false;
-  proposal.vetoed = false;
-  proposal.completed = false;
-  proposal.cancelled = false;
-
-  const executionStrategy = await handleExecutionStrategy(
-    event.proposal.execution_strategy,
-    event.payload
-  );
-  if (executionStrategy) {
-    proposal.execution_strategy_type = executionStrategy.executionStrategyType;
-    proposal.execution_destination = executionStrategy.destinationAddress;
-    proposal.quorum = executionStrategy.quorum;
-  }
-
-  try {
-    const metadataUri = longStringToText(event.metadata_uri);
-    await handleProposalMetadata(metadataUri);
-
-    proposal.metadata = dropIpfs(metadataUri);
-  } catch (e) {
-    console.log(JSON.stringify(e).slice(0, 256));
-  }
-
-  const existingUser = await User.loadEntity(author.address);
-  if (existingUser) {
-    existingUser.proposal_count += 1;
-    await existingUser.save();
-  } else {
-    const user = new User(author.address);
-    user.address_type = author.type;
-    user.created = parseInt(created.toString());
-    await user.save();
-  }
-
-  let leaderboardItem = await Leaderboard.loadEntity(
-    `${spaceId}/${author.address}`
-  );
-  if (!leaderboardItem) {
-    leaderboardItem = new Leaderboard(`${spaceId}/${author.address}`);
-    leaderboardItem.space = spaceId;
-    leaderboardItem.user = author.address;
-    leaderboardItem.vote_count = 0;
-    leaderboardItem.proposal_count = 0;
-  }
-
-  leaderboardItem.proposal_count += 1;
-  await leaderboardItem.save();
-
-  if (leaderboardItem.proposal_count === 1) space.proposer_count += 1;
-  space.proposal_count += 1;
-
-  const herodotusStrategiesIndices = space.strategies
-    .map((strategy, i) => [strategy, i] as const)
-    .filter(([strategy]) =>
-      networkProperties.herodotusStrategies.includes(
-        validateAndParseAddress(strategy)
+  const handlePropose: starknet.Writer = async ({ tx, rawEvent, event }) => {
+    if (!rawEvent || !event || !tx.transaction_hash) return;
+
+    console.log('Handle propose');
+
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (!space) return;
+
+    const proposalId = parseInt(BigInt(event.proposal_id).toString());
+    const author = formatAddressVariant(findVariant(event.author));
+
+    const created =
+      BigInt(event.proposal.start_timestamp) - BigInt(space.voting_delay);
+
+    // for erc20votes strategies we have to add artificial delay to prevent voting within same block
+    // snapshot needs to remain the same as we need real timestamp to compute VP
+    let startTimestamp = BigInt(event.proposal.start_timestamp);
+    let minEnd = BigInt(event.proposal.min_end_timestamp);
+    if (
+      space.strategies.some(
+        strategy => strategy === config.overrides.erc20VotesStrategy
       )
+    ) {
+      const minimumDelay = 10n * 60n;
+      startTimestamp =
+        startTimestamp > created + minimumDelay
+          ? startTimestamp
+          : created + minimumDelay;
+      minEnd = minEnd > startTimestamp ? minEnd : startTimestamp;
+    }
+
+    const proposal = new Proposal(
+      `${spaceId}/${proposalId}`,
+      config.indexerName
+    );
+    proposal.proposal_id = proposalId;
+    proposal.space = spaceId;
+    proposal.author = author.address;
+    proposal.metadata = null;
+    proposal.execution_hash = event.proposal.execution_payload_hash;
+    proposal.start = parseInt(startTimestamp.toString());
+    proposal.min_end = parseInt(minEnd.toString());
+    proposal.max_end = parseInt(
+      BigInt(event.proposal.max_end_timestamp).toString()
     );
+    proposal.snapshot = parseInt(
+      BigInt(event.proposal.start_timestamp).toString()
+    );
+    proposal.execution_time = 0;
+    proposal.execution_strategy = validateAndParseAddress(
+      event.proposal.execution_strategy
+    );
+    proposal.execution_strategy_type = 'none';
+    proposal.type = 'basic';
+    proposal.scores_1 = '0';
+    proposal.scores_2 = '0';
+    proposal.scores_3 = '0';
+    proposal.scores_total = '0';
+    proposal.quorum = 0n;
+    proposal.strategies_indices = space.strategies_indices;
+    // NOTE: deprecated
+    proposal.strategies_indicies = proposal.strategies_indices;
+    proposal.strategies = space.strategies;
+    proposal.strategies_params = space.strategies_params;
+    proposal.created = parseInt(created.toString());
+    proposal.tx = tx.transaction_hash;
+    proposal.execution_tx = null;
+    proposal.veto_tx = null;
+    proposal.vote_count = 0;
+    proposal.execution_ready = true;
+    proposal.executed = false;
+    proposal.vetoed = false;
+    proposal.completed = false;
+    proposal.cancelled = false;
+
+    const executionStrategy = await handleExecutionStrategy(
+      event.proposal.execution_strategy,
+      event.payload,
+      config
+    );
+    if (executionStrategy) {
+      proposal.execution_strategy_type =
+        executionStrategy.executionStrategyType;
+      proposal.execution_destination = executionStrategy.destinationAddress;
+      proposal.quorum = executionStrategy.quorum;
+    }
+
+    try {
+      const metadataUri = longStringToText(event.metadata_uri);
+      await handleProposalMetadata(metadataUri, config);
+
+      proposal.metadata = dropIpfs(metadataUri);
+    } catch (e) {
+      console.log(JSON.stringify(e).slice(0, 256));
+    }
+
+    const existingUser = await User.loadEntity(
+      author.address,
+      config.indexerName
+    );
+    if (existingUser) {
+      existingUser.proposal_count += 1;
+      await existingUser.save();
+    } else {
+      const user = new User(author.address, config.indexerName);
+      user.address_type = author.type;
+      user.created = parseInt(created.toString());
+      await user.save();
+    }
+
+    let leaderboardItem = await Leaderboard.loadEntity(
+      `${spaceId}/${author.address}`,
+      config.indexerName
+    );
+    if (!leaderboardItem) {
+      leaderboardItem = new Leaderboard(
+        `${spaceId}/${author.address}`,
+        config.indexerName
+      );
+      leaderboardItem.space = spaceId;
+      leaderboardItem.user = author.address;
+      leaderboardItem.vote_count = 0;
+      leaderboardItem.proposal_count = 0;
+    }
+
+    leaderboardItem.proposal_count += 1;
+    await leaderboardItem.save();
+
+    if (leaderboardItem.proposal_count === 1) space.proposer_count += 1;
+    space.proposal_count += 1;
+
+    const herodotusStrategiesIndices = space.strategies
+      .map((strategy, i) => [strategy, i] as const)
+      .filter(([strategy]) =>
+        config.overrides.herodotusStrategies.includes(
+          validateAndParseAddress(strategy)
+        )
+      );
+
+    for (const herodotusStrategy of herodotusStrategiesIndices) {
+      const [strategy, i] = herodotusStrategy;
+      const params = space.strategies_params[i];
+      if (!params) continue;
+
+      const [l1TokenAddress] = params.split(',');
+      if (!l1TokenAddress) continue;
+
+      try {
+        await registerProposal(
+          {
+            l1TokenAddress,
+            strategyAddress: strategy,
+            snapshotTimestamp: proposal.snapshot
+          },
+          config
+        );
+      } catch (e) {
+        console.log('failed to register proposal');
+      }
+    }
+
+    await Promise.all([proposal.save(), space.save()]);
+  };
+
+  const handleCancel: starknet.Writer = async ({ rawEvent, event }) => {
+    if (!rawEvent || !event) return;
+
+    console.log('Handle cancel');
+
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const proposalId = `${spaceId}/${parseInt(event.proposal_id)}`;
+
+    const [proposal, space] = await Promise.all([
+      Proposal.loadEntity(proposalId, config.indexerName),
+      Space.loadEntity(spaceId, config.indexerName)
+    ]);
+    if (!proposal || !space) return;
 
-  for (const herodotusStrategy of herodotusStrategiesIndices) {
-    const [strategy, i] = herodotusStrategy;
-    const params = space.strategies_params[i];
-    if (!params) continue;
+    proposal.cancelled = true;
+    space.proposal_count -= 1;
+    space.vote_count -= proposal.vote_count;
 
-    const [l1TokenAddress] = params.split(',');
-    if (!l1TokenAddress) continue;
+    await Promise.all([proposal.save(), space.save()]);
+  };
+
+  const handleUpdate: starknet.Writer = async ({ block, rawEvent, event }) => {
+    if (!rawEvent || !event) return;
+
+    console.log('Handle update');
+
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const proposalId = `${spaceId}/${parseInt(event.proposal_id)}`;
+    const metadataUri = longStringToText(event.metadata_uri);
+
+    const proposal = await Proposal.loadEntity(proposalId, config.indexerName);
+    if (!proposal) return;
 
     try {
-      await registerProposal({
-        l1TokenAddress,
-        strategyAddress: strategy,
-        snapshotTimestamp: proposal.snapshot
-      });
+      await handleProposalMetadata(metadataUri, config);
+
+      proposal.metadata = dropIpfs(metadataUri);
+      proposal.edited = block?.timestamp ?? getCurrentTimestamp();
     } catch (e) {
-      console.log('failed to register proposal');
+      console.log('failed to update proposal metadata', e);
     }
-  }
 
-  await Promise.all([proposal.save(), space.save()]);
-};
+    const executionStrategy = await handleExecutionStrategy(
+      event.execution_strategy,
+      event.payload,
+      config
+    );
+    if (executionStrategy) {
+      proposal.execution_strategy_type =
+        executionStrategy.executionStrategyType;
+      proposal.quorum = executionStrategy.quorum;
+    }
 
-export const handleCancel: starknet.Writer = async ({ rawEvent, event }) => {
-  if (!rawEvent || !event) return;
+    await proposal.save();
+  };
 
-  console.log('Handle cancel');
+  const handleExecute: starknet.Writer = async ({ tx, rawEvent, event }) => {
+    if (!rawEvent || !event) return;
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-  const proposalId = `${spaceId}/${parseInt(event.proposal_id)}`;
+    console.log('Handle execute');
 
-  const [proposal, space] = await Promise.all([
-    Proposal.loadEntity(proposalId),
-    Space.loadEntity(spaceId)
-  ]);
-  if (!proposal || !space) return;
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const proposalId = `${spaceId}/${parseInt(event.proposal_id)}`;
 
-  proposal.cancelled = true;
-  space.proposal_count -= 1;
-  space.vote_count -= proposal.vote_count;
+    const proposal = await Proposal.loadEntity(proposalId, config.indexerName);
+    if (!proposal) return;
 
-  await Promise.all([proposal.save(), space.save()]);
-};
+    proposal.executed = true;
+    proposal.completed = true;
+    proposal.execution_tx = tx.transaction_hash ?? null;
 
-export const handleUpdate: starknet.Writer = async ({
-  block,
-  rawEvent,
-  event
-}) => {
-  if (!rawEvent || !event) return;
-
-  console.log('Handle update');
-
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-  const proposalId = `${spaceId}/${parseInt(event.proposal_id)}`;
-  const metadataUri = longStringToText(event.metadata_uri);
-
-  const proposal = await Proposal.loadEntity(proposalId);
-  if (!proposal) return;
-
-  try {
-    await handleProposalMetadata(metadataUri);
-
-    proposal.metadata = dropIpfs(metadataUri);
-    proposal.edited = block?.timestamp ?? getCurrentTimestamp();
-  } catch (e) {
-    console.log('failed to update proposal metadata', e);
-  }
-
-  const executionStrategy = await handleExecutionStrategy(
-    event.execution_strategy,
-    event.payload
-  );
-  if (executionStrategy) {
-    proposal.execution_strategy_type = executionStrategy.executionStrategyType;
-    proposal.quorum = executionStrategy.quorum;
-  }
-
-  await proposal.save();
-};
+    await proposal.save();
+  };
 
-export const handleExecute: starknet.Writer = async ({
-  tx,
-  rawEvent,
-  event
-}) => {
-  if (!rawEvent || !event) return;
+  const handleVote: starknet.Writer = async ({
+    block,
+    tx,
+    rawEvent,
+    event
+  }) => {
+    if (!rawEvent || !event) return;
 
-  console.log('Handle execute');
+    console.log('Handle vote');
 
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-  const proposalId = `${spaceId}/${parseInt(event.proposal_id)}`;
+    const spaceId = validateAndParseAddress(rawEvent.from_address);
+    const proposalId = parseInt(event.proposal_id);
+    const choice = getVoteValue(findVariant(event.choice).key);
+    const vp = BigInt(event.voting_power);
 
-  const proposal = await Proposal.loadEntity(proposalId);
-  if (!proposal) return;
+    const created = block?.timestamp ?? getCurrentTimestamp();
+    const voter = formatAddressVariant(findVariant(event.voter));
 
-  proposal.executed = true;
-  proposal.completed = true;
-  proposal.execution_tx = tx.transaction_hash ?? null;
+    const vote = new Vote(
+      `${spaceId}/${proposalId}/${voter.address}`,
+      config.indexerName
+    );
+    vote.space = spaceId;
+    vote.proposal = proposalId;
+    vote.voter = voter.address;
+    vote.choice = choice;
+    vote.vp = vp.toString();
+    vote.created = created;
+    vote.tx = tx.transaction_hash;
 
-  await proposal.save();
-};
+    try {
+      const metadataUri = longStringToText(event.metadata_uri);
+      await handleVoteMetadata(metadataUri, config);
 
-export const handleVote: starknet.Writer = async ({
-  block,
-  tx,
-  rawEvent,
-  event
-}) => {
-  if (!rawEvent || !event) return;
-
-  console.log('Handle vote');
-
-  const spaceId = validateAndParseAddress(rawEvent.from_address);
-  const proposalId = parseInt(event.proposal_id);
-  const choice = getVoteValue(findVariant(event.choice).key);
-  const vp = BigInt(event.voting_power);
-
-  const created = block?.timestamp ?? getCurrentTimestamp();
-  const voter = formatAddressVariant(findVariant(event.voter));
-
-  const vote = new Vote(`${spaceId}/${proposalId}/${voter.address}`);
-  vote.space = spaceId;
-  vote.proposal = proposalId;
-  vote.voter = voter.address;
-  vote.choice = choice;
-  vote.vp = vp.toString();
-  vote.created = created;
-  vote.tx = tx.transaction_hash;
-
-  try {
-    const metadataUri = longStringToText(event.metadata_uri);
-    await handleVoteMetadata(metadataUri);
-
-    vote.metadata = dropIpfs(metadataUri);
-  } catch (e) {
-    console.log(JSON.stringify(e).slice(0, 256));
-  }
-
-  await vote.save();
-
-  const existingUser = await User.loadEntity(voter.address);
-  if (existingUser) {
-    existingUser.vote_count += 1;
-    await existingUser.save();
-  } else {
-    const user = new User(voter.address);
-    user.address_type = voter.type;
-    user.created = created;
-    await user.save();
-  }
-
-  let leaderboardItem = await Leaderboard.loadEntity(
-    `${spaceId}/${voter.address}`
-  );
-  if (!leaderboardItem) {
-    leaderboardItem = new Leaderboard(`${spaceId}/${voter.address}`);
-    leaderboardItem.space = spaceId;
-    leaderboardItem.user = voter.address;
-    leaderboardItem.vote_count = 0;
-    leaderboardItem.proposal_count = 0;
-  }
-
-  leaderboardItem.vote_count += 1;
-  await leaderboardItem.save();
-
-  const space = await Space.loadEntity(spaceId);
-  if (space) {
-    space.vote_count += 1;
-    if (leaderboardItem.vote_count === 1) space.voter_count += 1;
+      vote.metadata = dropIpfs(metadataUri);
+    } catch (e) {
+      console.log(JSON.stringify(e).slice(0, 256));
+    }
 
-    await space.save();
-  }
-
-  const proposal = await Proposal.loadEntity(`${spaceId}/${proposalId}`);
-  if (proposal) {
-    proposal.vote_count += 1;
-    proposal.scores_total = (
-      BigInt(proposal.scores_total) + BigInt(vote.vp)
-    ).toString();
-    proposal[`scores_${choice}`] = (
-      BigInt(proposal[`scores_${choice}`]) + BigInt(vote.vp)
-    ).toString();
-    await proposal.save();
-  }
-};
+    await vote.save();
+
+    const existingUser = await User.loadEntity(
+      voter.address,
+      config.indexerName
+    );
+    if (existingUser) {
+      existingUser.vote_count += 1;
+      await existingUser.save();
+    } else {
+      const user = new User(voter.address, config.indexerName);
+      user.address_type = voter.type;
+      user.created = created;
+      await user.save();
+    }
+
+    let leaderboardItem = await Leaderboard.loadEntity(
+      `${spaceId}/${voter.address}`,
+      config.indexerName
+    );
+    if (!leaderboardItem) {
+      leaderboardItem = new Leaderboard(
+        `${spaceId}/${voter.address}`,
+        config.indexerName
+      );
+      leaderboardItem.space = spaceId;
+      leaderboardItem.user = voter.address;
+      leaderboardItem.vote_count = 0;
+      leaderboardItem.proposal_count = 0;
+    }
+
+    leaderboardItem.vote_count += 1;
+    await leaderboardItem.save();
+
+    const space = await Space.loadEntity(spaceId, config.indexerName);
+    if (space) {
+      space.vote_count += 1;
+      if (leaderboardItem.vote_count === 1) space.voter_count += 1;
+
+      await space.save();
+    }
+
+    const proposal = await Proposal.loadEntity(
+      `${spaceId}/${proposalId}`,
+      config.indexerName
+    );
+    if (proposal) {
+      proposal.vote_count += 1;
+      proposal.scores_total = (
+        BigInt(proposal.scores_total) + BigInt(vote.vp)
+      ).toString();
+      proposal[`scores_${choice}`] = (
+        BigInt(proposal[`scores_${choice}`]) + BigInt(vote.vp)
+      ).toString();
+      await proposal.save();
+    }
+  };
+
+  return {
+    handleContractDeployed,
+    handleSpaceCreated,
+    handleMetadataUriUpdated,
+    handleMinVotingDurationUpdated,
+    handleMaxVotingDurationUpdated,
+    handleOwnershipTransferred,
+    handleVotingDelayUpdated,
+    handleAuthenticatorsAdded,
+    handleAuthenticatorsRemoved,
+    handleVotingStrategiesAdded,
+    handleVotingStrategiesRemoved,
+    handleProposalValidationStrategyUpdated,
+    handlePropose,
+    handleCancel,
+    handleUpdate,
+    handleExecute,
+    handleVote
+  };
+}

From bd07bdf6ee58dc3cd55470a1da739127457c2497 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= <wiktor.tkaczynski@gmail.com>
Date: Thu, 23 Jan 2025 14:33:21 +0100
Subject: [PATCH 2/6] refactor: move starknet specific code to directory

---
 apps/api/src/index.ts                             | 15 +++------------
 apps/api/src/{ => starknet}/abis/encoders.json    |  0
 .../{ => starknet}/abis/executionStrategy.json    |  0
 .../abis/l1/L1AvatarExectionStrategy.json         |  0
 .../abis/l1/SimpleQuorumExecutionStrategy.json    |  0
 apps/api/src/{ => starknet}/abis/space.json       |  0
 .../api/src/{ => starknet}/abis/spaceFactory.json |  0
 apps/api/src/{ => starknet}/config.ts             |  0
 apps/api/src/starknet/index.ts                    | 14 ++++++++++++++
 apps/api/src/{ => starknet}/ipfs.ts               |  2 +-
 apps/api/src/{ => starknet}/utils.ts              |  2 +-
 apps/api/src/{writer.ts => starknet/writers.ts}   |  2 +-
 12 files changed, 20 insertions(+), 15 deletions(-)
 rename apps/api/src/{ => starknet}/abis/encoders.json (100%)
 rename apps/api/src/{ => starknet}/abis/executionStrategy.json (100%)
 rename apps/api/src/{ => starknet}/abis/l1/L1AvatarExectionStrategy.json (100%)
 rename apps/api/src/{ => starknet}/abis/l1/SimpleQuorumExecutionStrategy.json (100%)
 rename apps/api/src/{ => starknet}/abis/space.json (100%)
 rename apps/api/src/{ => starknet}/abis/spaceFactory.json (100%)
 rename apps/api/src/{ => starknet}/config.ts (100%)
 create mode 100644 apps/api/src/starknet/index.ts
 rename apps/api/src/{ => starknet}/ipfs.ts (99%)
 rename apps/api/src/{ => starknet}/utils.ts (99%)
 rename apps/api/src/{writer.ts => starknet/writers.ts} (99%)

diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts
index eb84f5f05..aafda0e21 100644
--- a/apps/api/src/index.ts
+++ b/apps/api/src/index.ts
@@ -6,12 +6,10 @@ import { ApolloServerPluginLandingPageLocalDefault } from '@apollo/server/plugin
 import { startStandaloneServer } from '@apollo/server/standalone';
 import Checkpoint, {
   createGetLoader,
-  LogLevel,
-  starknet
+  LogLevel
 } from '@snapshot-labs/checkpoint';
-import { createConfig } from './config';
 import overrides from './overrides.json';
-import { createWriters } from './writer';
+import { addStarknetIndexers } from './starknet';
 
 const dir = __dirname.endsWith('dist/src') ? '../' : '';
 const schemaFile = path.join(__dirname, `${dir}../src/schema.gql`);
@@ -24,12 +22,6 @@ if (process.env.CA_CERT) {
   process.env.CA_CERT = process.env.CA_CERT.replace(/\\n/g, '\n');
 }
 
-const snConfig = createConfig('sn');
-const snSepConfig = createConfig('sn-sep');
-
-const snIndexer = new starknet.StarknetIndexer(createWriters(snConfig));
-const snSepIndexer = new starknet.StarknetIndexer(createWriters(snSepConfig));
-
 const checkpoint = new Checkpoint(schema, {
   logLevel: LogLevel.Info,
   resetOnConfigChange: true,
@@ -37,8 +29,7 @@ const checkpoint = new Checkpoint(schema, {
   overridesConfig: overrides
 });
 
-checkpoint.addIndexer(snConfig.indexerName, snConfig, snIndexer);
-checkpoint.addIndexer(snSepConfig.indexerName, snSepConfig, snSepIndexer);
+addStarknetIndexers(checkpoint);
 
 const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
 
diff --git a/apps/api/src/abis/encoders.json b/apps/api/src/starknet/abis/encoders.json
similarity index 100%
rename from apps/api/src/abis/encoders.json
rename to apps/api/src/starknet/abis/encoders.json
diff --git a/apps/api/src/abis/executionStrategy.json b/apps/api/src/starknet/abis/executionStrategy.json
similarity index 100%
rename from apps/api/src/abis/executionStrategy.json
rename to apps/api/src/starknet/abis/executionStrategy.json
diff --git a/apps/api/src/abis/l1/L1AvatarExectionStrategy.json b/apps/api/src/starknet/abis/l1/L1AvatarExectionStrategy.json
similarity index 100%
rename from apps/api/src/abis/l1/L1AvatarExectionStrategy.json
rename to apps/api/src/starknet/abis/l1/L1AvatarExectionStrategy.json
diff --git a/apps/api/src/abis/l1/SimpleQuorumExecutionStrategy.json b/apps/api/src/starknet/abis/l1/SimpleQuorumExecutionStrategy.json
similarity index 100%
rename from apps/api/src/abis/l1/SimpleQuorumExecutionStrategy.json
rename to apps/api/src/starknet/abis/l1/SimpleQuorumExecutionStrategy.json
diff --git a/apps/api/src/abis/space.json b/apps/api/src/starknet/abis/space.json
similarity index 100%
rename from apps/api/src/abis/space.json
rename to apps/api/src/starknet/abis/space.json
diff --git a/apps/api/src/abis/spaceFactory.json b/apps/api/src/starknet/abis/spaceFactory.json
similarity index 100%
rename from apps/api/src/abis/spaceFactory.json
rename to apps/api/src/starknet/abis/spaceFactory.json
diff --git a/apps/api/src/config.ts b/apps/api/src/starknet/config.ts
similarity index 100%
rename from apps/api/src/config.ts
rename to apps/api/src/starknet/config.ts
diff --git a/apps/api/src/starknet/index.ts b/apps/api/src/starknet/index.ts
new file mode 100644
index 000000000..57c66bfc1
--- /dev/null
+++ b/apps/api/src/starknet/index.ts
@@ -0,0 +1,14 @@
+import Checkpoint, { starknet } from '@snapshot-labs/checkpoint';
+import { createConfig } from './config';
+import { createWriters } from './writers';
+
+const snConfig = createConfig('sn');
+const snSepConfig = createConfig('sn-sep');
+
+const snIndexer = new starknet.StarknetIndexer(createWriters(snConfig));
+const snSepIndexer = new starknet.StarknetIndexer(createWriters(snSepConfig));
+
+export function addStarknetIndexers(checkpoint: Checkpoint) {
+  checkpoint.addIndexer(snConfig.indexerName, snConfig, snIndexer);
+  checkpoint.addIndexer(snSepConfig.indexerName, snSepConfig, snSepIndexer);
+}
diff --git a/apps/api/src/ipfs.ts b/apps/api/src/starknet/ipfs.ts
similarity index 99%
rename from apps/api/src/ipfs.ts
rename to apps/api/src/starknet/ipfs.ts
index 5d3bcaf27..9486c9f4f 100644
--- a/apps/api/src/ipfs.ts
+++ b/apps/api/src/starknet/ipfs.ts
@@ -11,7 +11,7 @@ import {
   SpaceMetadataItem,
   StrategiesParsedMetadataDataItem,
   VoteMetadataItem
-} from '../.checkpoint/models';
+} from '../../.checkpoint/models';
 
 export async function handleSpaceMetadata(
   space: string,
diff --git a/apps/api/src/utils.ts b/apps/api/src/starknet/utils.ts
similarity index 99%
rename from apps/api/src/utils.ts
rename to apps/api/src/starknet/utils.ts
index 5714b98e4..91372d982 100644
--- a/apps/api/src/utils.ts
+++ b/apps/api/src/starknet/utils.ts
@@ -23,7 +23,7 @@ import {
   Space,
   StrategiesParsedMetadataItem,
   VotingPowerValidationStrategiesParsedMetadataItem
-} from '../.checkpoint/models';
+} from '../../.checkpoint/models';
 
 type StrategyConfig = {
   address: BigNumberish;
diff --git a/apps/api/src/writer.ts b/apps/api/src/starknet/writers.ts
similarity index 99%
rename from apps/api/src/writer.ts
rename to apps/api/src/starknet/writers.ts
index dd11456b6..82bad500d 100644
--- a/apps/api/src/writer.ts
+++ b/apps/api/src/starknet/writers.ts
@@ -24,7 +24,7 @@ import {
   Space,
   User,
   Vote
-} from '../.checkpoint/models';
+} from '../../.checkpoint/models';
 
 type Strategy = {
   address: string;

From e6d4ce78f40fa83caf4774146c946d097f3bb41c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= <wiktor.tkaczynski@gmail.com>
Date: Mon, 27 Jan 2025 16:30:33 +0100
Subject: [PATCH 3/6] chore: bump @checkpoint-labs/checkpoint

---
 apps/api/package.json |  2 +-
 yarn.lock             | 57 ++++++++++++++++---------------------------
 2 files changed, 22 insertions(+), 37 deletions(-)

diff --git a/apps/api/package.json b/apps/api/package.json
index 62e78ef42..3979a75c1 100644
--- a/apps/api/package.json
+++ b/apps/api/package.json
@@ -23,7 +23,7 @@
     "@ethersproject/providers": "^5.7.2",
     "@ethersproject/units": "^5.6.1",
     "@faker-js/faker": "^7.4.0",
-    "@snapshot-labs/checkpoint": "^0.1.0-beta.44",
+    "@snapshot-labs/checkpoint": "^0.1.0-beta.46",
     "@snapshot-labs/sx": "^0.1.0",
     "@types/bn.js": "^5.1.0",
     "@types/mysql": "^2.15.21",
diff --git a/yarn.lock b/yarn.lock
index 77536093a..bf4a149f4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2420,6 +2420,14 @@
     "@noble/hashes" "~1.3.2"
     "@scure/base" "~1.1.4"
 
+"@scure/starknet@~0.3.0":
+  version "0.3.0"
+  resolved "https://registry.yarnpkg.com/@scure/starknet/-/starknet-0.3.0.tgz#b8273a42fc721025f8098b1f1d96368a7067e1c4"
+  integrity sha512-Ma66yZlwa5z00qI5alSxdWtIpky5LBhy22acVFdoC5kwwbd9uDyMWEYzWHdNyKmQg9t5Y2UOXzINMeb3yez+Gw==
+  dependencies:
+    "@noble/curves" "~1.2.0"
+    "@noble/hashes" "~1.3.2"
+
 "@scure/starknet@~1.0.0":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@scure/starknet/-/starknet-1.0.0.tgz#4419bc2fdf70f3dd6cb461d36c878c9ef4419f8c"
@@ -2445,10 +2453,10 @@
   resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f"
   integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==
 
-"@snapshot-labs/checkpoint@^0.1.0-beta.44":
-  version "0.1.0-beta.44"
-  resolved "https://registry.yarnpkg.com/@snapshot-labs/checkpoint/-/checkpoint-0.1.0-beta.44.tgz#61bac9957fcaf1fcdbd819a99deba02bdc7705f8"
-  integrity sha512-SE4p0TLAwb28phlfKp02obBUm4wlAwpf+gxuv40t1ei3vuYGeBHeGhoOsvCIKaUDXqyUbicdwQ2BE2WDEU2HIQ==
+"@snapshot-labs/checkpoint@^0.1.0-beta.46":
+  version "0.1.0-beta.46"
+  resolved "https://registry.yarnpkg.com/@snapshot-labs/checkpoint/-/checkpoint-0.1.0-beta.46.tgz#7a68ed67cb8f8952e2000b45f03d643c28303c8f"
+  integrity sha512-4u5G6+KZJFiUJXwtgrcgSz8++/KxapNMd/EkFi4HPynq4k5dgbQQrOvfmC4E/QcOeFpqup6cAi9egCiMcnpq1A==
   dependencies:
     "@ethersproject/abi" "^5.7.0"
     "@ethersproject/address" "^5.7.0"
@@ -2469,7 +2477,7 @@
     pino "^8.3.1"
     pino-pretty "^8.1.0"
     pluralize "^8.0.0"
-    starknet "^5.19.3"
+    starknet "~5.19.3"
     yargs "^17.7.2"
     zod "^3.21.4"
 
@@ -4013,28 +4021,7 @@ abbrev@^2.0.0:
   resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf"
   integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==
 
-"abi-wan-kanabi-v1@npm:abi-wan-kanabi@^1.0.3":
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/abi-wan-kanabi/-/abi-wan-kanabi-1.0.3.tgz#0d8607f2a2ccb2151a69debea1c3bb68b76c5aa2"
-  integrity sha512-Xwva0AnhXx/IVlzo3/kwkI7Oa7ZX7codtcSn+Gmoa2PmjGPF/0jeVud9puasIPtB7V50+uBdUj4Mh3iATqtBvg==
-  dependencies:
-    abi-wan-kanabi "^1.0.1"
-    fs-extra "^10.0.0"
-    rome "^12.1.3"
-    typescript "^4.9.5"
-    yargs "^17.7.2"
-
-"abi-wan-kanabi-v2@npm:abi-wan-kanabi@^2.1.1":
-  version "2.2.3"
-  resolved "https://registry.yarnpkg.com/abi-wan-kanabi/-/abi-wan-kanabi-2.2.3.tgz#d1c410325aac866f31f3d589279a87b341e5641f"
-  integrity sha512-JlqiAl9CPvTm5kKG0QXmVCWNWoC/XyRMOeT77cQlbxXWllgjf6SqUmaNqFon72C2o5OSZids+5FvLdsw6dvWaw==
-  dependencies:
-    ansicolors "^0.3.2"
-    cardinal "^2.1.1"
-    fs-extra "^10.0.0"
-    yargs "^17.7.2"
-
-abi-wan-kanabi@^1.0.1:
+abi-wan-kanabi@^1.0.1, abi-wan-kanabi@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/abi-wan-kanabi/-/abi-wan-kanabi-1.0.3.tgz#0d8607f2a2ccb2151a69debea1c3bb68b76c5aa2"
   integrity sha512-Xwva0AnhXx/IVlzo3/kwkI7Oa7ZX7codtcSn+Gmoa2PmjGPF/0jeVud9puasIPtB7V50+uBdUj4Mh3iATqtBvg==
@@ -12150,16 +12137,14 @@ starknet@6.11.0:
     ts-mixer "^6.0.3"
     url-join "^4.0.1"
 
-starknet@^5.19.3:
-  version "5.29.0"
-  resolved "https://registry.yarnpkg.com/starknet/-/starknet-5.29.0.tgz#26d8074340f26a2da4bc373169a1a5ed6dc9bedf"
-  integrity sha512-eEcd6uiYIwGvl8MtHOsXGBhREqjJk84M/qUkvPLQ3n/JAMkbKBGnygDlh+HAsvXJsGlMQfwrcVlm6KpDoPha7w==
+starknet@^5.19.3, starknet@~5.19.3:
+  version "5.19.6"
+  resolved "https://registry.yarnpkg.com/starknet/-/starknet-5.19.6.tgz#61e43e083888ef64598473bc2e4bd3e1e07fe9d4"
+  integrity sha512-MyO48QKu+d8CBH/7ruI/RPrDwoFRNV/uxql2nBeA4ccqBXs698FkeGkMhZv++VIwf1MRajHcTM91KuyZQMyMLw==
   dependencies:
-    "@noble/curves" "~1.3.0"
-    "@scure/base" "~1.1.3"
-    "@scure/starknet" "~1.0.0"
-    abi-wan-kanabi-v1 "npm:abi-wan-kanabi@^1.0.3"
-    abi-wan-kanabi-v2 "npm:abi-wan-kanabi@^2.1.1"
+    "@noble/curves" "~1.2.0"
+    "@scure/starknet" "~0.3.0"
+    abi-wan-kanabi "^1.0.3"
     isomorphic-fetch "^3.0.0"
     lossless-json "^2.0.8"
     pako "^2.0.4"

From 8348211d4d1f5c65c3e50afbabbf521f3b207bcd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= <wiktor.tkaczynski@gmail.com>
Date: Mon, 27 Jan 2025 16:30:53 +0100
Subject: [PATCH 4/6] feat: use env variables for networkNodeUrls

You can use either Infura with single variable (INFURA_API_KEY)
or specify full URL for certain network:
 - NETWORK_NODE_URL_SN
 - NETWORK_NODE_URL_SN_SEP
---
 apps/api/src/starknet/config.ts | 19 +++++++++++++------
 1 file changed, 13 insertions(+), 6 deletions(-)

diff --git a/apps/api/src/starknet/config.ts b/apps/api/src/starknet/config.ts
index cc3622cdc..760e456de 100644
--- a/apps/api/src/starknet/config.ts
+++ b/apps/api/src/starknet/config.ts
@@ -4,6 +4,17 @@ import { validateAndParseAddress } from 'starknet';
 import spaceAbi from './abis/space.json';
 import spaceFactoryAbi from './abis/spaceFactory.json';
 
+const DEFAULT_INFURA_API_KEY =
+  process.env.INFURA_API_KEY || '46a5dd9727bf48d4a132672d3f376146';
+
+const snNetworkNodeUrl =
+  process.env.NETWORK_NODE_URL_SN ||
+  `https://starknet-mainnet.infura.io/v3/${DEFAULT_INFURA_API_KEY}`;
+const snSepNetworkNodeUrl =
+  process.env.NETWORK_NODE_URL_SN_SEP ||
+  `https://starknet-sepolia.infura.io/v3/${DEFAULT_INFURA_API_KEY}`;
+const manaRpcUrl = process.env.VITE_MANA_URL || 'https://mana.box';
+
 export type FullConfig = {
   indexerName: 'sn' | 'sn-sep';
   overrides: ReturnType<typeof createOverrides>;
@@ -12,8 +23,7 @@ export type FullConfig = {
 const CONFIG = {
   sn: {
     indexerName: 'sn',
-    networkNodeUrl:
-      'https://starknet-mainnet.infura.io/v3/46a5dd9727bf48d4a132672d3f376146',
+    networkNodeUrl: snNetworkNodeUrl,
     l1NetworkNodeUrl: 'https://rpc.brovider.xyz/1',
     contract: starknetNetworks['sn'].Meta.spaceFactory,
     start: 445498,
@@ -26,8 +36,7 @@ const CONFIG = {
   },
   'sn-sep': {
     indexerName: 'sn-sep',
-    networkNodeUrl:
-      'https://starknet-sepolia.infura.io/v3/46a5dd9727bf48d4a132672d3f376146',
+    networkNodeUrl: snSepNetworkNodeUrl,
     l1NetworkNodeUrl: 'https://rpc.brovider.xyz/11155111',
     contract: starknetNetworks['sn-sep'].Meta.spaceFactory,
     start: 17960,
@@ -37,8 +46,6 @@ const CONFIG = {
   }
 };
 
-const manaRpcUrl = process.env.VITE_MANA_URL || 'https://mana.box';
-
 function createOverrides(networkId: keyof typeof CONFIG) {
   const config = starknetNetworks[networkId];
 

From 5c33a496884c0a56d6811c3abe99a77704a5a002 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= <wiktor.tkaczynski@gmail.com>
Date: Mon, 27 Jan 2025 16:49:22 +0100
Subject: [PATCH 5/6] ci: disable api deployments

---
 .github/workflows/deploy.yml | 73 ++++++++++++++++++------------------
 1 file changed, 37 insertions(+), 36 deletions(-)

diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index f1457d42c..8204736ce 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -6,43 +6,44 @@ on:
       - master
 
 jobs:
-  deploy_api_mainnet:
-    name: Deploy API to mainnet
-    environment: api_mainnet
-    concurrency:
-      group: api_mainnet
-      cancel-in-progress: true
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout Repo
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 2
-      - name: Install doctl
-        uses: digitalocean/action-doctl@v2
-        with:
-          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
-      - name: Deploy if files have changed
-        run: ./scripts/deploy-api-prod.sh
+  # Those are disabled as part of transition to unified API
+  # deploy_api_mainnet:
+  #   name: Deploy API to mainnet
+  #   environment: api_mainnet
+  #   concurrency:
+  #     group: api_mainnet
+  #     cancel-in-progress: true
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - name: Checkout Repo
+  #       uses: actions/checkout@v3
+  #       with:
+  #         fetch-depth: 2
+  #     - name: Install doctl
+  #       uses: digitalocean/action-doctl@v2
+  #       with:
+  #         token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
+  #     - name: Deploy if files have changed
+  #       run: ./scripts/deploy-api-prod.sh
 
-  deploy_api_testnet:
-    name: Deploy API to testnet
-    environment: api_testnet
-    concurrency:
-      group: api_testnet
-      cancel-in-progress: true
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout Repo
-        uses: actions/checkout@v3
-        with:
-          fetch-depth: 2
-      - name: Install doctl
-        uses: digitalocean/action-doctl@v2
-        with:
-          token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
-      - name: Deploy if files have changed
-        run: ./scripts/deploy-changed.sh apps/api ${{ vars.DIGITALOCEAN_APP_ID }}
+  # deploy_api_testnet:
+  #   name: Deploy API to testnet
+  #   environment: api_testnet
+  #   concurrency:
+  #     group: api_testnet
+  #     cancel-in-progress: true
+  #   runs-on: ubuntu-latest
+  #   steps:
+  #     - name: Checkout Repo
+  #       uses: actions/checkout@v3
+  #       with:
+  #         fetch-depth: 2
+  #     - name: Install doctl
+  #       uses: digitalocean/action-doctl@v2
+  #       with:
+  #         token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
+  #     - name: Deploy if files have changed
+  #       run: ./scripts/deploy-changed.sh apps/api ${{ vars.DIGITALOCEAN_APP_ID }}
 
   deploy_mana_prod:
     name: Deploy mana to production

From 1115aac8754ce72691d4ea437dc30e0555d8d5e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Wiktor=20Tkaczy=C5=84ski?= <wiktor.tkaczynski@gmail.com>
Date: Mon, 27 Jan 2025 16:59:59 +0100
Subject: [PATCH 6/6] test: move starknet utils tests

---
 apps/api/test/unit/{ => starknet}/utils.test.ts | 2 +-
 apps/api/test/unit/writer.test.ts               | 7 -------
 2 files changed, 1 insertion(+), 8 deletions(-)
 rename apps/api/test/unit/{ => starknet}/utils.test.ts (95%)
 delete mode 100644 apps/api/test/unit/writer.test.ts

diff --git a/apps/api/test/unit/utils.test.ts b/apps/api/test/unit/starknet/utils.test.ts
similarity index 95%
rename from apps/api/test/unit/utils.test.ts
rename to apps/api/test/unit/starknet/utils.test.ts
index 963e8bf32..90763b64e 100644
--- a/apps/api/test/unit/utils.test.ts
+++ b/apps/api/test/unit/starknet/utils.test.ts
@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest';
-import { formatAddressVariant } from '../../src/utils';
+import { formatAddressVariant } from '../../../src/starknet/utils';
 
 describe('formatAddressVariant', () => {
   it.each([
diff --git a/apps/api/test/unit/writer.test.ts b/apps/api/test/unit/writer.test.ts
deleted file mode 100644
index 0f0924a01..000000000
--- a/apps/api/test/unit/writer.test.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { describe, expect, it } from 'vitest';
-
-describe('smoke test', () => {
-  it('should pass', () => {
-    expect(true).toBe(true);
-  });
-});