blockbook/build/tools/templates.go
Vladyslav Burzakovskyy f6111af5da Add fiat rates functionality (#316)
* Add initial commit for fiat rates functionality

* templates.go: use bash from current user's environment

* bitcoinrpc.go: add FiatRates and FiatRatesParams to config

* blockbook.go: add initFiatRatesDownloader kickoff

* bitcoin.json: add coingecko API URL

* rockdb.go: add FindTicker and StoreTicker functions

* rocksdb_test.go: add a simple test for storing and getting FiatRate tickers

* rocksdb: add FindLastTicker and convertDate, make FindTicker return strings

* rocksdb: add ConvertDate function and CoinGeckoTicker struct, update tests

* blockbook.go, fiat: finalize the CoinGecko downloader

* coingecko.go: do not stop syncing when encountered an error

* rocksdb_test: fix the exported function name

* worker.go: make getBlockInfoFromBlockID a public function

* public.go: apiTickers kickoff

* rocksdb_test: fix the unittest comment

* coingecko.go: update comments

* blockbook.go, fiat: reword CoinGecko -> FiatRates, fix binary search upper bound, remove assignment of goroutine call result

* rename coingecko -> fiat_rates

* fiat_rates: export only the necessary methods

* blockbook.go: update log message

* bitcoinrpc.go: remove fiatRates settings

* use CurrencyRatesTicker structure everywhere, fix time format string, update tests, use UTC time

* add /api/v2/tickers tests, store rates as strings (json.Number)

* fiat_rates: add more tests, metrics and tickers-list endpoint, make the "currency" parameter mandatory

* public, worker: move FiatRates API logic to worker.go

* fiat_rates: add a future date test, fix comments, add more checks, store time as a pointer

* rocksdb_test: remove unneeded code

* fiat_rates: add a "ping" call to check server availability

* fiat_rates: do not return empty ticker, return nil instead if not found

add a test for non-existent ticker

* rocksdb_test: remove Sleep from tests

* worker.go: do not propagate all API errors to the client

* move InitTestFiatRates from rocksdb.go to public_test.go

* public.go: fix FiatRatesFindLastTicker result check

* fiat_rates: mock API server responses

* remove commented-out code

* fiat_rates: add comment explaining what periodSeconds attribute is used for

* websocket.go: implement fiatRates websocket endpoints & add tests

* fiatRates: add getFiatRatesTickersList websocket endpoint & test

* fiatRates: make websocket getFiatRatesByDate accept an array of dates, add more tests

* fiatRates: remove getFiatRatesForBlockID from websocket endpoints

* fiatRates: remove "if test", use custom startTime instead

Update tests and mock data

* fiatRates: finalize websocket functionality

add "date" parameter to TickerList

return data timestamps where needed

fix sync bugs (nil timestamp, duplicate save)

* fiatRates: add FiatRates configs for different coins

* worker.go: make GetBlockInfoFromBlockID private again

* fiatRates: wait & retry on errors, remove Ping function

* websocket.go: remove incorrect comment

* fiatRates: move coingecko-related code to a separate file, use interface

* fiatRates: if the new rates are the same as previous, try five more times, and only then store them

* coingecko: fix getting actual rates, add a timestamp parameter to get uncached responses

* vertcoin_testnet.json: remove fiat rates parameters

* fiat_rates: add timestamp to log message about skipping the repeating rates
2019-12-17 10:40:02 +01:00

336 lines
9.4 KiB
Go

package build
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"text/template"
"time"
)
type Config struct {
Coin struct {
Name string `json:"name"`
Shortcut string `json:"shortcut"`
Label string `json:"label"`
Alias string `json:"alias"`
} `json:"coin"`
Ports struct {
BackendRPC int `json:"backend_rpc"`
BackendMessageQueue int `json:"backend_message_queue"`
BlockbookInternal int `json:"blockbook_internal"`
BlockbookPublic int `json:"blockbook_public"`
} `json:"ports"`
IPC struct {
RPCURLTemplate string `json:"rpc_url_template"`
RPCUser string `json:"rpc_user"`
RPCPass string `json:"rpc_pass"`
RPCTimeout int `json:"rpc_timeout"`
MessageQueueBindingTemplate string `json:"message_queue_binding_template"`
} `json:"ipc"`
Backend struct {
PackageName string `json:"package_name"`
PackageRevision string `json:"package_revision"`
SystemUser string `json:"system_user"`
Version string `json:"version"`
BinaryURL string `json:"binary_url"`
VerificationType string `json:"verification_type"`
VerificationSource string `json:"verification_source"`
ExtractCommand string `json:"extract_command"`
ExcludeFiles []string `json:"exclude_files"`
ExecCommandTemplate string `json:"exec_command_template"`
LogrotateFilesTemplate string `json:"logrotate_files_template"`
PostinstScriptTemplate string `json:"postinst_script_template"`
ServiceType string `json:"service_type"`
ServiceAdditionalParamsTemplate string `json:"service_additional_params_template"`
ProtectMemory bool `json:"protect_memory"`
Mainnet bool `json:"mainnet"`
ServerConfigFile string `json:"server_config_file"`
ClientConfigFile string `json:"client_config_file"`
AdditionalParams interface{} `json:"additional_params,omitempty"`
} `json:"backend"`
Blockbook struct {
PackageName string `json:"package_name"`
SystemUser string `json:"system_user"`
InternalBindingTemplate string `json:"internal_binding_template"`
PublicBindingTemplate string `json:"public_binding_template"`
ExplorerURL string `json:"explorer_url"`
AdditionalParams string `json:"additional_params"`
BlockChain struct {
Parse bool `json:"parse,omitempty"`
Subversion string `json:"subversion,omitempty"`
AddressFormat string `json:"address_format,omitempty"`
MempoolWorkers int `json:"mempool_workers"`
MempoolSubWorkers int `json:"mempool_sub_workers"`
BlockAddressesToKeep int `json:"block_addresses_to_keep"`
XPubMagic uint32 `json:"xpub_magic,omitempty"`
XPubMagicSegwitP2sh uint32 `json:"xpub_magic_segwit_p2sh,omitempty"`
XPubMagicSegwitNative uint32 `json:"xpub_magic_segwit_native,omitempty"`
Slip44 uint32 `json:"slip44,omitempty"`
AdditionalParams map[string]json.RawMessage `json:"additional_params"`
} `json:"block_chain"`
} `json:"blockbook"`
Meta struct {
BuildDatetime string `json:"-"` // generated field
PackageMaintainer string `json:"package_maintainer"`
PackageMaintainerEmail string `json:"package_maintainer_email"`
} `json:"meta"`
Env struct {
Version string `json:"version"`
BackendInstallPath string `json:"backend_install_path"`
BackendDataPath string `json:"backend_data_path"`
BlockbookInstallPath string `json:"blockbook_install_path"`
BlockbookDataPath string `json:"blockbook_data_path"`
} `json:"-"`
}
func jsonToString(msg json.RawMessage) (string, error) {
d, err := msg.MarshalJSON()
if err != nil {
return "", err
}
return string(d), nil
}
func generateRPCAuth(user, pass string) (string, error) {
cmd := exec.Command("/usr/bin/env", "bash", "-c", "build/scripts/rpcauth.py \"$0\" \"$1\" | sed -n -e 2p", user, pass)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", err
}
return out.String(), nil
}
func (c *Config) ParseTemplate() *template.Template {
templates := map[string]string{
"IPC.RPCURLTemplate": c.IPC.RPCURLTemplate,
"IPC.MessageQueueBindingTemplate": c.IPC.MessageQueueBindingTemplate,
"Backend.ExecCommandTemplate": c.Backend.ExecCommandTemplate,
"Backend.LogrotateFilesTemplate": c.Backend.LogrotateFilesTemplate,
"Backend.PostinstScriptTemplate": c.Backend.PostinstScriptTemplate,
"Backend.ServiceAdditionalParamsTemplate": c.Backend.ServiceAdditionalParamsTemplate,
"Blockbook.InternalBindingTemplate": c.Blockbook.InternalBindingTemplate,
"Blockbook.PublicBindingTemplate": c.Blockbook.PublicBindingTemplate,
}
funcMap := template.FuncMap{
"jsonToString": jsonToString,
"generateRPCAuth": generateRPCAuth,
}
t := template.New("").Funcs(funcMap)
for name, def := range templates {
t = template.Must(t.Parse(fmt.Sprintf(`{{define "%s"}}%s{{end}}`, name, def)))
}
return t
}
func LoadConfig(configsDir, coin string) (*Config, error) {
config := new(Config)
f, err := os.Open(filepath.Join(configsDir, "coins", coin+".json"))
if err != nil {
return nil, err
}
d := json.NewDecoder(f)
err = d.Decode(config)
if err != nil {
return nil, err
}
f, err = os.Open(filepath.Join(configsDir, "environ.json"))
if err != nil {
return nil, err
}
d = json.NewDecoder(f)
err = d.Decode(&config.Env)
if err != nil {
return nil, err
}
config.Meta.BuildDatetime = time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700")
if !isEmpty(config, "backend") {
switch config.Backend.ServiceType {
case "forking":
case "simple":
default:
return nil, fmt.Errorf("Invalid service type: %s", config.Backend.ServiceType)
}
switch config.Backend.VerificationType {
case "":
case "gpg":
case "sha256":
case "gpg-sha256":
default:
return nil, fmt.Errorf("Invalid verification type: %s", config.Backend.VerificationType)
}
}
return config, nil
}
func isEmpty(config *Config, target string) bool {
switch target {
case "backend":
return config.Backend.PackageName == ""
case "blockbook":
return config.Blockbook.PackageName == ""
default:
panic("Invalid target name: " + target)
}
}
func GeneratePackageDefinitions(config *Config, templateDir, outputDir string) error {
templ := config.ParseTemplate()
err := makeOutputDir(outputDir)
if err != nil {
return err
}
for _, subdir := range []string{"backend", "blockbook"} {
if isEmpty(config, subdir) {
continue
}
root := filepath.Join(templateDir, subdir)
err = os.Mkdir(filepath.Join(outputDir, subdir), 0755)
if err != nil {
return err
}
err = filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return fmt.Errorf("%s: %s", path, err)
}
if path == root {
return nil
}
if filepath.Base(path)[0] == '.' {
return nil
}
subpath := path[len(root)-len(subdir):]
if info.IsDir() {
err = os.Mkdir(filepath.Join(outputDir, subpath), info.Mode())
if err != nil {
return fmt.Errorf("%s: %s", path, err)
}
return nil
}
t := template.Must(templ.Clone())
t = template.Must(t.ParseFiles(path))
err = writeTemplate(filepath.Join(outputDir, subpath), info, t, config)
if err != nil {
return fmt.Errorf("%s: %s", path, err)
}
return nil
})
if err != nil {
return err
}
}
if !isEmpty(config, "backend") {
err = writeBackendServerConfigFile(config, outputDir)
if err == nil {
err = writeBackendClientConfigFile(config, outputDir)
}
if err != nil {
return err
}
}
return nil
}
func makeOutputDir(path string) error {
err := os.RemoveAll(path)
if err == nil {
err = os.Mkdir(path, 0755)
}
return err
}
func writeTemplate(path string, info os.FileInfo, templ *template.Template, config *Config) error {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
defer f.Close()
err = templ.ExecuteTemplate(f, "main", config)
if err != nil {
return err
}
return nil
}
func writeBackendServerConfigFile(config *Config, outputDir string) error {
out, err := os.OpenFile(filepath.Join(outputDir, "backend/server.conf"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer out.Close()
if config.Backend.ServerConfigFile == "" {
return nil
} else {
in, err := os.Open(filepath.Join(outputDir, "backend/config", config.Backend.ServerConfigFile))
if err != nil {
return err
}
defer in.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
}
return nil
}
func writeBackendClientConfigFile(config *Config, outputDir string) error {
out, err := os.OpenFile(filepath.Join(outputDir, "backend/client.conf"), os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer out.Close()
if config.Backend.ClientConfigFile == "" {
return nil
} else {
in, err := os.Open(filepath.Join(outputDir, "backend/config", config.Backend.ClientConfigFile))
if err != nil {
return err
}
defer in.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
}
return nil
}