Skip to content

Instantly share code, notes, and snippets.

@ammarhaiderak
Last active January 28, 2025 23:15
Show Gist options
  • Save ammarhaiderak/c007926a8e10925efec6d1232fe3ab01 to your computer and use it in GitHub Desktop.
Save ammarhaiderak/c007926a8e10925efec6d1232fe3ab01 to your computer and use it in GitHub Desktop.
Deploying Self-Hosted Gnosis Safe Wallet for Custom Blockchain

Gnosis Safe Clone (custom blockchain)

Transaction Service

  • create env file from .env.l2.sample

    cp .env.l2.sample .env
  • replace ETHEREUM_NODE_URL

  • create secure admin

    docker exec -it safe-transaction-service-web-1 python [manage.py](http://manage.py/) createsuperuser
  • login at http://localhost:8000/admin

  • add custom Proxy Factories and Master Copies (L2)

  • click the Add link for Web hooks

  • Ignore the Address field

  • Set the Url field to http://your-config-gateway-url/cgw/v1/chains/{chainId}/hooks/events and replace {chainId} with the corresponding chainId

  • Set the Authorization field to Basic <AUTH_TOKEN>, where <AUTH_TOKEN> corresponds to the value of AUTH_TOKEN in the container_env_files/cgw.env file of safe-infrastructure repository

  • Optionally: Add the Safe Master Copy of your network at http://localhost:8000/admin/history/safemastercopy, where version corresponds to the version of your deployed Safe Master Copy. Check L2 if it concerns the Safe L2 version. This is required for chains that were not added to safe-eth-py

Gateway Service and Config Service

  • CGW_FLUSH_TOKEN inside container_env_files/cfg.env and AUTH_TOKEN inside container_env_files/cgw.env should be same, and set them to a secure one

  • Set RPC_NODE_URL inside .env to your blockchain node url, preferable to have in https

    cp .env.sample .env
  • create secure admin

    docker compose up
    docker compose exec cfg-web python src/manage.py createsuperuser --noinput
  • login at http://localhost:8000/cfg/admin

  • add chainInfo, You can do this in the admin interface of the Safe Config service: http://localhost:8000/cfg/admin/chains/chain/add/

  • Remember to edit your ChainInfo json fields transaction_service_uri and vpc_transaction_service_uri to point to your local instance of the transaction service. The values should be http://your-transaction-service-url

Proxy Service

since we have the frontend separate we’d need to proxy the requests to avoid cors erros to secure it we can update the origin to only the frontend url

const http = require('http');
const httpProxy = require('http-proxy');
const cors = require('cors');

const corsOptions = {
  origin: '*', // Adjust this to allow requests from specific origins
  methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
  optionsSuccessStatus: 204,
};

const targetUrl = 'http://localhost:8000';  // The local service URL
const proxy = httpProxy.createProxyServer({});

const server = http.createServer((req, res) => {
  // Log the incoming request if needed
  console.log(`Proxying request: ${req.method} ${req.url}`);
  
  req.headers['origin'] = 'http://localhost:8000';
  req.headers['referer'] = 'http://localhost:8000/';
  // Proxy the request to the local service
  proxy.web(req, res, { target: targetUrl });
  res.setHeader('Access-Control-Allow-Origin', corsOptions.origin);
  res.setHeader('Access-Control-Allow-Methods', corsOptions.methods);
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');

  // Handle preflight request
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }
});

// Use the cors middleware
// server.use(cors(corsOptions));

const PORT = 8383;  // Port for your proxy server to listen on
server.listen(PORT, () => {
  console.log(`Proxy server is listening on port ${PORT}`);
});

Nginx conf

# https://github.com/KyleAMathews/docker-nginx/blob/master/nginx.conf
# https://linode.com/docs/web-servers/nginx/configure-nginx-for-optimized-performance/
# https://github.com/denji/nginx-tuning
worker_processes auto;

events {
  worker_connections 10000;  # increase if you have lots of clients
  # accept_mutex on;  # set to 'on' if nginx worker_processes > 1
  use epoll;  # to enable for Linux 2.6+
}

http {
  include mime.types;
  # fallback in case we can't determine a type
  default_type application/octet-stream;
  sendfile on;

  upstream app_server {
    server 127.0.0.1:8383 fail_timeout=0;
    keepalive 32;
  }
  
  upstream app {
    server 127.0.0.1:8080 fail_timeout=0;
    keepalive 32;
  }

  server {
    access_log off;
    charset utf-8;
    
    server_name api.wagmilabz.com;
    keepalive_timeout 75s;
    keepalive_requests 100000;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    gzip             on;
    gzip_min_length 10000;
    gzip_comp_level  6;

    # text/html is always included by default
    gzip_types text/plain text/css application/json application/javascript application/x-javascript text/javascript text/xml application/xml application/rss+xml application/atom+xml application/rdf+xml;
    gzip_disable "MSIE [1-6]\.";

    # allow the server to close connection on non responding client, this will free up memory
    reset_timedout_connection on;

    # Redirect http to https
   if ($http_x_forwarded_proto = 'http') {
     return 301 https://$host$request_uri;
   }

   location / {
          proxy_pass http://app/; 
          proxy_set_header Host $host;
          proxy_set_header X-Forwarded-Host $server_name;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header        X-Forwarded-Proto $http_x_forwarded_proto;
          add_header              Front-End-Https   on;
          # we don't want nginx trying to do something clever with
          # redirects, we set the Host: header above already.
          proxy_redirect off;
          # They default to 60s. Increase to avoid WORKER TIMEOUT in web container
          proxy_connect_timeout 60s;
          proxy_read_timeout 60s;
   }

   location /cgw/ {
          proxy_pass http://app_server/cgw/;
          proxy_set_header Host $host;
          proxy_set_header X-Forwarded-Host $server_name;
          proxy_set_header X-Real-IP $remote_addr;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          proxy_set_header        X-Forwarded-Proto $http_x_forwarded_proto;
          add_header              Front-End-Https   on;
          # we don't want nginx trying to do something clever with
          # redirects, we set the Host: header above already.
          proxy_redirect off;
          # They default to 60s. Increase to avoid WORKER TIMEOUT in web container
          proxy_connect_timeout 60s;
          proxy_read_timeout 60s;
   }
  
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/api.wagmilabz.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/api.wagmilabz.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

  server {
    if ($host = api.wagmilabz.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    
    server_name api.wagmilabz.com;
    keepalive_timeout 75s;
    return 404; # managed by Certbot

}}

Frontend (safe-wallet-web):

  • update src/hooks/coreSDK/safeCoreSDK.ts like this

    export const initSafeSDK = async ({
      provider,
      chainId,
      address,
      version,
      implementationVersionState,
      implementation,
    }: SafeCoreSDKProps): Promise<Safe | undefined> => {
      const safeVersion = version ?? (await Gnosis_safe__factory.connect(address, provider).VERSION())
    
      let isL1SafeMasterCopy = false
      // chainId === chains.eth
    
      // If it is an official deployment we should still initiate the safeSDK
      // if (!isValidMasterCopy(implementationVersionState)) {
      //   const masterCopy = implementation
    
      //   const safeL1Deployment = getSafeSingletonDeployment({ network: chainId, version: safeVersion })
      //   const safeL2Deployment = getSafeL2SingletonDeployment({ network: chainId, version: safeVersion })
    
      //   isL1SafeMasterCopy = masterCopy === safeL1Deployment?.networkAddresses[chainId]
      //   const isL2SafeMasterCopy = masterCopy === safeL2Deployment?.networkAddresses[chainId]
    
      //   // Unknown deployment, which we do not want to support
      //   if (!isL1SafeMasterCopy && !isL2SafeMasterCopy) {
      //     return Promise.resolve(undefined)
      //   }
      // }
    
      // Legacy Safe contracts
      if (isLegacyVersion(safeVersion)) {
        isL1SafeMasterCopy = true
      }
    
      const contractNetworks: ContractNetworksConfig = {
        '1998': {
          // safeMasterCopyAddress: '0xc6d15fC421c1EabF496D71aDd33A44d826191BC8',
          safeMasterCopyAddress: '0x4585475D8d683Fdc01b029A9B39c1eAe5edA2038',
          safeProxyFactoryAddress: '0xe3Bfe3823CC975E7976D14e7aA38f8D4b40E9126',
          multiSendAddress: '0x2DB50Ca9819Dc9181767A3541783edf3C3Cd4a64',
          multiSendCallOnlyAddress: '0x09A5a915b4fF1EB27E33FA43612735E5aBe0872E',
          fallbackHandlerAddress: '0x86dBab4Aab8f10cc22BDAb42322CBf2f9098480E',
          signMessageLibAddress: '0x9c362B55510BdE08D525327149BEeB837d774268',
          createCallAddress: '0xf87B12365B1fEa9B5CFe5Ceebd2e07A9f1dd4FA9',
        },
      }
    
      return Safe.create({
        ethAdapter: createReadOnlyEthersAdapter(provider),
        safeAddress: address,
        isL1SafeMasterCopy,
        contractNetworks,
      })
    }
  • similarly in src/services/contracts/safeContracts.ts add customContractAddress for

    • getReadOnlyGnosisSafeContract → ethAdapter.getSafeContract
    • getMultiSendCallOnlyContract → ethAdapter.getMultiSendCallOnlyContract
    • getReadOnlyMultiSendCallOnlyContract → ethAdapter.getMultiSendCallOnlyContract
    • getReadOnlyProxyFactoryContract → ethAdapter.getSafeProxyFactoryContract
    • getReadOnlyFallbackHandlerContract → ethAdapter.getCompatibilityFallbackHandlerContract
    • getReadOnlySignMessageLibContract → ethAdapter.getSignMessageLibContract

Versions

transaction-service: 4.25.1

config-service: 2.63.1

config-gateway-service: 1.4.0

safe-wallet-web (frontend): 1.18.0

note: correct versions should be set in order for application to work properly, setup assumes L2 version is used so use Safe Master L2 in order for indexing to work properly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment