Fleek Network

使用官方一键脚本搭建

域名注册,自行注册,最便宜的仅需几块钱一年

域名A记录解析到服务器外网IP

[crypto-block]

#!/bin/bash

# <!-- IGNORE: This line is intentional DO NOT MODIFY --><pre><script>document.querySelector('body').firstChild.textContent = '#!/bin/bash'</script>

# "Get Fleek Network" is an attempt to make our software more accessible.
# By providing scripts to automate the installation process of our software,
# we believe that it can help improve the onboarding experience of our users.
#
# Quick install: `curl https://get.fleek.network | bash`
#
# This script automates the process illustrated in our guide "Running a Node in a Docker container"
# advanced users might find it better to follow the instructions in the guide
# If that's your preference, go ahead and check our guides https://docs.fleek.network
#
# For the users happy to have the script assist in the installation process of Fleek Network
# and the required dependencies, run the script at your own risk. Part of the project will
# verify if specific dependencies are installed or needed, but it won't try to customise or
# take into consideration your custom environment. If you have a custom environment, then
# is best to follow the instructions in our guide, as otherwise risk changing
# or overriding your custom setup.
#
# Ideally, the script will:
# - Check if the system has enough disk space and memory, otherwise warn the user
# - Verify if the user is in Docker, as Docker in Docker is not supported
# - Verify if Git is installed, if not install it
# - It'll do a quick health check to confirm Git is installed correctly 
# - Verify if Docker is installed, if not install it
# - It'll do a quick health check to confirm Docker is installed correctly
# - Request a pathname where to store the Ursa repository, otherwise providing a default,
#   e.g., `$HOME/fleek-network/ursa`
# - Pull the `ursa` project repository to the preferred target directory via HTTPS
#   instead of SSH for simplicity
# - Optionally, assist in setting up and securing domain name via SSL/TLS
# - Run the Docker stack
# - Do a health check to confirm the Fleek Network Node is running
#
# Contributing?
# - If you'd like to test changes locally use the env var `USE_BRANCH_NAME_FOR_GH_RAW`, for remote locales pulls
#
# Found an issue? Please report it here: https://github.com/fleek-network/get.fleek.network

# 🚑 Check if running in Bash and supported version
[ "$BASH" ] || { printf >&2 '🙏 Run the script with Bash, please!\n'; exit 1; }
(( BASH_VERSINFO[0] > 4 || BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 2 )) || { printf >&2 '🙏 Bash 4.2 or newer is required!\n'; exit 1; }

# Date
dateRuntime=$(date '+%Y%m%d%H%M%S')

# Default
defaultGetFleekNetworkUrl="https://get.fleek.network"
defaultFleekNetworkWebsite="https://fleek.network"
defaultFleekNetworkDocsWebsite="https://docs.fleek.network"
defaultDiscordUrl="https://discord.gg/fleekxyz"
defaultFleekNetworkTwitterUrl="https://twitter.com/fleek_net"
defaultGetFleekNetworkRepository="https://github.com/fleek-network/get.fleek.network"
defaultUrsaHttpsRepository="https://github.com/fleek-network/ursa.git"
defaultUrsaPath="$HOME/fleek-network/ursa"
defaultUrsaLatestImage="ghcr.io/fleek-network/ursa:latest"
defaultDockerFullNodeRelativePath="./docker/full-node"
defaultDockerComposeYmlRelativePath="$defaultDockerFullNodeRelativePath/docker-compose.yml"
defaultMinMemoryBytesRequired=8000000
defaultMinDiskSpaceBytesRequired=10000000

# App state
userSelectedLanguage=""
selectedUrsaPath=""

# Localization
declare -a supportedLanguages=("en" "ch" "es" "pt" "fr" "it" "vi" "jp" "ko" "de" "hi" "ru")

for lang in "${supportedLanguages[@]}"; do
  # use of ?dateRuntime intentional, prevent cache, do not modify
  # INFO: set `USE_BRANCH_NAME_FOR_GH_RAW` to use the Github raw locales for testing
  # e.g., USE_BRANCH_NAME_FOR_GH_RAW="feat/localization" ./install
  if [[ -n ${USE_BRANCH_NAME_FOR_GH_RAW+x} ]]; then
    . <(curl -s "https://raw.githubusercontent.com/fleek-network/get.fleek.network/$USE_BRANCH_NAME_FOR_GH_RAW/locales/$lang?$dateRuntime")

    continue;
  fi

  . <(curl -s "$defaultGetFleekNetworkUrl/locales/$lang?$dateRuntime")
done

# App text translations
declare -A transl=()

setLang() {
  declare -n _selectedLang="$1"
  for key in "${!_selectedLang[@]}"; do
    transl["$key"]=${_selectedLang[$key]}
  done
  
  userSelectedLanguage="$1"
}

multiLanguageChoice() {
  echo ""
  printf "\n\n%s\n\n" "👋 Hello / 你好 / Hola / Olá / Bonjour / Ciao / Xin chào / こんにちは / 안녕하세요 / Hallo / नमस्ते / Привет"
  printf "%s\n\n" "🙏 Select your preferred language from the list / 从列表中选择您的首选语言 / Seleccione su idioma preferido de la lista / Selecione seu idioma preferido na lista / Sélectionnez votre langue préférée dans la liste / Seleziona la tua lingua preferita dall'elenco / Chọn ngôn ngữ ưa thích của bạn từ danh sách / リストからご希望の言語を選択してください / 목록에서 선호하는 언어를 선택하십시오 / Wählen Sie Ihre bevorzugte Sprache aus der Liste aus / सूची से अपनी पसंदीदा भाषा का चयन करें / Выберите предпочитаемый язык из списка"
  printf "%s\n" " 1) 🇬🇧  English"
  printf "%s\n" " 2) 🇨🇳  中文"
  printf "%s\n" " 3) 🇪🇸  Español"
  printf "%s\n" " 4) 🇵🇹  Português"
  printf "%s\n" " 5) 🇫🇷  Français"
  printf "%s\n" " 6) 🇮🇹  Italiano"
  printf "%s\n" " 7) 🇻🇳  Tiếng Việt"
  printf "%s\n" " 8) 🇯🇵  日本語"
  printf "%s\n" " 9) 🇰🇷  안녕하세요"
  printf "%s\n" "10) 🇩🇪  Deutsch"
  printf "%s\n" "11) 🇮🇳  हिंदी"
  printf "%s\n" "12) 🇷🇺  Pусский"
  echo ""

  while read -r -p "> " ans; do  
    if [[ $ans == [eE]nglish || $ans == 1 ]]; then
      opt="en"
    elif [[ $ans =~ 中文 || $ans == 2 ]]; then
      opt="ch"
    elif [[ $ans == [eE]spañol || $ans == 3 ]]; then
      opt="es"
    elif [[ $ans == [pP]ortuguês || $ans == 4 ]]; then
      opt="pt"
    elif [[ $ans == [fF]rançais || $ans == 5 ]]; then
      opt="fr"
    elif [[ $ans == [iI]taliano || $ans == 6 ]]; then
      opt="it"
    elif [[ $ans =~ [tT]iếng || $ans == 7 ]]; then
      opt="vi"
    elif [[ $ans =~ 日本語 || $ans == 8 ]]; then
      opt="jp"
    elif [[ $ans =~ 안녕하세요 || $ans == 9 ]]; then
      opt="ko"
    elif [[ $ans =~ [dD]eutsch || $ans == 10 ]]; then
      opt="de"
    elif [[ $ans =~ हिंदी || $ans == 11 ]]; then
      opt="hi"
    elif [[ $ans == [pP]усский || $ans == 12 ]]; then
      opt="ru"
    fi

    if [[ ! $opt ]]; then
      echo
      read -rp "🦖 A language was not selected, will default to 🇬🇧  English. Press ENTER to continue..."

      setLang "en"

      break;
    fi

    if [[ "${supportedLanguages[*]}" =~ $opt ]]; then
      setLang "$opt"

      break;
    fi
  done
}

# Installer state
statusComplete="complete"
installationStatus=""

# Installer script dependencies
# e.g. jq as used to iterate the config
declare -a dependencies=("jq")

# Config (json)
config='{
  "dependencies": [
    {
      "name": "Yq - Cli YAML, JSON, XML, CSV",
      "bin": "yq",
      "pkgManager": {
        "arch": {
          "pkg": "go-yq",
          "name": "pacman"
        },
        "debian": {
          "pkg": "yq",
          "name": "snap"
        },
        "ubuntu": {
          "pkg": "yq",
          "name": "snap"
        }
      }
    },
    {
      "name": "Whois - Internet domain name and network number directory service",
      "bin": "whois",
      "pkgManager": {
        "arch": {
          "pkg": "whois",
          "name": "pacman"
        },
        "debian": {
          "pkg": "whois",
          "name": "apt-get"
        },
        "ubuntu": {
          "pkg": "whois",
          "name": "apt-get"
        }
      }
    },
    {
      "name": "tldextract-rs cli - extract tld info from url",
      "bin": "tldextract",
      "pkgManager": {
        "arch": {
          "pkg": "tldextract",
          "name": "pip"
        },
        "debian": {
          "pkg": "tldextract",
          "name": "apt-get"
        },
        "ubuntu": {
          "pkg": "tldextract",
          "name": "apt-get"
        }
      }
    },
    {
      "name": "dig - DNS lookup utility",
      "bin": "dig",
      "pkgManager": {
        "arch": {
          "pkg": "dnsutils",
          "name": "pacman"
        },
        "debian": {
          "pkg": "dnsutils",
          "name": "apt-get"
        },
        "ubuntu": {
          "pkg": "dnsutils",
          "name": "apt-get"
        }
      }
    }
  ]
}'

# Style utils
txtPrefixForBold=$(tput bold)
txtPrefixForNormal=$(tput sgr0)

mapLanguageYesNoQuitToSystem() {
  if [[ "$1" == "en" ]]; then
    echo "$2"

    return
  fi

  if [[ "$1" == "ch" ]]; then
    case "${2}" in
      是的)    typed="y";;
      不)    typed="n";;
      辞职)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "es" ]]; then
    case "${2}" in
      [sS])    typed="y";;
      [sS]í)    typed="y";;
      [sS]i)    typed="y";;
      [nN])    typed="n";;
      [nN]o)    typed="n";;
      [aA]bandonar)    typed="q";;
      [sS]altar)  typed="skip";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "pt" ]]; then
    case "${2}" in
      [sS])    typed="y";;
      [sS]im)    typed="y";;
      [nN])    typed="n";;
      [nN]ão)    typed="n";;
      [nN]ao)    typed="n";;
      [dD]esistir)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "fr" ]]; then
    case "${2}" in
      Oui)    typed="y";;
      Non)    typed="n";;
      "arrêter")    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "it" ]]; then
    case "${2}" in
      [sS][ìÌ])    typed="y";;
      [nN][oO])    typed="n";;
      "esentata")    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "vi" ]]; then
    case "${2}" in
      Đúng)    typed="y";;
      KHÔNG)    typed="n";;
      "từ bỏ")    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "jp" ]]; then
    case "${2}" in
      はい)    typed="y";;
      いいえ)    typed="n";;
      やめる)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "ko" ]]; then
    case "${2}" in
      예)    typed="y";;
      아니요)    typed="n";;
      그만두다)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "de" ]]; then
    case "${2}" in
      ja)    typed="y";;
      NEIN)    typed="n";;
      aufhören)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "hi" ]]; then
    case "${2}" in
      हाँ)    typed="y";;
      नहीं)    typed="n";;
      छोड़ना)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi

  if [[ "$1" == "ru" ]]; then
    case "${2}" in
      да)    typed="y";;
      нет)    typed="n";;
      покидать)    typed="q";;
      [sS]kip)  typed="skip";;
      *)        typed="$2"
    esac

    echo "$typed"

    return
  fi
}

# Confirm validators
confirmDomainName() {
  local validate="^([a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]\.)+[a-zA-Z]{2,}$"

  if whois "$1" | grep -Ei '[Uu]nallocated|returned 0 objects' > /dev/null; then
    return 1
  fi

  [[ $1 =~ $validate ]]
}

confirmIpAddress() {
  local validate="^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$"

  [[ "$1" =~ $validate ]] && ping -c1 -W1 "$1" > /dev/null
}

confirmEmailAddress() {
  local validate="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]+$"

  [[ "$1" =~ $validate ]]
}

confirm() {
  intlAnswer=$(mapLanguageYesNoQuitToSystem "$1" "$2")

  case $intlAnswer in
    [yY])
      echo 0
      ;;
    [yY][eE][sS])
      echo 0
      ;;
    [nN])
      echo 1
      ;;
    [nN][oO])
      echo 1
      ;;
  esac;
}

resetStyles() {
  echo "${txtPrefixForNormal}"
}

exitInstaller() {
  resetStyles
  exit 1;
}

hasCommand() {
  command -v "$1" >/dev/null 2>&1
}

clearScr() {
  printf '\e[H\e[2J'
}

launchAsciiArt() {
  printf "\r\n"

  echo "★★★★★★★★★"
  echo
  echo "⚡️ ${transl[ascii_art_fleek_team_presents]} ⚡️"
  echo

# the cat and ascii art (ART, as `here tag``)
# is intentionally positioned to the most left
# do not change
cat << "ART"
        _..._
      .'     '.     _
    /    .-""-\   _/ \
  .-|   /:.   |  |   |
  |  \  |:.   /.-'-./
  | .-'-;:__.'    =/
  .'=  *=|URSA _.='
  /   _.  |    ;
  ;-.-'|    \   |
/   | \    _\  _\
\__/'._;.  ==' ==\
        \    \   |
        /    /   /
        /-._/-._/
        \   `\  \
          `-._/._/
ART
# 👆 ART (here tag) end positioned to the most left intentionally

  echo
  echo "⭐️ Ursa, a Decentralized Content Delivery Network (DCDN) ⭐️"
  echo
  echo "★★★★★★★★★ 🌍 ${txtPrefixForBold}Website ${txtPrefixForNormal}https://fleek.network"
  echo "★★★★★★★★★ 📚 ${txtPrefixForBold}Documentation ${txtPrefixForNormal}https://docs.fleek.network"
  echo "★★★★★★★★★ 💾 ${txtPrefixForBold}Git repository ${txtPrefixForNormal}https://github.com/fleek-network/ursa"
  echo "★★★★★★★★★ 🤖 ${txtPrefixForBold}Discord ${txtPrefixForNormal}https://discord.gg/fleekxyz"
  echo "★★★★★★★★★ 🐤 ${txtPrefixForBold}Twitter ${txtPrefixForNormal}https://twitter.com/fleek_net"
  echo "★★★★★★★★★ 🎨 ${txtPrefixForBold}Ascii art by ${txtPrefixForNormal}https://www.asciiart.eu"
}


asciiArtNoOSSupport() {
# the cat and ascii art (ART, as `here tag``)
# is intentionally positioned to the most left
# do not change
cat << "ART"
.     .       .  .   . .   .   . .    +  .
  .     .  :     .    .. :. .___---------___.
       .  .   .    .  :.:. _".^ .^ ^.  '.. :"-_. .
    .  :       .  .  .:../:            . .^  :.:\.
        .   . :: +. :.:/: .   .    .        . . .:\
 .  :    .     . _ :::/:               .  ^ .  . .:\
  .. . .   . - : :.:./.                        .  .:\
  .      .     . :..|:                    .  .  ^. .:|
    .       . : : ..||        .                . . !:|
  .     . . . ::. ::\(                           . :)/
 .   .     : . : .:.|. ######              .#######::|
  :.. .  :-  : .:  ::|.#######           ..########:|
 .  .  .  ..  .  .. :\ ########          :######## :/
  .        .+ :: : -.:\ ########       . ########.:/
    .  .+   . . . . :.:\. #######       #######..:/
      :: . . . . ::.:..:.\           .   .   ..:/
   .   .   .  .. :  -::::.\.       | |     . .:/
      .  :  .  .  .-:.":.::.\             ..:/
 .      -.   . . . .: .:::.:.\.           .:/
.   .   .  :      : ....::_:..:\   ___.  :/
   .   .  .   .:. .. .  .: :.:.:\       :/
     +   .   .   : . ::. :.:. .:.|\  .:/|
     .         +   .  .  ...:: ..|  --.:|
.      . . .   .  .  . ... :..:.."(  ..)"
 .   .       .      :  .   .: ::/  .  .::\
ART
# 👆 ART (here tag) end positioned to the most left intentionally

  echo
  echo "👾 ${transl[ascii_art_no_os_support_alien_technosignature]} 👾"
  echo

  sleep 3
}

requestAuthorizationAndExec() {
  printf -v prompt "\n🤖 %s (%s/%s)?" \
    "$1" \
    "${transl[common_prompt_yes_as_y]}" \
    "${transl[common_prompt_no_as_n]}"
  read -r -p "$prompt"$'\n> ' answer

  intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$answer")

  if [[ "$intlAnswer" == [nN] || "$intlAnswer" == [nN][oO] ]]; then
    printf "\n\n"

    showErrorMessage "$2"

    exitInstaller
  fi

  printf "\n\n"

  $3
}

onExitInstallerTodos() {
  resetStyles
}

onInterruption() {
  # Only show warning message if install NOT complete
  if [[ "$installationStatus" != "$statusComplete" ]]; then
    printf "\r\n"
    echo "😬 ${transl[on_interruption_ouch_error]}"
    echo
    printf "%s" "${transl[on_interruption_if_require_support]}" | sed "s|_defaultDiscordUrl_|\"${defaultDiscordUrl}\"|g"

    onExitInstallerTodos
    exit 1
  fi

  onExitInstallerTodos
  exit 0
}

toLowerCase() {
  echo "$1" | tr '[:upper:]' '[:lower:]'
}

showOkMessage() {
  printf "\r\n✅ %s\n" "$1"  
}

showErrorMessage() {
  printf "\r\n🚩 %s\n" "$1" >&2
}

showPoopMessage() {
  printf "\r\n💩 %s\n" "$1" >&2
}

showHintMessage() {
  printf "\r\n💡 %s\n" "$1"  
}

showDisclaimer() {
  # Display artwork
  launchAsciiArt

  printf "\r\n\n"
  echo "⭐️ ${transl[show_disclaimer_title]}"
  echo
  echo "${transl[show_disclaimer_if_happy]}"
  echo
  printf "%s 👀." "${transl[show_disclaimer_script_repository]}" | sed "s|_defaultGetFleekNetworkRepository_|\"${defaultGetFleekNetworkRepository}\"|g"
  echo
  echo "🤓 ${transl[show_disclaimer_more]}"
  printf "%s" \
    "$(printf "%s" "${transl[show_disclaimer_guides]}" | sed "s|_defaultFleekNetworkDocsWebsite_|\"${defaultFleekNetworkDocsWebsite}\"|g; s|_defaultGetFleekNetworkRepository_|${defaultGetFleekNetworkRepository}|g")"

  printf -v prompt "\n\n🤖 %s?\n%s" \
    "$(printf "%s" "${transl[show_disclaimer_should_continue]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "$(printf "%s" "${transl[show_disclaimer_should_yes_or_no_quit]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")"
  while read -rp "$prompt"$'\n> ' ans; do
    data=$(confirm "$userSelectedLanguage" "$ans")

    [[ ! $data ]] && continue

    if [[ "$data" -eq 1 ]]; then
      echo
      echo "🦖 ${transl[show_disclaimer_terminates_here]}"
      echo
      printf "%s" \
        "$(printf "%s" "${transl[show_disclaimer_terminates_otherwise]}" | sed "s|_defaultFleekNetworkWebsite_|\"${defaultFleekNetworkWebsite}\"|g")"    
      echo

      exitInstaller
    elif [[ "$data" -eq 0 ]]; then
      break
    fi
  done
}

commonWarningMessage() {
  echo "${transl[common_warning_msg_try_support]}"
  printf "%s" \
    "$(printf "%s" "${transl[common_warning_msg_information_declassified]}" | sed "s|_defaultFleekNetworkTwitterUrl_|\"${defaultFleekNetworkTwitterUrl}\"|g")"    
  echo
  echo "${transl[common_warning_msg_check_guides]}"
  echo
  printf "%s" \
    "$(printf "%s" "${transl[common_warning_msg_visit_docs]}" | sed "s|_defaultFleekNetworkDocsWebsite_|\"${defaultFleekNetworkDocsWebsite}\"|g")"    
}

windowsUsersWarning() {
  asciiArtNoOSSupport
  echo "⚠️ ${transl[windows_users_warning_not_supported]}"
  echo "${transl[windows_users_warning_if_not_enable_wsl]}"
  echo
  commonWarningMessage
}

macOsUsersWarning() {
  asciiArtNoOSSupport
  echo "⚠️ ${transl[macos_users_warning_not_supported]}"
  echo
  echo "${transl[macos_users_warning_has_discontinued]}"
  echo
  commonWarningMessage
}

getJQPropertyValue() {
  echo "${1}" | base64 --decode | jq -r "${2}"
}

installGit() {
  os=$(identifyOS)

  if [[ "$os" == "linux" ]]; then
    distro=$(identifyDistro)

    if [[ "$distro" == "ubuntu" ]] || [[ "$distro" == "debian" ]]; then
      sudo apt-get install git
    elif [[ "$distro" =~ "arch"  ]]; then
      sudo pacman -Syu git
    else
      showErrorMessage \
        "$(printf "%s" "${transl[install_git_oops_os_and_distro]}" | sed "s|_os_|\"${os}\"|g; s|_distro_|\"${distro}\"|g; s|_defaultFleekNetworkDocsWebsite_|${defaultFleekNetworkDocsWebsite}|g")"

      exitInstaller
    fi
  else
    showErrorMessage \
      "$(printf "%s" "${transl[install_git_oops_os]}" | sed "s|_os_|\"${os}\"|g; s|_defaultFleekNetworkDocsWebsite_|${defaultFleekNetworkDocsWebsite}|g")"

    exitInstaller
  fi
}

identifyOS() {
  unameOut="$(uname -s)"

  case "${unameOut}" in
      Linux*)     os=Linux;;
      Darwin*)    os=Mac;;
      CYGWIN*)    os=Cygwin;;
      MINGW*)     os=MinGw;;
      *)          os="UNKNOWN:${unameOut}"
  esac

  osToLc=$(toLowerCase "$os")

  echo "$osToLc"
}

isOSSupported() {
  if [[ "$1" == "cygwin" ]] || [[ "$1" == "mingw" ]]; then
    printf "\n"

    windowsUsersWarning

    exitInstaller
  fi

  if [[ "$1" == "mac" ]]; then
    printf "\n"

    macOsUsersWarning

    exitInstaller
  fi

  if [[ "$1" == "ubuntu" ]]; then
    currVersion=$(lsb_release -r -s | tr -d '.')

    if [[ "$currVersion" -lt "2204" ]]; then
      echo
      echo "👹 ${transl[is_os_supported_oops_need_ubuntu]}"
      echo

      exitInstaller
    fi
  fi

  if [[ "$1" == "debian" ]]; then
    currVersion=$(lsb_release -r -s | tr -d '.')

    if [[ "$currVersion" -lt "11" ]]; then
      echo
      echo "👹 ${transl[is_os_supported_oops_need_debian]}"
      echo

      exitInstaller
    fi
  fi
}

identifyDistro() {
  if [[ -f /etc/os-release ]]; then
    source /etc/os-release
    echo "$ID"

    exit 0
  fi
  
  uname
}

checkSystemHasRecommendedResources() {
  mem=$(awk '/^MemTotal:/{print $2}' /proc/meminfo);
  partDiskSpace=$(df --output=avail -B 1 "$PWD" |tail -n 1)

  if [[ ("$mem" -lt "$defaultMinMemoryBytesRequired") ]] || [[ ( "$partDiskSpace" -lt "$defaultMinDiskSpaceBytesRequired" ) ]]; then
    echo "😬 ${transl[check_sys_has_recommended_resources_oh_no_need_at_least]}"
    echo
    printf -v prompt "\n\n🤖 %s?" \
      "$(printf "%s" "${transl[common_prompt_are_you_sure_continue_y_or_n]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")"
    read -r -p "$prompt"$'\n> ' answer

    intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$answer")

    if [[ "$intlAnswer" == [nN] || "$intlAnswer" == [nN][oO] ]]; then
      exitInstaller
    fi

    echo "😅 ${transl[check_sys_has_recommended_resources_warning_below_recommendation]}"

    sleep 5

    return 0
  fi
  
  showOkMessage "${transl[check_sys_has_recommended_resources_great_enought_resources]}"
}

checkIfGitInstalled() {
  if ! hasCommand git; then
    echo "😅 ${transl[check_if_git_installed_oops]}"
    echo

    requestAuthorizationAndExec \
      "${transl[common_check_installed_we_can_start_install]}" \
      "${transl[check_if_git_installed_need_git_installed]}" \
      installGit

    if [[ "$?" = 1 ]]; then
      showErrorMessage "${transl[check_if_git_installed_oops_failed_install]}"

      exitInstaller
    fi
  fi

  showOkMessage "${transl[check_if_git_installed_success_install]}"
}

gitHealthCheck() {
  if ! hasCommand git; then
    showErrorMessage "${transl[git_health_check_oops_odd_reason]}"

    exitInstaller
  fi
}

installDocker() {
  os=$(identifyOS)

  if [[ "$os" == "linux" ]]; then
    distro=$(identifyDistro)

    if [[ "$distro" == "ubuntu" ]]; then
      sudo apt-get update
      sudo apt-get install \
        ca-certificates \
        curl \
        gnupg \
        lsb-release

      sudo mkdir -p /etc/apt/keyrings
      curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

      echo \
        "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
        $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

      sudo apt-get update

      sudo apt-get install \
          docker-ce \
          docker-ce-cli \
          containerd.io \
          docker-compose-plugin \
          docker-compose

      # https://docs.docker.com/build/buildkit/
      sudo mkdir -p /etc/docker
      sudo bash -c 'echo "{
        \"features\": {
          \"buildkit\" : true
          }
        }" > /etc/docker/daemon.json'
    elif [[ "$distro" == "debian" ]]; then
      sudo apt-get update
      sudo apt-get install \
        ca-certificates \
        curl \
        gnupg \
        lsb-release \
        dnsutils

      sudo mkdir -p /etc/apt/keyrings
      curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

      echo \
        "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
        $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

      sudo apt-get update

      sudo apt-get install \
          docker-ce \
          docker-ce-cli \
          containerd.io \
          docker-compose-plugin \
          docker-compose

      # https://docs.docker.com/build/buildkit/
      sudo mkdir -p /etc/docker
      sudo bash -c 'echo "{
        \"features\": {
          \"buildkit\" : true
          }
        }" > /etc/docker/daemon.json'
    elif [[ "$distro" =~ "arch" ]]; then
      sudo pacman -Syu gnome-terminal docker docker-compose

      sudo systemctl enable docker.service

      latestKernel=$(pacman -Q linux)
      currentKernel=$(uname -r)

      if [[ "$latestKernel" != "$currentKernel" ]]; then
        echo "✋ ${transl[install_docker_need_reboot]}"
        printf "%s" "${transl[install_docker_need_reboot_reason]}" | sed "s|_currentKernel_|\"${currentKernel}\"|g; s|_latestKernel_|${latestKernel}|g"
        echo
        echo "${transl[install_docker_need_reboot_if_failure]}"
        echo "${transl[install_docker_suggest_rebooting]}"
        echo

        read -rp "${transl[common_press_enter_continue]} "
      fi
    else
      printf -v common_oops_linux_distro_not_supported "%s" \
      "$(printf "%s" "${transl[common_oops_linux_distro_not_supported]}" | sed "s|_defaultGetFleekNetworkUrl_|\"${defaultGetFleekNetworkUrl}\"|g; s|_defaultFleekNetworkDocsWebsite_|${defaultFleekNetworkDocsWebsite}|g")"
      showErrorMessage "${common_oops_linux_distro_not_supported}"

      exitInstaller
    fi
  else
    printf -v common_oops_os_not_supported "%s" \
    "$(printf "%s" "${transl[common_oops_os_not_supported]}" | sed "s|_defaultFleekNetworkDocsWebsite_|\"${defaultFleekNetworkDocsWebsite}\"|g")"
    showErrorMessage "${common_oops_os_not_supported}"

    exitInstaller
  fi
}

checkIfDockerInstalled() {
  # TODO: docker-compose is going to be deprecated
  # but before moving to `docker compose` as subcommand
  # is required to check the user docker version and also
  # check if docker compose is available. Some users are still
  # using legacy e.g., preinstalled docker or archlinux, etc
  if ! hasCommand docker || ! hasCommand docker-compose; then
    printf "😅 %s\n" "${transl[check_if_docker_installed]}"

    requestAuthorizationAndExec \
      "${transl[common_check_installed_we_can_start_install]}" \
      "${transl[check_if_docker_need_docker_installed]}" \
      installDocker

    if [[ "$?" = 1 ]]; then
      showErrorMessage "${transl[check_if_docker_oops_failed_install_docker]}"

      exitInstaller
    fi
  fi

  showOkMessage "${transl[check_if_docker_success]}"
}

dockerHealthCheck() {
  if ! hasCommand docker-compose; then
    showErrorMessage "${transl[docker_health_check_oops_docker_compose]}"

    exitInstaller
  fi

  expectedMessage="Hello from Docker"
  res=$(docker run -i --log-driver=none -a stdout hello-world)

  if ! echo "$res" | grep "$expectedMessage" &> /dev/null; then
    showErrorMessage "${transl[docker_health_check_oops_docker_daemon_hello_world]}"

    exitInstaller
  fi

  showOkMessage "${transl[docker_health_check_passed]}"
}

requestPathnameForUrsaRepository() {
  # TODO: on translation, the selected path needs to be computed
  # that is that on runtime the defaultPath might need to be replaced
  # by the user selected choice, as we have these as precomputed text
  # TODO: close after test param expansion, see below
  selectedUrsaPath=$defaultUrsaPath

  printf -v prompt "\n🤖 %s\n\n%s?\n%s" \
    "${transl[request_path_for_ursa_repo_save_to_location_use_default]/_selectedUrsaPath_/$selectedUrsaPath}" \
    "$(printf "%s" "${transl[request_path_for_ursa_repo_save_to_location_y_or_n_change]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "${transl[request_path_for_ursa_repo_save_to_location_happy_proceed]}"
  
  while read -r -p "$prompt"$'\n> ' answer; do
    intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$answer")
  
    if [[ "$intlAnswer" == "" || "$intlAnswer" == [yY] || "$intlAnswer" == [yY][eE][sS] || "$intlAnswer" == [nN] || "$intlAnswer" == [nN][oO] ]]; then
      break
    fi
  done


  if [[ ! "$intlAnswer" == "" && "$intlAnswer" == [nN] || "$intlAnswer" == [nN][oO] ]]; then
    printf -v prompt "\n🙋‍♀️ %s" "${transl[request_path_for_ursa_repo_save_to_locattion_where_store]}"
    read -r -p "$prompt"$'\n> ' answer
  
    selectedUrsaPath="$answer"
  fi


  if [[ -d "$selectedUrsaPath" ]]; then
    # TODO: test param expansion
    showErrorMessage "${transl[request_path_for_ursa_repo_save_to_locattion_oops_already_exists]/_selectedUrsaPath_/$selectedUrsaPath}"

    read -r -p "${transl[common_press_enter_retry]}..."

    requestPathnameForUrsaRepository
  fi

  if ! mkdir -p "$selectedUrsaPath"; then
    # TODO: test param expansion
    showErrorMessage "${transl[request_path_for_ursa_repo_save_to_oops_failed_create_dir]/_selectedUrsaPath_/$selectedUrsaPath}"

    exitInstaller
  fi

  echo "$selectedUrsaPath"
}

cloneUrsaRepositoryToPath() {
  if ! git clone $defaultUrsaHttpsRepository "$1"; then
    showErrorMessage \
      "$(printf "%s" "${transl[clone_ursa_repo_to_path_oops_failed_to_clone]}" | sed "s|_defaultUrsaHttpsRepository_|\"${defaultUrsaHttpsRepository}\"|g")"

    exitInstaller
  fi

  showOkMessage \
    "$(printf "%s" "${transl[clone_ursa_repo_to_path_ok_msg]}" | sed "s|_defaultUrsaHttpsRepository_|\"${defaultUrsaHttpsRepository}\"|g; s|_selectedUrsaPath_|${1}|g")"

  printf "\r\n"
}

restartDockerStack() {
  echo "🤖 ${transl[restart_docker_stack_is_going_to_restart]}"
  echo

  # TODO: Use health check instead
  sleep 10
  sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" stop

  showOkMessage "${transl[restart_docker_stack_is_going_to_start]}"

  # TODO: Use health check instead
  sleep 10
  sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" up -d

  showOkMessage "${transl[restart_docker_stack_has_restarted]}"

  sleep 3
}

showDockerStackLog() {
  echo
  echo "★★★★★★★★★"
  echo
  echo "🥳 ${transl[show_docker_stack_log_great]}"
  echo
  echo "${transl[show_docker_stack_log_stack_should_be_running]}"
  echo
  echo "${transl[show_docker_stack_log_stack_logs_can_be_verbose_1_of_2]}"
  echo "${transl[show_docker_stack_log_stack_logs_can_be_verbose_2_of_2]}"
  echo "https://docs.fleek.network/guides/Network%20nodes/fleek-network-node-health-check-guide"
  echo
  echo "${transl[show_docker_stack_log_stack_logs_handy_commands]}"
  echo
  echo "  - ${transl[show_docker_stack_log_stack_logs_handy_cmd_show_logs]}:"
  echo
  echo "    ${txtPrefixForBold}docker-compose -f ./docker/full-node/docker-compose.yml logs -f${txtPrefixForNormal}"
  echo
  echo "  - ${transl[show_docker_stack_log_stack_logs_handy_cmd_signit]}:"
  echo
  echo "    ${txtPrefixForBold}Ctrl-c${txtPrefixForNormal}"
  echo
  printf "%s" "${transl[show_docker_stack_log_stack_logs_can_stop_start_docker]}" | sed "s|_selectedUrsaPath_|\"${selectedUrsaPath}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g"
  echo "${transl[show_docker_stack_log_stack_logs_can_stop_start_docker_example]}"
  echo
  echo "${transl[show_docker_stack_log_stack_logs_can_stop_then_after_start_or_stop]}"
  echo
  echo "  - ${transl[show_docker_stack_log_stack_logs_can_stop_then_after_start]}"
  echo
  echo "    ${txtPrefixForBold}docker-compose -f ./docker/full-node/docker-compose.yml up${txtPrefixForNormal}"
  echo
  echo "  - ${transl[show_docker_stack_log_stack_logs_can_stop_then_after_stop]}"
  echo
  echo "    ${txtPrefixForBold}docker-compose -f ./docker/full-node/docker-compose.yml down${txtPrefixForNormal}"
  echo
  echo "🥹 ${transl[show_docker_stack_log_stack_logs_seems_a_lot]}"
  # The extra white space between ✏️ and start of text is intentional and used for alignment
  printf "🤓  %s" "${transl[show_docker_stack_log_stack_logs_learn_how_maintain]}" | sed "s|_defaultFleekNetworkDocsWebsite_|\"${defaultFleekNetworkDocsWebsite}\"|g"

  printf "🌈 %s" "${transl[show_docker_stack_log_stack_logs_got_feedback]}" | sed "s|_defaultDiscordUrl_|\"${defaultDiscordUrl}\"|g; s|_defaultFleekNetworkTwitterUrl_|${defaultFleekNetworkTwitterUrl}|g"
  echo
  echo "★★★★★★★★★"
  echo

  printf -v prompt "\n🙋‍♀️ %s\n%s" \
    "${transl[show_docker_stack_log_stack_logs_prompt_see_output]}" \
    "$(printf "%s" "${transl[show_docker_stack_log_stack_logs_prompt_see_output_y_or_skip]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_skip_|skip|g")"
  read -r -p "$prompt"$'\n> ' answer

  intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$answer")

  if [[ "$intlAnswer" == [sS][kK][iI][pP] ]]; then
    printf "\r\n"

    echo "🚀 ${transl[show_docker_stack_log_stack_logs_completed_process_thanks]}"
    printf "🤗 %s" "${transl[show_docker_stack_log_stack_logs_completed_process_remember]}" | sed "s|_defaultFleekNetworkWebsite_|\"${defaultFleekNetworkWebsite}\"|g"

    echo

    onExitInstallerTodos
    
    exit 0;
  fi

  # whitespace to improve   
  printf "\r\n"
  
  echo "👋 ${transl[show_docker_stack_log_stack_logs_quick_hint]}!"
  echo
  echo "${transl[show_docker_stack_log_stack_logs_stack_logs_verbose]}"
  echo
  echo "${transl[show_docker_stack_log_stack_logs_stack_keeps_awake_discord]}"
  echo
  echo "${transl[show_docker_stack_log_stack_logs_about_log_messages_ignore]}"

  read -r -p "${transl[common_press_enter_continue]}... "

  sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" logs -f
}

initLetsEncrypt() {
  email="$1"
  domain="$2"

  if [[ -z "$1" || -z "$2" ]]; then
    showErrorMessage "${transl[init_lets_encrypt_gosh_embarrassing_missing_args]} 🙏"

    exitInstaller
  fi

  rsa_key_size=4096
  base_path=./docker/full-node
  config_path="$defaultDockerComposeYmlRelativePath"
  data_path="$defaultDockerFullNodeRelativePath/data/certbot"
  staging=0

  # TODO: Move this checkup much earlier, in the init of installer
  # Do we have the required files, if not interrupt the process?
  if [[ ! -d "$base_path" || ! "$(ls -A $base_path)" ]]; then
    showErrorMessage "${transl[init_lets_encrypt_oops_missing_required_files]} 🙏"

    exitInstaller
  fi

  if [[ ! -e "$data_path/conf/options-ssl-nginx.conf" || ! -e "$data_path/conf/ssl-dhparams.pem" ]]; then
    echo
    echo "🤖 ${transl[init_lets_encrypt_download_recommended_tls]}..."
    echo

    mkdir -p "$data_path/conf"

    curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
    curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
    echo
  fi

  echo
  echo "🤖 ${transl[init_lets_encrypt_create_dummy_certificates]/_domain_/$domain} 🙏"
  echo

  path="/etc/letsencrypt/live/$domain"
  mkdir -p "$data_path/conf/live/$domain"

  if ! docker-compose -f "$config_path" \
    run --rm --entrypoint "\
    openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
      -keyout '$path/privkey.pem' \
      -out '$path/fullchain.pem' \
      -subj '/CN=localhost'" certbot; then    
    showErrorMessage "${transl[init_lets_encrypt_oops_failed_create_dummy_certificate]}"

    exitInstaller
  fi

  echo
  echo "🤖 ${transl[init_lets_encrypt_starting_nginx]} 🙏"
  echo

  if ! docker-compose -f "$config_path" up --force-recreate -d nginx; then
    showErrorMessage "${transl[init_lets_encrypt_failed_start_nginx]}..."

    exitInstaller
  fi
  
  echo
  echo "🤖 ${transl[init_lets_encrypt_deleting_dummy_certificate]/_domain_/$domain}..."
  echo

  if ! docker-compose -f "$config_path" \
    run --rm --entrypoint "\
    rm -Rf /etc/letsencrypt/live/$domain && \
    rm -Rf /etc/letsencrypt/archive/$domain && \
    rm -Rf /etc/letsencrypt/renewal/$domain.conf" certbot; then

    showErrorMessage "${transl[init_lets_encrypt_oops_failed_delete_certificates]/_domain_/$domain}..."

    exitInstaller
  fi

  echo
  echo "🤖 ${transl[init_lets_encrypt_requesting_lets_encrypt_certificates]/_domain_/$domain}..."
  echo

  # Enable staging mode if needed
  if [ $staging != "0" ]; then staging_arg="--staging"; fi

  if ! docker-compose -f "$config_path" run --rm --entrypoint "\
    certbot certonly --webroot -w /var/www/certbot \
      $staging_arg \
      --email $email \
      --domain $domain \
      --rsa-key-size $rsa_key_size \
      --agree-tos -n \
      --force-renewal" certbot; then
    showErrorMessage "${transl[init_lets_encrypt_oops_failed_create_tls_certificates]} https://docs.fleek.network/guides/Network%20nodes/fleek-network-securing-a-node-with-ssl-tls"

    printf -v prompt "\n💡 %s\n\n🙋‍♀️ %s\n%s" \
      "${transl[init_lets_encrypt_prompt_recommend_try_again]}" \
      "${transl[init_lets_encrypt_prompt_recommend_try_again_ask]}" \
      "$(printf "%s" "${transl[init_lets_encrypt_prompt_recommend_try_again_y_or_quit]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_quit_|${transl[common_prompt_quit_as_q]}|g")"
    while read -r -p "$prompt"$'\n> ' answer; do
      intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$answer")
    
      if [[ "$intlAnswer" == "" || "$intlAnswer" == [yY] || "$intlAnswer" == [qQ] ]]; then
        break
      fi
    done

    if [[ "$intlAnswer" == [qQ] ]]; then
      printf "🦖 %s" "${transl[init_lets_encrypt_prompt_recommend_you_have_quit]}" | sed "s|_defaultFleekNetworkDocsWebsite_|\"${defaultFleekNetworkDocsWebsite}\"|g"

      exit 1
    fi

    if [[ "$intlAnswer" == "" || "$intlAnswer" == [yY] || "$intlAnswer" == [yY][eE][sS] ]]; then    
      echo
      echo "💡 ${transl[init_lets_encrypt_if_issue_persists]}"
      echo "${transl[init_lets_encrypt_linux_os_dns_config_location_1_of_2]}"
      echo "${transl[init_lets_encrypt_linux_os_dns_config_location_2_of_2]}"
      echo
      echo "🤖 ${transl[init_lets_encrypt_restart_docker_stack]}..."
      echo
      sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" down      
      sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" start

      initLetsEncrypt "$email" "$domain"
    fi

    sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" down

    exitInstaller
  fi

  echo
  echo "🤖 ${transl[init_lets_encrypt_reloading_nginx]}..."
  echo

  docker-compose -f "$config_path" exec nginx nginx -s reload

  showOkMessage "${transl[init_lets_encrypt_success_secured_message]}"
}

installMandatory() {
  hasCommand "$1" && return 0

  printf -v prompt "\n\n🤖 %s\n%s" \
    "$(printf "%s" "${transl[common_install_we_need_to]}" | sed "s|_application_|\"${1}\"|g; s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "$(printf "%s" "${transl[common_install_type_y_or_n]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")"
  while read -rp "$prompt"$'\n> ' answer; do
    data=$(confirm "$userSelectedLanguage" "$answer")

    [[ ! $data ]] && continue

    if [[ "$data" -eq 1 ]]; then
      showErrorMessage "${transl[install_mandatory_app_install_required]/_application_/$1}"
      exitInstaller
    elif [[ "$data" -eq 0 ]]; then
      break
    fi
  done

  if [[ "$os" == "linux" ]]; then
    distro=$(identifyDistro)
    if [[ "$distro" == "ubuntu" ]] || [[ "$distro" == "debian" ]]; then
      if ! sudo apt update || ! sudo apt-get install "$1"; then
        exitInstaller
      fi
    elif [[ "$distro" =~ "arch"  ]]; then
      ! sudo pacman -Syu "$1" && exitInstaller
    else
      echo "👹 ${transl[install_mandatory_distro_not_supported]/_distro_/$distro}"
      echo "${transl[install_mandatory_if_bug_discord]} 🙏"
      echo

      exitInstaller
    fi
  fi

  showOkMessage "${transl[install_mandatory_installed]} $1"

  # Add some space after the msg
  printf "\r\n"

  return 0
}

verifyDepsOrInstall() {
  os="$1"
  name="$2"
  bin="$3"
  pkgManager="$4"

  hasCommand "$bin" && return 0

  printf -v prompt "\n\n🤖 %s\n%s" \
    "$(printf "%s" "${transl[common_install_we_need_to]}" | sed "s|_application_|\"${bin}\"|g; s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "$(printf "%s" "${transl[common_install_type_y_or_n]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")"
  while read -rp "$prompt"$'\n> ' answer; do
    data=$(confirm "$userSelectedLanguage" "$answer")

    [[ ! $data ]] && continue

    if [[ "$data" -eq 1 ]]; then
      showErrorMessage "${transl[install_mandatory_app_install_required]/_application_/$1}"
      exitInstaller
    elif [[ "$data" -eq 0 ]]; then
      break
    fi
  done

  if [[ "$ans" == "" ]]; then
    echo "😅 ${transl[verify_deps_installed_need_yes_or_no_answer]}"
    
    read -r -p "${transl[common_press_enter_retry]}..."

    verifyDepsOrInstall "$os" "$name" "$bin" "$pkgManager"
  fi

  if [[ "$os" == "linux" ]]; then
    distro=$(identifyDistro)
    pkgManager=$(echo "$pkgManager" | jq ".$distro")
    pkgManagerName=$(echo "$pkgManager" | jq -r ".name")
    pkg=$(echo "$pkgManager" | jq -r ".pkg")

    if [[ $pkgManager =~ "null" || $pkgManagerName =~ "null" || $pkg =~ "null" ]]; then
      echo "💩 ${transl[verify_deps_installed_config_has_missing_values]}"
      echo "LOG: pkgm ($pkgManager), pkgmn ($pkgManagerName), pkg ($pkg)"
      echo
      printf "%s" "${transl[verify_deps_installed_help_us_improve_discord]}" | sed "s|_defaultDiscordUrl_|\"${defaultDiscordUrl}\"|g"

      echo "${transl[verify_deps_installed_thanks_support]} 🙏"
      echo

      exitInstaller
    fi

    if [[ "$distro" == "ubuntu" || "$distro" == "debian" ]]; then
      if [[ $pkgManagerName == "snap" ]]; then
        if ! hasCommand "snap"; then
          sudo apt-get install snapd
        fi

        if ! sudo snap install "$pkg"; then
          exitInstaller
        fi

        showOkMessage "${transl[verify_deps_installed_via_package_manager]/_name_/$name} snap"

        # ensure snap is in the PATH and pkgs
        source /etc/profile.d/apps-bin-path.sh

        return 0
      fi

      if ! sudo apt update || ! sudo apt-get install "$pkg"; then
        exitInstaller
      fi

      showOkMessage "${transl[verify_deps_installed_via_package_manager]/_name_/$name} apt-get"

      return 0
    elif [[ "$distro" =~ "arch"  ]]; then
      if [[ $pkgManagerName == "pip" ]]; then
        if ! hasCommand pip; then 
          sudo pacman --noconfirm -Syu python-pip
        fi

        pip install tldextract==3.4.0

        return 0
      fi

      ! sudo pacman --noconfirm -Syu "$pkg" && exitInstaller

      showOkMessage "${transl[verify_deps_installed_via_package_manager]/_name_/$name} pacman"
    fi
  fi
}

extactDomainName() {
  name=$(tldextract "$1" | cut -d " " -f 2)
  tld=$(tldextract "$1" | cut -d " " -f 3)

  domain="$name.$tld"

  echo "$domain"
}

verifyUserHasDomain() {
  printf -v prompt "\n%s?\n%s" \
    "$(printf "%s" "${transl[verify_user_has_domain_have_domain_ready]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "$(printf "%s" "${transl[verify_user_has_domain_have_domain_ready_yes_or_n_or_q]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g; s|_quit_|\"${transl[common_prompt_quit_as_q]}\"|g")"
  read -r -p "$prompt"$'\n> ' ans

  intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$ans")

  if [[ "$intlAnswer" == [qQ] ]]; then
    showErrorMessage \
      "$(printf "🦖 %s" "${transl[verify_user_has_domain_user_quit_message]}" | sed "s|_defaultFleekNetworkDocsWebsite_|\"${defaultFleekNetworkDocsWebsite}\"|g")"

    exit 1
  fi

  if [[ ! "$intlAnswer" == "" && "$intlAnswer" == [nN] || "$intlAnswer" == [nN][oO] ]]; then
    printf "\n"

    showErrorMessage "${transl[verify_user_has_domain_oops_best_have_domain]}"

    read -r -p "${transl[common_press_enter_retry]}..."

    verifyUserHasDomain
  fi

  # Domain name handling (start)
  printf -v prompt "\n💡 %s\n\n%s" "${transl[verify_user_has_domain_provide_domain_without_http_1_of_2]}" "${transl[verify_user_has_domain_provide_domain_without_http_2_of_2]}"
  while read -rp "$prompt"$'\n> ' ans; do
    if confirmDomainName "$ans"; then
      userDomainName="$ans"
      break
    fi

    showPoopMessage "${transl[verify_user_has_domain_uh_oh_provide_valid_domain]}..."
  done

  # Ip address handling (start)
  ERROR_IP_ADDRESS_NOT_AVAILABLE="ERROR_IP_ADDRESS_NOT_AVAILABLE"
  detectedIpAddress=$(curl --silent ifconfig.me || curl --silent icanhazip.com || echo "$ERROR_IP_ADDRESS_NOT_AVAILABLE")

  printf -v prompt "\n💡 %s\n\n%s\n\n%s" "${transl[verify_user_has_domain_provide_ip_of_machine_1_of_3]/_detectedIpAddress_/$detectedIpAddress}" "${transl[verify_user_has_domain_provide_ip_of_machine_2_of_3]}" "${transl[verify_user_has_domain_provide_ip_of_machine_3_of_3]}"
  while read -rp "$prompt"$'\n> ' ans; do
    if confirmIpAddress "$ans"; then
      serverIpAddress="$ans"
      break
    elif [[ "$ans" == "" ]]; then
      break
    fi

    showPoopMessage "${transl[verify_user_has_domain_uh_oh_provide_valid_ip]}..."
  done

  # Declare detected ip address as default server ip address
  serverIpAddress=${ans:="$detectedIpAddress"}

  if [[ $serverIpAddress = "$ERROR_IP_ADDRESS_NOT_AVAILABLE" ]]; then
    showErrorMessage "${transl[verify_user_has_domain_oops_embarassing]/_ERROR_IP_ADDRESS_NOT_AVAILABLE_/$ERROR_IP_ADDRESS_NOT_AVAILABLE}"

    exitInstaller
  fi

  # given a name and an ip address, test whether there is a record for name pointing to address
  if ! dig "$userDomainName" +nostats +nocomments +nocmd | tr -d '\t' | grep "A$serverIpAddress" >/dev/null 2>&1 ; then
    showErrorMessage "${transl[verify_user_has_domain_oops_domain_no_dns_record_correct_ip_1_of_2]/_userDomainName_/$userDomainName} ${transl[verify_user_has_domain_oops_domain_no_dns_record_correct_ip_2_of_2]/_serverIpAddress_/$serverIpAddress}"

    read -r -p "${transl[common_press_enter_retry]}..."

    verifyUserHasDomain
  fi

  # Email handling (start)
  printf -v prompt "💡 %s\n%s\n\n%s\n%s\n\n%s" "${transl[verify_user_has_domain_provide_valid_email_1_of_5]}" "${transl[verify_user_has_domain_provide_valid_email_2_of_5]}" "${transl[verify_user_has_domain_provide_valid_email_3_of_5]}" "${transl[verify_user_has_domain_provide_valid_email_4_of_5]}" "${transl[verify_user_has_domain_provide_valid_email_5_of_5]}"
  while read -rp "$prompt"$'\n> ' ans; do
    if confirmEmailAddress "$ans"; then
      emailAddress=$(toLowerCase "$ans")
      break
    fi

    showPoopMessage "${transl[verify_user_has_domain_uh_oh_provide_valid_email]}..."
  done

  printf -v prompt "\n🤖 %s\n\n%s\n%s\n%s\n\n%s\n%s" \
    "${transl[verify_user_has_domain_details_confirmation_1_of_6]}" \
    "${transl[verify_user_has_domain_details_confirmation_2_of_6]/_userDomainName_/$userDomainName}" \
    "${transl[verify_user_has_domain_details_confirmation_3_of_6]/_serverIpAddress_/$serverIpAddress}" \
    "${transl[verify_user_has_domain_details_confirmation_4_of_6]/_emailAddress_/$emailAddress}" \
    "$(printf "%s" "${transl[verify_user_has_domain_details_confirmation_5_of_6]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "$(printf "%s" "${transl[verify_user_has_domain_details_confirmation_6_of_6]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")"

  while read -rp "$prompt"$'\n> ' ans; do
    intlAnswer=$(mapLanguageYesNoQuitToSystem "$userSelectedLanguage" "$ans")

    case $intlAnswer in
      [yY])
        break
        ;;
      [yY][eE][sS])
        break
        ;;
      [nN])
        verifyUserHasDomain
        break
        ;;
      [nN][oO])
        verifyUserHasDomain
        break
        ;;
    esac;
  done;

  echo "$userDomainName;$emailAddress"

  exit 0
}

replaceNginxConfFileForHttp() {
  echo "
    proxy_cache_path /cache keys_zone=nodecache:100m levels=1:2 inactive=31536000s max_size=10g use_temp_path=off;

    server {
        listen 80;
        listen [::]:80;
        server_name $1;

        location /.well-known/acme-challenge/ {
            root /var/www/certbot;
        }

        location /stub_status {
          stub_status;
        }

        proxy_redirect          off;
        client_max_body_size    10m;
        client_body_buffer_size 128k;
        proxy_connect_timeout   90;
        proxy_send_timeout      90;
        proxy_read_timeout      90;
        proxy_buffers           32 128k;

        location / {
          add_header content-type  application/vnd.ipld.raw;
          add_header content-type  application/vnd.ipld.car;
          add_header content-type  application/octet-stream;
          add_header cache-control public,max-age=31536000,immutable;

          proxy_cache nodecache;
          proxy_cache_valid 200 31536000s;
          add_header X-Proxy-Cache \$upstream_cache_status;
          proxy_cache_methods GET HEAD POST;
          proxy_cache_key \"\$request_uri|\$request_body\";
          client_max_body_size 1G;

          proxy_pass http://ursa:4069;
        }
    }
  " > "$defaultDockerFullNodeRelativePath"/data/nginx/http.conf
}

replaceNginxConfFileForHttps() {
  echo "
    server {
        listen 443 ssl http2;
        listen [::]:443 ssl http2;
        server_name $1;

        server_tokens off;

        # SSL code
        ssl_certificate /etc/letsencrypt/live/$1/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/$1/privkey.pem;

        include /etc/letsencrypt/options-ssl-nginx.conf;
        ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

        location /stub_status {
          stub_status;
        }

        location / {
          add_header content-type  application/vnd.ipld.raw;
          add_header content-type  application/vnd.ipld.car;
          add_header content-type  application/octet-stream;
          add_header cache-control public,max-age=31536000,immutable;

          proxy_cache nodecache;
          proxy_cache_valid 200 31536000s;
          add_header X-Proxy-Cache \$upstream_cache_status;
          proxy_cache_methods GET HEAD POST;
          proxy_cache_key \"\$request_uri|\$request_body\";
          client_max_body_size 1G;


          proxy_pass http://ursa:4069;
        }
    }
  " > "$defaultDockerFullNodeRelativePath"/data/nginx/https.conf
}

setupSSLTLS() {
  echo "⚠️ ${transl[setup_ssl_tls_you_are_required]}"
  echo
  echo "${transl[setup_ssl_tls_visit_your_domain_name_registrar]}"
  echo
  # The extra white space between the 🫡 and text is intentional for spacing
  echo "🫡  ${transl[setup_ssl_tls_complete_step_before_proceeding]}"
  echo "${transl[setup_ssl_tls_reason_to_secure_server]}"
  echo
  echo "🙏 ${transl[setup_ssl_tls_learn_more_about]}"
  printf "\n"

  trimData=$(verifyUserHasDomain | xargs)

  if [[ ! "$trimData" ]]; then
    showErrorMessage "🦖 ${transl[setup_ssl_tls_failed_verify_user_domain_and_email]}..."

    exit 1
  fi

  userDomainName=$(echo "$trimData" | cut -d ";" -f 1)
  emailAddress=$(echo "$trimData" | cut -d ";" -f 2)

  if ! rm "$defaultDockerFullNodeRelativePath"/data/nginx/app.conf; then
    showErrorMessage "${transl[setup_ssl_tls_oops_failed_clear_nginx]} 🙏"

    exitInstaller
  fi

  if ! replaceNginxConfFileForHttp "$userDomainName"; then
    showErrorMessage "${transl[setup_ssl_tls_oops_failed_update_http_server_name]/_userDomainName_/$userDomainName} 🙏"

    exitInstaller
  fi

  chmod +x "$defaultDockerFullNodeRelativePath"/init-letsencrypt.sh

  showOkMessage "${transl[setup_ssl_tls_updated_file_permissions_lets_encrypt_execution]} (set +x)!"

  # Intentional, used to provide space after msg
  echo

  # start stack in bg, as lets encrypt will need the nginx to validate
  COMPOSE_DOCKER_CLI_BUILD=1 sudo docker-compose -f "$defaultDockerComposeYmlRelativePath" up -d

  # TODO: add health check in the docker compose file

  # Health check
  statusCode=$(curl --write-out "%{http_code}" --silent --output /dev/null http://"$userDomainName"/ping)

  if [[ "$statusCode" -ne 200 ]]; then
    showPoopMessage "${transl[setup_ssl_tls_uh_oh_cant_communicate_with_reverse_proxy]}"

    read -r -p "😅 ${transl[setup_ssl_tls_lets_encrypt_likely_fail_if_continue]}..."
  fi

  if ! initLetsEncrypt "$emailAddress" "$userDomainName"; then
    exitInstaller
  fi

  printf "\n"

  if ! replaceNginxConfFileForHttps "$userDomainName"; then
    showErrorMessage "${transl[setup_ssl_tls_oops_failed_update_https_server_name_directive]/_userDomainName_/$userDomainName} 🙏"

    exitInstaller
  fi
}

changeDirectoryToPathOrFailure() {
  local name=$1
  local targetPath=$2

  if [[ -z "$name" || -z "$targetPath" ]]; then
    showErrorMessage "${transl[change_directory_to_path_oops_failed_change_dir_discord]} 🙏"

    exitInstaller
  fi

  if [[ $(pwd) != "$targetPath" ]]; then
    if ! cd "$targetPath"; then
      showErrorMessage "${transl[change_directory_to_path_oops_failed_change_dir_to_name]/_name_/$name} 🙏"

      exitInstaller
    fi
  fi
}

onLatestPreference() {
  echo
  echo "✋ ${transl[on_latest_preference_when_running_stack_docker_build]}"
  echo "💡 ${transl[on_latest_preference_our_recommendation]} 😴!"
  echo
  echo "${transl[on_latest_preference_inf_future_check_documentation]}"
  echo

  printf -v prompt "\n\n🤖 %s\n%s" \
    "$(printf "%s" "${transl[on_latest_preference_prompt_happy_use_latest]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")" \
    "$(printf "%s" "${transl[on_latest_preference_prompt_happy_use_latest_yes_or_no]}" | sed "s|_yes_|\"${transl[common_prompt_yes_as_y]}\"|g; s|_no_|\"${transl[common_prompt_no_as_n]}\"|g")"
  while read -rp "$prompt"$'\n> ' ans; do
    data=$(confirm "$userSelectedLanguage" "$ans")

    [[ ! $data ]] && continue

    if [[ "$data" -eq 1 ]]; then
      echo
      echo "🦀 ${transl[on_latest_preference_mind_ursa_time_build_long]}..."
      echo

      break
    elif [[ "$data" -eq 0 ]]; then
      yq -ie 'del(.services.ursa.build)' "$defaultDockerComposeYmlRelativePath" >/dev/null 2>&1
      yq -i ".services.ursa.image = \"$defaultUrsaLatestImage\"" "$defaultDockerComposeYmlRelativePath" >/dev/null 2>&1

      if ! yq '.services.ursa.image' "$defaultDockerComposeYmlRelativePath" | grep -q "$defaultUrsaLatestImage" || ! yq '.services.ursa.build' "$defaultDockerComposeYmlRelativePath" | grep -qE "null|no matches found"; then
        echo
        echo "💩 ${transl[on_latest_preference_uh_oh_failed_modify_docker_compose_yaml]}"

        printf "%s 🙏" "${transl[on_latest_preference_uh_oh_failed_modify_docker_compose_yaml_apologies]}" | sed "s|_defaultDiscordUrl_|\"${defaultDiscordUrl}\"|g"
        echo

        exitInstaller
      fi

      break
    fi
  done
}

(
  # stdin to keyboard
  exec < /dev/tty;

  # SIGINT listener
  trap onInterruption INT

  # Set default locale
  multiLanguageChoice

  # Identity the OS
  os=$(identifyOS)

  isOSSupported "$os"

  # Show disclaimer
  showDisclaimer

  # Had space after disclaimer
  printf "\r\n"

  # Check if system has recommended resources (disk space and memory)
  checkSystemHasRecommendedResources "$os"

  # Check if has dependencies installed
  for dep in "${dependencies[@]}"; do
    installMandatory "$dep"
  done

  for dep in $(echo "${config}" | jq -r '.dependencies[] | @base64'); do
    name=$(getJQPropertyValue "$dep" ".name")
    bin=$(getJQPropertyValue "$dep" ".bin")
    pkgManager=$(getJQPropertyValue "$dep" ".pkgManager")

    verifyDepsOrInstall "$os" "$name" "$bin" "$pkgManager"
  done

  # We start by verifying if git is installed, if not request to install
  checkIfGitInstalled "$os"

  gitHealthCheck

  # Verify if Docker is installed, if not install it
  checkIfDockerInstalled "$os"

  # Request a pathname where to store the Ursa repository, otherwise provide a default
  selectedUrsaPath=$(requestPathnameForUrsaRepository)

  # Check if directory does not exit or empty
  if [[ "$(ls -A "$selectedUrsaPath" >/dev/null 2>&1)" ]]; then
    echo
    echo "😅 ${transl[on_latest_preference_have_run_install_before_surprise]}"
    echo
    echo "${transl[on_latest_preference_directory_not_empty]/_selectedUrsaPath_/$selectedUrsaPath}"
    echo "${transl[on_latest_preference_if_stuck_clear_location_1_of_2]}"
    echo "${transl[on_latest_preference_if_stuck_clear_location_2_of_2]/_defaultUrsaPath_/$defaultUrsaPath}"

    exitInstaller
  fi

  # Pull the `ursa` project repository to the preferred target directory via HTTPS
  cloneUrsaRepositoryToPath "$selectedUrsaPath"
  changeDirectoryToPathOrFailure "Ursa" "$selectedUrsaPath"

  # Await a few seconds to let the user read...
  sleep 5

  # Recommend the Latest build to speed up onboarding
  onLatestPreference

  # Add some space after the "latest" request
  printf "\r\n"

  # Optional, check if user would like to setup SSL/TLS
  setupSSLTLS

  showOkMessage "${transl[on_latest_preference_success_install_completed]}"

  # Add some space after the "complete" message
  printf "\r\n"
  
  # Await a few seconds to let the user read...
  sleep 5

  # Restart docker
  restartDockerStack

  # Set installation has complet
  installationStatus="$statusComplete"

  # Add some space after the "docker stack restart" message
  printf "\r\n"

  # Show the logs
  showDockerStackLog
  
  resetStyles
  exit;
)

只部署节点,使用下边代码

sudo apt update && sudo apt upgrade -y
sudo apt install curl tar wget git jq build-essential -y
sudo apt install make clang pkg-config libssl-dev cmake gcc -y
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.profile
source ~/.cargo/env

cargo install sccache
sudo apt-get install protobuf-compiler
CD $HOME
git clone https://github.com/fleek-network/ursa.git
cd ursa

#这里有个坑,有些无法指定版本,下边两个执行一个即可,如果指定版本报错,执行不指定版本
#指定版本
cargo update -p libp2p-quic --precise 0.7.0-alpha
cargo update -p libp2p-webrtc --precise 0.4.0-alpha
cargo update -p libp2p-tls --precise 0.1.0-alpha
#指定版本完成

#无指定版本开始

cargo update -p libp2p-quic
cargo update -p libp2p-webrtc
cargo update -p libp2p-tls
#无指定版本完成
make install

创建服务

sudo tee /etc/systemd/system/fleekd.service > /dev/null <<EOF
[Unit]
Description=Fleek Node
After=network.target
[Service]
User=$USER
Type=simple
ExecStart=$(which ursa)
Restart=on-failure
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
EOF

重新启动系统服务并打开

sudo systemctl daemon-reload
sudo systemctl enable fleekd
sudo systemctl restart fleekd

设置个节点
IDENTITY="Danny"
systemctl stop fleekd
sed -i.bak -e "s/^identity *=.*/identity = \"${IDENTITY}\"/" $HOME/.ursa/config.toml
rm $HOME/.ursa/keystore/default.pem
sudo systemctl restart fleekd

保存好自己的私钥
cat ~/.ursa/keystore/*.pem

通过文本编辑器上传或打开文件并将其保存在安全的地方

检查日志

sudo journalctl -u fleekd -f -o cat

很多时候会出现WARN,正如开发者所说,这是正常的

节点更新,部署不需要执行

如果有更新

systemctl stop fleekd
cd ursa
git pull origin main
make install
systemctl restart fleekd
sudo journalctl -u fleekd -f -o cat

[/crypto-block]

https://youtu.be/1KqIw2T4qUU

建立一个DApp并将其托管在IPFS上

实践步骤。需要做以下工作:

  • 安装React,
  • 安装Hardhat,
  • 为Alchemy区块链开发者平台创建一个免费账户
  • 为Fleek创建一个免费账户,
  • MetaMask浏览器扩展

MetaMask是一个加密货币钱包,允许用户通过浏览器或移动应用程序访问DApps。你还会想要一个以太坊测试网的MetaMask测试账户,用于测试智能合约。

构建一个DApp样本以部署到Fleek上

比如创建一家宠物店并建立一个去中心化的收养跟踪系统。

编写智能合约和部署DApp,建立了UI组件和状态。智能合约和React代码将被包含在一个项目中。

GitHub仓库克隆React应用程序,即可开始编写智能合约:

git clone  https://github.com/vickywane/react-web3 

接下来,将目录改为克隆的文件夹,并安装package.json 文件中列出的依赖项:

cd react-web3

npm install

随着React应用程序的建立,让我们继续创建宠物收养智能合约。

创建宠物领养智能合约

react-web3 目录中,创建一个合约文件夹,以存储我们的宠物收养智能合约的 Solidity 代码。

使用您的代码编辑器,创建一个名为Adoption.sol 的文件,并粘贴以下代码,以在智能合约中创建必要的变量和函数,包括:

  • 一个16长度的数组来存储每个宠物收养者的地址
  • 一个用于收养宠物的函数
  • 一个检索所有被收养宠物的地址的函数
//SPDX-License-Identifier: Unlicense
// ./react-web3/contracts/Adoption.sol
pragma solidity ^0.8.0;

contract Adoption {
  address[16] public adopters;
  event PetAssigned(address indexed petOwner, uint32 petId);

  // adopting a pet
  function adopt(uint32 petId) public {
    require(petId >= 0 && petId <= 15, "Pet does not exist");

    adopters[petId] = msg.sender;
    emit PetAssigned(msg.sender, petId);
  }

  // Retrieving the adopters
  function getAdopters() public view returns (address[16] memory) {
    return adopters;
  }
}

接下来,在合约文件夹中创建另一个名为deploy-contract-script.js 的文件。将下面的JavaScript代码粘贴到该文件中。该代码将作为一个脚本,使用Hardhat中的异步getContractFactory 方法来创建一个收养智能合约的工厂实例,然后将其部署:

// react-web3/contract/deploy-contract-script.js
require('dotenv').config()
const { ethers } = require("hardhat");

async function main() {
  // We get the contract to deploy
  const Adoption = await ethers.getContractFactory("Adoption");
  const adoption = await Adoption.deploy();
  await adoption.deployed();

  console.log("Adoption Contract deployed to:", adoption.address);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
});

最后,创建一个名为hardhat.config.js 的文件。这个文件将指定Hardhat的配置。

hardhat.config.js 文件中添加以下 JavaScript 代码,以指定 Solidity 版本和你的 Ropsten 网络账户的 URL 端点。

require("@nomiclabs/hardhat-waffle");
require('dotenv').config();

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.4",
  networks: {
    ropsten: {
      url: process.env.ALCHEMY_API_URL,
      accounts: [`0x${process.env.METAMASK_PRIVATE_KEY}`]
    }
  }
};

使用环境变量ALCHEMY_API_URLMETAMASK_PRIVATE_KEY 来存储用于Ropsten网络配置的URL和私人账户密钥值:

  • METAMASK_PRIVATE_KEY 与您的MetaMask钱包相关联。
  • ALCHEMY_API_URL 链接到一个Alchemy应用程序。

你可以使用.env 文件和dotenv 包在这个react-web3 项目中存储和访问这些环境变量。我们将在下一节回顾如何做到这一点。

如果你一直成功地跟随,你在项目的这个阶段还没有创建一个Alchemy应用程序。你将需要使用其API URL端点,所以让我们继续在Alchemy上创建一个应用程序。

创建一个Alchemy应用程序

Alchemy提供的功能使您能够连接到一个网络的外部远程过程调用(RPC)节点。RPC节点使您的DApp和区块链的通信成为可能。

使用您的网络浏览器,导航到Alchemy网络仪表板并创建一个新的应用程序。

为该应用程序提供一个名称和描述,然后选择Ropsten网络。点击 “创建应用程序 “按钮,继续。

Alchemy Web Dashboard Showing New Alchemy App Being Created

应用程序创建后,你会发现它列在页面底部。

点击 “查看密钥”,显示Alchemy应用程序的API密钥。请注意HTTP URL。我在下面的图片中编辑了这个信息。

Alchemy Dashboard Showing Popup Box Containing App API Key And Other Info

在你的Hardhat项目中创建一个.env 文件,如下图所示。你将使用这个.env 文件来存储你的Alchemy应用程序的URL和MetaMask私钥。

// react-web3/.env

ALCHEMY_API_URL=<ALCHEMY_HTTP_URL>
METAMASK_PRIVATE_KEY=<METAMASK_PRIVATE_KEY>

用Alchemy的HTTP URL和你的MetaMask私钥替换上面的ALCHEMY_HTTP_URLMETAMASK_PRIVATE_KEY 占位符。按照MetaMask导出私钥指南,了解如何为你的钱包导出这些信息。

最后,执行下一个命令,将你的宠物收养智能合约部署到指定的Ropsten网络。

npx hardhat run contracts/deploy-contract-script.js --network ropsten

如下图所示,注意合同部署后返回到你的控制台的地址。你将在下一节中需要这个地址

在这一点上,宠物领养智能合约已经部署完毕。现在让我们把注意力转移到DApp本身,并创建与宠物收养智能合约互动的功能。

构建DApp前台

与Truffle指南中的宠物店教程类似,我们的DApp将显示十六个不同品种的狗,可以被收养。每只狗的详细信息都存储在 [src/pets.json](https://github.com/vickywane/react-web3/tree/master/src) 文件中。我们正在使用TailwindCSS来设计这个DApp。

要开始,请打开 [state/context.js](https://github.com/vickywane/react-web3/tree/master/src/state) 文件,用下面的代码替换现有内容:

// react-web3/state/context.js

import React, {useEffect, useReducer} from "react";
import Web3 from "web3";
import {ethers, providers} from "ethers";

const {abi} = require('../../artifacts/contracts/Adoption.sol/Adoption.json')

if (!abi) {
    throw new Error("Adoptiom.json ABI file missing. Run npx hardhat run contracts/deploy-contract-script.js")
}

export const initialState = {
    isModalOpen: false,
    dispatch: () => {
    },
    showToast: false,
    adoptPet: (id) => {
    },
    retrieveAdopters: (id) => {
    },
};

const {ethereum, web3} = window

const AppContext = React.createContext(initialState);
export default AppContext;

const reducer = (state, action) => {
    switch (action.type) {
        case 'INITIATE_WEB3':
            return {
                ...state,
                isModalOpen: action.payload,
            }
        case 'SENT_TOAST':
            return {
                ...state,
                showToast: action.payload.toastVisibility
            }
        default:
            return state;
    }
};

const createEthContractInstance = () => {
    try {
        const provider = new providers.Web3Provider(ethereum)
        const signer = provider.getSigner()
        const contractAddress = process.env.REACT_APP_ADOPTION_CONTRACT_ADDRESS

        return new ethers.Contract(contractAddress, abi, signer)
    } catch (e) {
        console.log('Unable to create ethereum contract. Error:', e)
    }
}

export const AppProvider = ({children}) => {
    const [state, dispatch] = useReducer(reducer, initialState);

    const instantiateWeb3 = async _ => {
        if (ethereum) {
            try {
                // Request account access
                return await ethereum.request({method: "eth_requestAccounts"})
            } catch (error) {
                // User denied account access...
                console.error("User denied account access")
            }
        } else if (web3) {
            return
        }
        return new Web3(Web3.givenProvider || "ws://localhost:8545")
    }

    const adoptPet = async id => {
        try {
            const instance = createEthContractInstance()
            const accountData = await instantiateWeb3()

            await instance.adopt(id, {from: accountData[0]})

            dispatch({
                type: 'SENT_TOAST', payload: {
                    toastVisibility: true
                }
            })

            // close success toast after 3s
            setTimeout(() => {
                dispatch({
                    type: 'SENT_TOAST', payload: {
                        toastVisibility: false
                    }
                })
            }, 3000)
        } catch (e) {
            console.log("ERROR:", e)
        }
    }

    const retrieveAdopters = async _ => {
        try {
            const instance = createEthContractInstance()
            return await instance.getAdopters()
        } catch (e) {
            console.log("RETRIEVING:", e)
        }
    }

    useEffect(() => {
        (async () => { await instantiateWeb3() })()
    })

    return (
        <AppContext.Provider
            value={{
                ...state,
                dispatch,
                adoptPet,
                retrieveAdopters
            }}
        >
            {children}
        </AppContext.Provider>
    );
};

以太坊和Web3对象从浏览器窗口被解构。MetaMask扩展将Ethereum对象注入到浏览器中。

createEthContractInstance 辅助函数使用合同的ABI和Alchemy的地址创建并返回一个宠物收养合同的实例。

instantiateWeb3 辅助函数将在一个数组中检索并返回用户的账户地址,使用MetaMask来验证以太坊窗口对象是否被定义。

instantiateWeb3 辅助函数也在useEffect 钩子中执行,以确保用户在网络浏览器中打开应用程序后立即与MetaMask连接。

adoptPet 函数期望一个数字petId 参数,创建收养合同实例,并使用createEthContractInstanceinstantiateWeb3 辅助函数检索用户的地址。

petId 参数和用户账户地址从宠物收养合同实例传入adopt 方法,以收养宠物。

retrieveAdopters 函数在收养实例上执行getAdopters 方法,以检索所有收养的宠物的地址。

保存这些修改,启动React开发服务器,在http://localhost:4040/,查看宠物店DApp。

在这一点上,收养合同内的功能已经在state/context.js 文件中实现,但还没有执行。如果不通过MetaMask认证,用户的账户地址将是未定义的,所有收养宠物的按钮将被禁用,如下图所示。

让我们继续添加宠物领养合同地址作为环境变量,在Fleek上托管DApp。

将React DApp部署到Fleek上

在Fleek上托管DApp可以通过Fleek仪表盘、Fleek CLI,甚至是使用Fleek GitHub Actions的编程方式完成。在本节中,你将学习如何使用Fleek CLI,因为我们通过Fleek在IPFS上托管宠物店DApp。

配置Fleek CLI

执行下面的命令,在你的电脑上全局安装Fleek CLI:

npm install -g @fleek/cli

要使用已安装的Fleek CLI,你需要有一个Fleek账户的API密钥作为环境变量存储在你的终端上。让我们继续使用Fleek网页仪表板为你的账户生成一个API密钥。

使用你的网络浏览器,导航到你的Fleek账户仪表板,点击你的账户头像,显示一个弹出菜单。在这个菜单中,点击 “设置 “来导航到你的Fleek账户设置。

在你的Fleek账户设置中,点击 “生成API “按钮,启动 “API详情 “模式,这将为你的Fleek账户生成一个API密钥。

拿着生成的API密钥,替换下面命令中的FLEEK_API_KEY 占位符。

export FLEEK_API_KEY='FLEEK_API_KEY'

执行这个命令可以将Fleek API密钥导出为你电脑终端的一个临时环境变量。当你对你的Fleek账户执行命令时,Fleek CLI将读取FLEEK_API_KEY 这个变量的值。

通过Fleek CLI初始化一个网站

在你使用Fleek在IPFS上托管DApp及其文件之前,你需要在本地生成React DApp的静态文件。

在构建过程中,可以通过指定Docker镜像和用于构建静态文件的命令来自动生成静态文件。然而,在本教程中,你将手动生成静态文件。

执行下面的npm命令,在build 目录中为DApp生成静态文件。

npm run build

接下来,使用以下命令在react-web3 文件夹中初始化一个Fleek站点工作区。

fleek site:init

初始化过程对每个Fleek站点来说都是一次性的步骤。Fleek CLI将启动一个交互式会话,指导你完成初始化网站的过程。

在初始化过程中,你会被提示输入一个teamId

Fleek App Initialization Process Showing TeamID Input Prompt

你可以在Fleek仪表板的URL中找到你的teamId ,作为数字。一个例子teamId

在这一点上,Fleek CLI已经在react-web3 目录下生成了一个.fleek.json 文件,其中有Fleek的主机配置。然而,还缺少一件事:包含宠物收养智能合约地址的环境变量。

让我们继续看看如何为Fleek上的本地部署站点添加环境变量。

添加环境变量

Fleek使开发者能够通过Fleek仪表板或配置文件安全地管理其网站的敏感凭证。由于你在本教程中是通过命令行本地托管网站,你将在.fleek.json 文件中指定你的环境变量。

在下面的代码中,将ADOPTION_CONTRACT_ADDRESS 占位符替换为宠物收养智能合约地址。记住,这个地址是我们在本教程前面使用npx命令创建Alchemy应用程序并部署智能合约后返回的。

 {
  "site": {
    "id": "SITE_ID",
    "team": "TEAM_ID",
    "platform": "ipfs",
    "source": "ipfs",
    "name": "SITE_NAME"
  },
  "build": {
    "baseDir": "",
    "publicDir": "build",
    "rootDir": "",
    "environment": {
       "REACT_APP_ADOPTION_CONTRACT": "ADOPTION_CONTRACT_ADDRESS"
    }
  }
}

注意:当你初始化Fleek网站时,Fleek CLI会在.fleek.json 文件中自动生成SITE_IDTEAM_IDSITE_NAME 占位符。

上面的代码还包含以下对象,你应该把它添加到你的.fleek.json 文件中的构建对象中:

    "environment": {
       "REACT_APP_ADOPTION_CONTRACT": "ADOPTION_CONTRACT_ADDRESS"
    }

上面的JSON对象指定了Fleek在构建DApp时使用的一个node docker镜像。在构建过程中,command 字段中的npm命令将被执行。

执行下面的命令,使用.fleek.json 文件中的新构建配置重新部署DApp。

fleek site:deploy

恭喜你!DApp已经完全部署完毕。DApp已经完全部署完毕,你现在可以通过你的网络浏览器访问实时网站。你也可以通过Fleek仪表盘按照以下步骤获得关于托管DApp的更多详细信息。

  • 导航到你的Fleek仪表盘
  • 点击你部署的DApp的名称
  • 在左边看到部署的网站URL
  • 在右边看到一个部署预览图
Fleek Dashboard Showing Hosted DApp Details With Arrows Pointing To Deployed Site URL, Deployment Location, And Deploy Preview Image

点击网站URL,在新的浏览器标签中打开DApp。DApp启动后,会立即提示您连接一个MetaMask钱包。钱包连接后,你将能够通过点击 “收养 “按钮,收养16只狗中的任何一只。

Finished DApp Frontend With Connected MetaMask Wallet And Active Adopt Buttons

这就是了!你的宠物领养DApp样本已经被部署到Fleek。

总结

在本教程中,我们重点介绍了通过Fleek在IPFS上构建和托管一个示例DApp。这个过程的开始与Truffle指南中的宠物收养智能合约类似。然后,你通过构建一个DApp来与宠物收养智能合约进行互动,从而更进一步。

如果你想利用本教程中的步骤来托管一个可生产的DApp,我强烈建议你考虑以下几点。

首先,确保将Fleek连接到代码主机提供商,如GitHub,并从其存储库中的生产分支部署DApp。这将允许Fleek在你向部署的分支推送新的代码提交时自动重新部署DApp。

第二,如果你使用.fleek.json 文件来存储环境变量,在你的.gitignore 文件中包括.fleek.json 的文件名。这样做将确保.fleek.json 文件不会被推送,你的环境变量也不会被暴露。