From 9ed97c6d7758fc777f656628d031b71336f7335a Mon Sep 17 00:00:00 2001 From: Igor Date: Fri, 25 Nov 2022 14:57:38 +0300 Subject: [PATCH 1/3] add auth func --- fix/golang/auth.go | 158 ++++++++++++++++++++++++++++++++ fix/golang/client.go | 172 +++++++++++++++++++++++++++++++++++ fix/golang/config/config.cfg | 21 +++++ fix/golang/go.mod | 18 ++++ fix/golang/go.sum | 36 ++++++++ 5 files changed, 405 insertions(+) create mode 100644 fix/golang/auth.go create mode 100644 fix/golang/client.go create mode 100644 fix/golang/config/config.cfg create mode 100644 fix/golang/go.mod create mode 100644 fix/golang/go.sum diff --git a/fix/golang/auth.go b/fix/golang/auth.go new file mode 100644 index 0000000..d4acfe4 --- /dev/null +++ b/fix/golang/auth.go @@ -0,0 +1,158 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/binary" + "fmt" + "strconv" + "time" + + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/enum" + "github.com/quickfixgo/quickfix/tag" +) + +const ( + delimiter = 0x01 +) + +type Presign struct { + SendingTime int64 + MsgSeqNum int + SenderCompID string + TargetCompID string + Password string +} + +func SignLogonMsg(msg *quickfix.Message, secret ApiSecret) { + if msg.IsMsgTypeOf(enum.MsgType_LOGON) { + //set passPhrase + msg.Body.SetString(tag.Password, strconv.FormatInt(time.Now().UTC().Unix(), 10)) + + //extract presign struct + presign, err := getMsgPresign(msg) + if err != nil { + fmt.Println(err) + return + } + + //make preSignByte from presign + preSignByte, err := makePresignByte(presign) + if err != nil { + fmt.Println(err) + return + } + + //sign is 64 based sha512(preSignByte) + sign := createSignFromBodyAndSecret(preSignByte, []byte(secret)) + + //sign the logonMessage + msg.Body.SetString(tag.RawData, sign) + } +} + +func getMsgPresign(msg *quickfix.Message) (Presign, error) { + var senderCompID quickfix.FIXString + err := msg.Header.GetField(tag.SenderCompID, &senderCompID) + if err != nil { + return Presign{}, err + } + var targetCompID quickfix.FIXString + err = msg.Header.GetField(tag.TargetCompID, &targetCompID) + if err != nil { + return Presign{}, err + } + var msgSeqNum quickfix.FIXInt + err = msg.Header.GetField(tag.MsgSeqNum, &msgSeqNum) + if err != nil { + return Presign{}, err + } + var sendingTime quickfix.FIXUTCTimestamp + err = msg.Header.GetField(tag.SendingTime, &sendingTime) + if err != nil { + return Presign{}, err + } + + var password quickfix.FIXString + err = msg.Body.GetField(tag.Password, &password) + if err != nil { + return Presign{}, err + } + + return Presign{ + SendingTime: sendingTime.UTC().Unix(), + MsgSeqNum: msgSeqNum.Int(), + SenderCompID: senderCompID.String(), + TargetCompID: targetCompID.String(), + Password: password.String(), + }, nil +} + +func makePresignByte(msg Presign) ([]byte, error) { + presignByte := new(bytes.Buffer) + + //sendingTime + binaryWriteErr := addToPresign(presignByte, msg.SendingTime, true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //msgSeqNum + binaryWriteErr = addToPresign(presignByte, int64(msg.MsgSeqNum), true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //senderCompID + binaryWriteErr = addToPresign(presignByte, msg.SenderCompID, true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //targetCompID + binaryWriteErr = addToPresign(presignByte, msg.TargetCompID, true) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + //password + binaryWriteErr = addToPresign(presignByte, msg.Password, false) + if binaryWriteErr != nil { + return nil, binaryWriteErr + } + + return presignByte.Bytes(), nil +} + +func addToPresign[Field int64 | string](presignByte *bytes.Buffer, field Field, withDelimeter bool) (binaryWriteErr error) { + switch f := any(field).(type) { + case string: + _, binaryWriteErr = presignByte.WriteString(f) + if binaryWriteErr != nil { + return binaryWriteErr + } + default: + binaryWriteErr = binary.Write(presignByte, binary.LittleEndian, f) + if binaryWriteErr != nil { + return binaryWriteErr + } + } + if withDelimeter { + return addDelimiter(presignByte) + } + return nil +} + +func addDelimiter(presignByte *bytes.Buffer) error { + binaryWriteErr := presignByte.WriteByte(delimiter) + if binaryWriteErr != nil { + return binaryWriteErr + } + return nil +} + +func createSignFromBodyAndSecret(body, secret []byte) string { + mac := hmac.New(sha512.New, secret) + _, _ = mac.Write(body) + + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} diff --git a/fix/golang/client.go b/fix/golang/client.go new file mode 100644 index 0000000..9ce543f --- /dev/null +++ b/fix/golang/client.go @@ -0,0 +1,172 @@ +// Copyright (c) quickfixengine.org All rights reserved. +// +// This file may be distributed under the terms of the quickfixengine.org +// license as defined by quickfixengine.org and appearing in the file +// LICENSE included in the packaging of this file. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING +// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE. +// +// See http://www.quickfixengine.org/LICENSE for licensing information. +// +// Contact ask@quickfixengine.org if any conditions of this licensing +// are not clear to you. + +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "os/signal" + "path" + "syscall" + + "github.com/fatih/color" + "github.com/pkg/errors" + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/config" + "github.com/quickfixgo/quickfix/enum" +) + +type ( + ApiKey string + ApiSecret string +) + +// Client implements the quickfix.Application interface +type Client struct { + keys map[ApiKey]ApiSecret +} + +// OnCreate implemented as part of Application interface +func (e Client) OnCreate(sessionID quickfix.SessionID) {} + +// OnLogon implemented as part of Application interface +func (e Client) OnLogon(sessionID quickfix.SessionID) {} + +// OnLogout implemented as part of Application interface +func (e Client) OnLogout(sessionID quickfix.SessionID) {} + +// FromAdmin implemented as part of Application interface +func (e Client) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { + if msg.IsMsgTypeOf(enum.MsgType_LOGON) { + log.Printf("api key [%s] is logged on", sessionID.SenderCompID) + } + return nil +} + +// ToAdmin implemented as part of Application interface +func (e Client) ToAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) { + apiKey := ApiKey(sessionID.SenderCompID) + secret, exist := e.keys[apiKey] + if !exist { + log.Fatalf("unknown api-key [%s] in sessionID", apiKey) + } + SignLogonMsg(msg, secret) +} + +// ToApp implemented as part of Application interface +func (e Client) ToApp(msg *quickfix.Message, sessionID quickfix.SessionID) (err error) { + fmt.Printf("Sending %s\n", msg) + return +} + +// FromApp implemented as part of Application interface. This is the callback for all Application level messages from the counter party. +func (e Client) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { + fmt.Printf("FromApp: %s\n", msg.String()) + return +} + +func main() { + err := startClient() + if err != nil { + log.Fatal(err) + } +} + +func startClient() error { + cfgFileName := path.Join("config", "config.cfg") + + cfg, err := os.Open(cfgFileName) + if err != nil { + return fmt.Errorf("Error opening %v, %v\n", cfgFileName, err) + } + defer cfg.Close() + + stringData, readErr := io.ReadAll(cfg) + if readErr != nil { + return fmt.Errorf("error reading cfg: %s", readErr) + } + + appSettings, err := quickfix.ParseSettings(bytes.NewReader(stringData)) + if err != nil { + return fmt.Errorf("error reading cfg: %s", err) + } + + sessions := appSettings.SessionSettings() + apiKeys := make(map[ApiKey]ApiSecret, len(sessions)) + for _, sessionSettings := range sessions { + sci, serr := sessionSettings.Setting(config.SenderCompID) + sec, perr := sessionSettings.Setting("Password") + if serr == nil && perr == nil { + apiKeys[ApiKey(sci)] = ApiSecret(sec) + } + } + app := Client{keys: apiKeys} + screenLogFactory := quickfix.NewScreenLogFactory() + + if err != nil { + return fmt.Errorf("error creating file log factory: %s", err) + } + + initiator, err := quickfix.NewInitiator(app, quickfix.NewMemoryStoreFactory(), appSettings, screenLogFactory) + if err != nil { + return fmt.Errorf("Unable to create Initiator: %s\n", err) + } + + err = initiator.Start() + if err != nil { + return fmt.Errorf("Unable to start Initiator: %s\n", err) + } + + printConfig(bytes.NewReader(stringData)) + + defer initiator.Stop() + awaitTermination() + return nil +} + +func printConfig(reader io.Reader) { + scanner := bufio.NewScanner(reader) + color.Set(color.Bold) + fmt.Println("Started FIX initiator with config:") + color.Unset() + + color.Set(color.FgHiMagenta) + for scanner.Scan() { + line := scanner.Text() + fmt.Println(line) + } + + color.Unset() +} + +func awaitTermination() { + // Listen to interrupt signal + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + defer func() { + stop() + if errors.Is(ctx.Err(), context.Canceled) { + log.Println("FIX initiator is stopped") + return + } + log.Println(ctx.Err()) + }() + <-ctx.Done() +} diff --git a/fix/golang/config/config.cfg b/fix/golang/config/config.cfg new file mode 100644 index 0000000..b9bc995 --- /dev/null +++ b/fix/golang/config/config.cfg @@ -0,0 +1,21 @@ +[DEFAULT] +SocketConnectHost=127.0.0.1 +SocketConnectPort=5001 +HeartBtInt=30 +TargetCompID=EXMO +ResetOnLogon=Y +FileLogPath=tmp + +[SESSION] +BeginString=FIX.4.4 +# this is your Api key obtained from https://exmo.com/profile/api +SenderCompID=K-1a07dcb30e6f6aea5421f5a24a321c5d5b78a952 +# this is your Api secret obtained from https://exmo.com/profile/api +Password=S-1b32ef93597b809a96a44e473a8413aacaaa9ac8 + +[SESSION] +BeginString=FIX.4.4 +# this is your Api key obtained from https://exmo.com/profile/api +SenderCompID=K-c3f69c0233b84eebd659ff58a6118bb2a704a628 +# this is your Api secret obtained from https://exmo.com/profile/api +Password=S-e3ff449f6b5772d41cff26da594679d1beca9631 \ No newline at end of file diff --git a/fix/golang/go.mod b/fix/golang/go.mod new file mode 100644 index 0000000..4d8667c --- /dev/null +++ b/fix/golang/go.mod @@ -0,0 +1,18 @@ +module gitlab.exmoney.com/golang/exmo_api_lib/fix/golang + +go 1.19 + +require ( + github.com/fatih/color v1.13.0 + github.com/pkg/errors v0.9.1 + github.com/quickfixgo/quickfix v0.6.0 +) + +require ( + github.com/mattn/go-colorable v0.1.9 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/stretchr/testify v1.8.1 // indirect + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect +) diff --git a/fix/golang/go.sum b/fix/golang/go.sum new file mode 100644 index 0000000..5443e1d --- /dev/null +++ b/fix/golang/go.sum @@ -0,0 +1,36 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quickfixgo/quickfix v0.6.0 h1:sSUFaKiMVaaFLGgWaK1ZmwFNZeQ0/awu+IzEu3cJWJE= +github.com/quickfixgo/quickfix v0.6.0/go.mod h1:RuN5MIPnzolPNDYibgBXHhgMoTEjjPzcCN3rLFcODS4= +github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 509f784991c8b597a37f17706a02b91dfecba9fd Mon Sep 17 00:00:00 2001 From: Aleksey Mikhaylov Date: Wed, 7 Dec 2022 17:07:41 +0300 Subject: [PATCH 2/3] golang client example for fix protocol --- .github/dependabot.yml | 20 +++ .github/workflows/bump-version.yml | 23 +++ .github/workflows/ci.yaml | 50 ++++++ .github/workflows/codeql-analysis.yml | 75 ++++++++ .gitignore | 1 + README.md | 26 +-- fix/golang/.golangci.yml | 102 +++++++++++ fix/golang/Makefile | 17 ++ fix/golang/auth.go | 76 ++++---- fix/golang/client.go | 172 ------------------- fix/golang/config/{config.cfg => config.ini} | 3 +- fix/golang/go.mod | 10 +- fix/golang/go.sum | 8 +- fix/golang/main.go | 155 +++++++++++++++++ fix/golang/quickfixapp.go | 105 +++++++++++ fix/golang/routes.go | 67 ++++++++ fix/golang/scenario.go | 157 +++++++++++++++++ fix/golang/util.go | 5 + 18 files changed, 852 insertions(+), 220 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/bump-version.yml create mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/codeql-analysis.yml create mode 100644 fix/golang/.golangci.yml create mode 100644 fix/golang/Makefile delete mode 100644 fix/golang/client.go rename fix/golang/config/{config.cfg => config.ini} (89%) create mode 100644 fix/golang/main.go create mode 100644 fix/golang/quickfixapp.go create mode 100644 fix/golang/routes.go create mode 100644 fix/golang/scenario.go create mode 100644 fix/golang/util.go diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f6bf1c4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + +# update go modules + - package-ecosystem: "gomod" # See documentation for possible values + directory: "/fix/golang" # Location of package manifests + schedule: + interval: "weekly" + +# Set update schedule for GitHub Actions + - package-ecosystem: "github-actions" + directory: "/fix/golang" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/workflows/bump-version.yml b/.github/workflows/bump-version.yml new file mode 100644 index 0000000..a7b541c --- /dev/null +++ b/.github/workflows/bump-version.yml @@ -0,0 +1,23 @@ +name: Bump version +on: + workflow_run: + workflows: ["CI.build-test-lint"] + branches: + - master + - main + types: + - completed + +jobs: + build: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + # check https://github.com/marketplace/actions/github-tag for creating auto-releases diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..cfb9550 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,50 @@ +name: CI.build-test-lint +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + branches: + - master + - main +permissions: + contents: read + +jobs: + build-test-lint: + permissions: + contents: read # for actions/checkout to fetch code + pull-requests: read # for golangci/golangci-lint-action to fetch pull requests + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + go1_18_project: + - fix/golang + steps: + - name: Checkout source code + uses: actions/checkout@v3 + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: '1.18' + - name: Install golangci-lint + run: | + curl -sSLO https://github.com/golangci/golangci-lint/releases/download/v$GOLANGCI_LINT_VERSION/golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz + shasum -a 256 golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz | grep "^$GOLANGCI_LINT_SHA256 " > /dev/null + tar -xf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64.tar.gz + sudo mv golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64/golangci-lint /usr/local/bin/golangci-lint + rm -rf golangci-lint-$GOLANGCI_LINT_VERSION-linux-amd64* + env: + GOLANGCI_LINT_VERSION: '1.50.1' + GOLANGCI_LINT_SHA256: '4ba1dc9dbdf05b7bdc6f0e04bdfe6f63aa70576f51817be1b2540bbce017b69a' + - name: Build + run: cd ${{ matrix.go1_18_project }} && make build + - name: Test + run: cd ${{ matrix.go1_18_project }} && make test + - name: Lint + run: cd ${{ matrix.go1_18_project }} && make lint + diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..0516b58 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,75 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: + - master + - main + pull_request: + # The branches below must be a subset of the branches above + branches: + - master + - main + schedule: + - cron: '55 05 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # âœī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.gitignore b/.gitignore index 421af61..c73ebe0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist .sass-cache bower_components target +bin diff --git a/README.md b/README.md index e89e54a..5d69761 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,17 @@ https://exmo.com/api_doc ### Supported languages: -- php. -- js. -- nodejs. -- c#. -- c++11. -- python. -- objective c. -- swift. -- java. -- ruby. -- golang. -- R. +| | rest | ws | fix | +|-------------|------------------------|---------------------------------------------------|-------------------| +| php | [yes](rest/php) | | | +| js | [yes](rest/js) | [yes](ws/js) | | +| nodejs | [yes](rest/nodejs) | | | +| c# | [yes](rest/c%23) | [yes](ws/.net) | | +| c++11 | [yes](rest/ҁ++) | | | +| python | [yes](rest/python) | [python2](ws/python2)
[python3](ws/python3) | | +| objective c | [yes](rest/objectivec) | | | +| swift | [yes](rest/swift) | | | +| java | [yes](rest/java) | [yes](ws/java) | | +| ruby | [yes](rest/ruby) | | | +| golang | [yes](rest/golang) | [yes](ws/golang) | [yes](fix/golang) | +| R | [yes](rest/r) | | | diff --git a/fix/golang/.golangci.yml b/fix/golang/.golangci.yml new file mode 100644 index 0000000..09e1646 --- /dev/null +++ b/fix/golang/.golangci.yml @@ -0,0 +1,102 @@ +run: + timeout: 30s + +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + - cyclop + - decorder + - depguard + - dogsled + - dupl + - dupword + - durationcheck + - errchkjson + - errname + - errorlint + - execinquery + - exhaustive + - exhaustruct + - exportloopref + - forbidigo + - forcetypeassert + - funlen +# - gci + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - goerr113 + - gofmt + - gofumpt + - goheader + - goimports + - gomnd + - gomoddirectives + - gomodguard + - goprintffuncname + - gosec + - grouper + - importas + - interfacebloat +# - ireturn + - lll + - loggercheck + - maintidx + - makezero + - misspell + - nakedret + - nestif + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - nosprintfhostport +# - paralleltest + - prealloc + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - sqlclosecheck + - stylecheck + - tagliatelle + - tenv + - testableexamples + - testpackage + - thelper + - tparallel + - unconvert + - unparam + - usestdlibvars + - varnamelen + - wastedassign + - whitespace + - wrapcheck + - wsl + +linters-settings: + gofmt: + simplify: true + goimports: + local-prefixes: gitlab.exmoney.com/golang/exmo_api_lib/fix/golang + diff --git a/fix/golang/Makefile b/fix/golang/Makefile new file mode 100644 index 0000000..84fe8a3 --- /dev/null +++ b/fix/golang/Makefile @@ -0,0 +1,17 @@ + +dep: ; go mod tidy + +.PHONY: run +run: dep ; go run *.go + +.PHONY: all +all: build test lint + +.PHONY: build +build: dep ; go build -o bin/exmo-fix-client ./... + +.PHONY: test +test: dep ; go test ./... + +.PHONY: lint +lint: ; golangci-lint run diff --git a/fix/golang/auth.go b/fix/golang/auth.go index d4acfe4..f9fe860 100644 --- a/fix/golang/auth.go +++ b/fix/golang/auth.go @@ -6,10 +6,11 @@ import ( "crypto/sha512" "encoding/base64" "encoding/binary" - "fmt" + "log" "strconv" "time" + "github.com/pkg/errors" "github.com/quickfixgo/quickfix" "github.com/quickfixgo/quickfix/enum" "github.com/quickfixgo/quickfix/tag" @@ -27,62 +28,72 @@ type Presign struct { Password string } -func SignLogonMsg(msg *quickfix.Message, secret ApiSecret) { +func SignLogonMsg(msg *quickfix.Message, secret APISecret) { if msg.IsMsgTypeOf(enum.MsgType_LOGON) { - //set passPhrase + // set passPhrase msg.Body.SetString(tag.Password, strconv.FormatInt(time.Now().UTC().Unix(), 10)) - //extract presign struct + // extract presign struct presign, err := getMsgPresign(msg) if err != nil { - fmt.Println(err) + log.Println(err) + return } - //make preSignByte from presign + // make preSignByte from presign preSignByte, err := makePresignByte(presign) if err != nil { - fmt.Println(err) + log.Println(err) + return } - //sign is 64 based sha512(preSignByte) + // sign is base64-encoded HMAC-hashed (with sha512-hash, and secret) preSignByte sign := createSignFromBodyAndSecret(preSignByte, []byte(secret)) - //sign the logonMessage + // sign the logonMessage msg.Body.SetString(tag.RawData, sign) } } -func getMsgPresign(msg *quickfix.Message) (Presign, error) { +func getMsgPresign(msg *quickfix.Message) (*Presign, error) { var senderCompID quickfix.FIXString + err := msg.Header.GetField(tag.SenderCompID, &senderCompID) if err != nil { - return Presign{}, err + return nil, err } + var targetCompID quickfix.FIXString + err = msg.Header.GetField(tag.TargetCompID, &targetCompID) if err != nil { - return Presign{}, err + return nil, err } + var msgSeqNum quickfix.FIXInt + err = msg.Header.GetField(tag.MsgSeqNum, &msgSeqNum) if err != nil { - return Presign{}, err + return nil, err } + var sendingTime quickfix.FIXUTCTimestamp + err = msg.Header.GetField(tag.SendingTime, &sendingTime) if err != nil { - return Presign{}, err + return nil, err } var password quickfix.FIXString + err = msg.Body.GetField(tag.Password, &password) if err != nil { - return Presign{}, err + return nil, err } - return Presign{ + return &Presign{ SendingTime: sendingTime.UTC().Unix(), MsgSeqNum: msgSeqNum.Int(), SenderCompID: senderCompID.String(), @@ -91,30 +102,30 @@ func getMsgPresign(msg *quickfix.Message) (Presign, error) { }, nil } -func makePresignByte(msg Presign) ([]byte, error) { +func makePresignByte(msg *Presign) ([]byte, error) { presignByte := new(bytes.Buffer) - //sendingTime + // sendingTime binaryWriteErr := addToPresign(presignByte, msg.SendingTime, true) if binaryWriteErr != nil { return nil, binaryWriteErr } - //msgSeqNum + // msgSeqNum binaryWriteErr = addToPresign(presignByte, int64(msg.MsgSeqNum), true) if binaryWriteErr != nil { return nil, binaryWriteErr } - //senderCompID + // senderCompID binaryWriteErr = addToPresign(presignByte, msg.SenderCompID, true) if binaryWriteErr != nil { return nil, binaryWriteErr } - //targetCompID + // targetCompID binaryWriteErr = addToPresign(presignByte, msg.TargetCompID, true) if binaryWriteErr != nil { return nil, binaryWriteErr } - //password + // password binaryWriteErr = addToPresign(presignByte, msg.Password, false) if binaryWriteErr != nil { return nil, binaryWriteErr @@ -123,30 +134,37 @@ func makePresignByte(msg Presign) ([]byte, error) { return presignByte.Bytes(), nil } -func addToPresign[Field int64 | string](presignByte *bytes.Buffer, field Field, withDelimeter bool) (binaryWriteErr error) { - switch f := any(field).(type) { +func addToPresign[Field int64 | string]( + presignByte *bytes.Buffer, + field Field, + withDelimeter bool, +) error { + switch typedField := any(field).(type) { case string: - _, binaryWriteErr = presignByte.WriteString(f) + _, binaryWriteErr := presignByte.WriteString(typedField) if binaryWriteErr != nil { - return binaryWriteErr + return errors.Wrap(binaryWriteErr, "unable to write presignString") } default: - binaryWriteErr = binary.Write(presignByte, binary.LittleEndian, f) + binaryWriteErr := binary.Write(presignByte, binary.LittleEndian, typedField) if binaryWriteErr != nil { - return binaryWriteErr + return errors.Wrap(binaryWriteErr, "unable to write presignByte") } } + if withDelimeter { return addDelimiter(presignByte) } + return nil } func addDelimiter(presignByte *bytes.Buffer) error { binaryWriteErr := presignByte.WriteByte(delimiter) if binaryWriteErr != nil { - return binaryWriteErr + return errors.Wrap(binaryWriteErr, "unable to write delimiter") } + return nil } diff --git a/fix/golang/client.go b/fix/golang/client.go deleted file mode 100644 index 9ce543f..0000000 --- a/fix/golang/client.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) quickfixengine.org All rights reserved. -// -// This file may be distributed under the terms of the quickfixengine.org -// license as defined by quickfixengine.org and appearing in the file -// LICENSE included in the packaging of this file. -// -// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING -// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A -// PARTICULAR PURPOSE. -// -// See http://www.quickfixengine.org/LICENSE for licensing information. -// -// Contact ask@quickfixengine.org if any conditions of this licensing -// are not clear to you. - -package main - -import ( - "bufio" - "bytes" - "context" - "fmt" - "io" - "log" - "os" - "os/signal" - "path" - "syscall" - - "github.com/fatih/color" - "github.com/pkg/errors" - "github.com/quickfixgo/quickfix" - "github.com/quickfixgo/quickfix/config" - "github.com/quickfixgo/quickfix/enum" -) - -type ( - ApiKey string - ApiSecret string -) - -// Client implements the quickfix.Application interface -type Client struct { - keys map[ApiKey]ApiSecret -} - -// OnCreate implemented as part of Application interface -func (e Client) OnCreate(sessionID quickfix.SessionID) {} - -// OnLogon implemented as part of Application interface -func (e Client) OnLogon(sessionID quickfix.SessionID) {} - -// OnLogout implemented as part of Application interface -func (e Client) OnLogout(sessionID quickfix.SessionID) {} - -// FromAdmin implemented as part of Application interface -func (e Client) FromAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { - if msg.IsMsgTypeOf(enum.MsgType_LOGON) { - log.Printf("api key [%s] is logged on", sessionID.SenderCompID) - } - return nil -} - -// ToAdmin implemented as part of Application interface -func (e Client) ToAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) { - apiKey := ApiKey(sessionID.SenderCompID) - secret, exist := e.keys[apiKey] - if !exist { - log.Fatalf("unknown api-key [%s] in sessionID", apiKey) - } - SignLogonMsg(msg, secret) -} - -// ToApp implemented as part of Application interface -func (e Client) ToApp(msg *quickfix.Message, sessionID quickfix.SessionID) (err error) { - fmt.Printf("Sending %s\n", msg) - return -} - -// FromApp implemented as part of Application interface. This is the callback for all Application level messages from the counter party. -func (e Client) FromApp(msg *quickfix.Message, sessionID quickfix.SessionID) (reject quickfix.MessageRejectError) { - fmt.Printf("FromApp: %s\n", msg.String()) - return -} - -func main() { - err := startClient() - if err != nil { - log.Fatal(err) - } -} - -func startClient() error { - cfgFileName := path.Join("config", "config.cfg") - - cfg, err := os.Open(cfgFileName) - if err != nil { - return fmt.Errorf("Error opening %v, %v\n", cfgFileName, err) - } - defer cfg.Close() - - stringData, readErr := io.ReadAll(cfg) - if readErr != nil { - return fmt.Errorf("error reading cfg: %s", readErr) - } - - appSettings, err := quickfix.ParseSettings(bytes.NewReader(stringData)) - if err != nil { - return fmt.Errorf("error reading cfg: %s", err) - } - - sessions := appSettings.SessionSettings() - apiKeys := make(map[ApiKey]ApiSecret, len(sessions)) - for _, sessionSettings := range sessions { - sci, serr := sessionSettings.Setting(config.SenderCompID) - sec, perr := sessionSettings.Setting("Password") - if serr == nil && perr == nil { - apiKeys[ApiKey(sci)] = ApiSecret(sec) - } - } - app := Client{keys: apiKeys} - screenLogFactory := quickfix.NewScreenLogFactory() - - if err != nil { - return fmt.Errorf("error creating file log factory: %s", err) - } - - initiator, err := quickfix.NewInitiator(app, quickfix.NewMemoryStoreFactory(), appSettings, screenLogFactory) - if err != nil { - return fmt.Errorf("Unable to create Initiator: %s\n", err) - } - - err = initiator.Start() - if err != nil { - return fmt.Errorf("Unable to start Initiator: %s\n", err) - } - - printConfig(bytes.NewReader(stringData)) - - defer initiator.Stop() - awaitTermination() - return nil -} - -func printConfig(reader io.Reader) { - scanner := bufio.NewScanner(reader) - color.Set(color.Bold) - fmt.Println("Started FIX initiator with config:") - color.Unset() - - color.Set(color.FgHiMagenta) - for scanner.Scan() { - line := scanner.Text() - fmt.Println(line) - } - - color.Unset() -} - -func awaitTermination() { - // Listen to interrupt signal - ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) - defer func() { - stop() - if errors.Is(ctx.Err(), context.Canceled) { - log.Println("FIX initiator is stopped") - return - } - log.Println(ctx.Err()) - }() - <-ctx.Done() -} diff --git a/fix/golang/config/config.cfg b/fix/golang/config/config.ini similarity index 89% rename from fix/golang/config/config.cfg rename to fix/golang/config/config.ini index b9bc995..f8b0caf 100644 --- a/fix/golang/config/config.cfg +++ b/fix/golang/config/config.ini @@ -4,6 +4,7 @@ SocketConnectPort=5001 HeartBtInt=30 TargetCompID=EXMO ResetOnLogon=Y +ResetOnDisconnect="Y" FileLogPath=tmp [SESSION] @@ -18,4 +19,4 @@ BeginString=FIX.4.4 # this is your Api key obtained from https://exmo.com/profile/api SenderCompID=K-c3f69c0233b84eebd659ff58a6118bb2a704a628 # this is your Api secret obtained from https://exmo.com/profile/api -Password=S-e3ff449f6b5772d41cff26da594679d1beca9631 \ No newline at end of file +Password=S-e3ff449f6b5772d41cff26da594679d1beca9631 diff --git a/fix/golang/go.mod b/fix/golang/go.mod index 4d8667c..3154f3b 100644 --- a/fix/golang/go.mod +++ b/fix/golang/go.mod @@ -1,18 +1,20 @@ module gitlab.exmoney.com/golang/exmo_api_lib/fix/golang -go 1.19 +go 1.18 require ( github.com/fatih/color v1.13.0 + github.com/mikhalytch/eggs v0.0.37 github.com/pkg/errors v0.9.1 github.com/quickfixgo/quickfix v0.6.0 + github.com/shopspring/decimal v1.3.1 ) require ( github.com/mattn/go-colorable v0.1.9 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-sqlite3 v1.14.16 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/stretchr/testify v1.8.1 // indirect - golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + github.com/stretchr/objx v0.5.0 // indirect + golang.org/x/exp v0.0.0-20221114191408-850992195362 // indirect + golang.org/x/sys v0.1.0 // indirect ) diff --git a/fix/golang/go.sum b/fix/golang/go.sum index 5443e1d..bbee145 100644 --- a/fix/golang/go.sum +++ b/fix/golang/go.sum @@ -10,6 +10,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mikhalytch/eggs v0.0.37 h1:6BtOOz6NzdDHTn/Y9c+QiH2hbb/Yb4wZI/9U/8FJgSs= +github.com/mikhalytch/eggs v0.0.37/go.mod h1:fsVa/VF1bUEU8NSiwdKcc1p/rpv87iBnm2yTHhcJwl8= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -25,11 +27,13 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +golang.org/x/exp v0.0.0-20221114191408-850992195362 h1:NoHlPRbyl1VFI6FjwHtPQCN7wAMXI6cKcqrmXhOOfBQ= +golang.org/x/exp v0.0.0-20221114191408-850992195362/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/fix/golang/main.go b/fix/golang/main.go new file mode 100644 index 0000000..01fce20 --- /dev/null +++ b/fix/golang/main.go @@ -0,0 +1,155 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log" + "os" + "os/signal" + "path" + "syscall" + + "github.com/fatih/color" + "github.com/mikhalytch/eggs/try" + "github.com/pkg/errors" + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/config" +) + +func main() { + if err := main0(); err != nil { + log.Fatalln(err) + } +} + +func main0() error { + sessionsCh := make(chan quickfix.SessionID) + defer close(sessionsCh) + + scenarioFinishedCh := make(chan struct{}) + defer close(scenarioFinishedCh) + + clientCloser, err := startClient(sessionsCh) + if err != nil { + return err + } + + defer clientCloser() + + ctx, ctxCloser := getNotifiedCtx() + defer ctxCloser() + + go func() { + defer func() { scenarioFinishedCh <- struct{}{} }() + + try.Trie(awaitLogon(ctx, sessionsCh)). + Proc(scenario). + ProcFailure(LogPrintlnErr) + }() + + select { + case <-ctx.Done(): + case <-scenarioFinishedCh: + } + + return nil +} + +func startClient(logonCh chan quickfix.SessionID) (func(), error) { + configContents, err := os.ReadFile(path.Join("config", "config.ini")) + if err != nil { + return nil, errors.Wrap(err, "unable to read config") + } + + appSettings, err := quickfix.ParseSettings(bytes.NewReader(configContents)) + if err != nil { + return nil, fmt.Errorf("error reading ini: %w", err) + } + + sessions := appSettings.SessionSettings() + apiKeys := make(map[APIKey]APISecret, len(sessions)) + + for _, sessionSettings := range sessions { + sci, errS := sessionSettings.Setting(config.SenderCompID) + sec, errP := sessionSettings.Setting("Password") + + if errS == nil && errP == nil { + apiKeys[APIKey(sci)] = APISecret(sec) + } + } + + app := NewQuickFixApp(apiKeys, logonCh) + screenLogFactory := quickfix.NewScreenLogFactory() + + if err != nil { + return nil, fmt.Errorf("error creating file log factory: %w", err) + } + + initiator, err := quickfix.NewInitiator(app, quickfix.NewMemoryStoreFactory(), appSettings, screenLogFactory) + if err != nil { + return nil, fmt.Errorf("unable to create Initiator: %w", err) + } + + err = initiator.Start() + if err != nil { + return nil, fmt.Errorf("unable to start Initiator: %w", err) + } + + printConfig(bytes.NewReader(configContents)) + + return initiator.Stop, nil +} + +func printConfig(reader io.Reader) { + scanner := bufio.NewScanner(reader) + + color.Set(color.Bold) + log.Println("Started FIX initiator with config:") + color.Unset() + + color.Set(color.FgHiMagenta) + + for scanner.Scan() { + line := scanner.Text() + log.Println(line) + } + + color.Unset() +} + +func getNotifiedCtx() (context.Context, func()) { + // Listen to interrupt signal + ctx, stop := signal.NotifyContext(context.Background(), + syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) + + return ctx, func() { + stop() + + if errors.Is(ctx.Err(), context.Canceled) { + log.Println("FIX initiator is stopped") + + return + } + + log.Println(ctx.Err()) + } +} + +func awaitLogon(ctx context.Context, sessionsCh chan quickfix.SessionID) (quickfix.SessionID, error) { + log.Println("awaiting logon...") + select { + case <-ctx.Done(): + return quickfix.SessionID{}, errors.Wrap(ctx.Err(), "context was closed while awaiting for logon") + case session, open := <-sessionsCh: + if !open { + return quickfix.SessionID{}, errors.New("no more sessions, closing") + } + + log.Println("logged on:", session) + + return session, nil + } +} diff --git a/fix/golang/quickfixapp.go b/fix/golang/quickfixapp.go new file mode 100644 index 0000000..036e1db --- /dev/null +++ b/fix/golang/quickfixapp.go @@ -0,0 +1,105 @@ +// Copyright (c) quickfixengine.org All rights reserved. +// +// This file may be distributed under the terms of the quickfixengine.org +// license as defined by quickfixengine.org and appearing in the file +// LICENSE included in the packaging of this file. +// +// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING +// THE WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE. +// +// See http://www.quickfixengine.org/LICENSE for licensing information. +// +// Contact ask@quickfixengine.org if any conditions of this licensing +// are not clear to you. + +package main + +import ( + "log" + + "github.com/pkg/errors" + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/enum" +) + +type ( + APIKey string + APISecret string +) + +// QuickFixApp implements the quickfix.Application interface. +type QuickFixApp struct { + *quickfix.MessageRouter + keys map[APIKey]APISecret + logonCh chan quickfix.SessionID +} + +func NewQuickFixApp(keys map[APIKey]APISecret, logonSessionsCh chan quickfix.SessionID) *QuickFixApp { + res := &QuickFixApp{ + MessageRouter: quickfix.NewMessageRouter(), + keys: keys, + logonCh: logonSessionsCh, + } + setupRoutes(res.MessageRouter) + + return res +} + +// OnCreate implemented as part of Application interface. +func (e *QuickFixApp) OnCreate(quickfix.SessionID) {} + +// OnLogon implemented as part of Application interface. +func (e *QuickFixApp) OnLogon(sessionID quickfix.SessionID) { e.logonCh <- sessionID } + +// OnLogout implemented as part of Application interface. +func (e *QuickFixApp) OnLogout(quickfix.SessionID) {} + +// FromAdmin implemented as part of Application interface. +func (e *QuickFixApp) FromAdmin( + msg *quickfix.Message, + sessionID quickfix.SessionID, +) quickfix.MessageRejectError { + if msg.IsMsgTypeOf(enum.MsgType_LOGON) { + log.Printf("api key [%s] is logged on", sessionID.SenderCompID) + } + + return nil +} + +// ToAdmin implemented as part of Application interface. +func (e *QuickFixApp) ToAdmin(msg *quickfix.Message, sessionID quickfix.SessionID) { + apiKey := APIKey(sessionID.SenderCompID) + + secret, exist := e.keys[apiKey] + if !exist { + log.Fatalf("unknown api-key [%s] in sessionID", apiKey) + } + + SignLogonMsg(msg, secret) +} + +// ToApp implemented as part of Application interface. +func (e *QuickFixApp) ToApp(msg *quickfix.Message, _ quickfix.SessionID) error { + log.Printf("Sending %s\n", msg) + + return nil +} + +// FromApp implemented as part of Application interface. +// This is the callback for all Application level messages from the counterparty. +func (e *QuickFixApp) FromApp( + msg *quickfix.Message, + sessionID quickfix.SessionID, +) quickfix.MessageRejectError { + err := e.Route(msg, sessionID) + if err != nil { + if errors.Is(err, quickfix.UnsupportedMessageType()) { + log.Printf("FromApp: %s\n", msg.String()) + } else { + log.Fatalln(err) + } + } + + return nil +} diff --git a/fix/golang/routes.go b/fix/golang/routes.go new file mode 100644 index 0000000..7af993b --- /dev/null +++ b/fix/golang/routes.go @@ -0,0 +1,67 @@ +package main + +import ( + "log" + + "github.com/mikhalytch/eggs/math" + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/fix44/executionreport" + "github.com/quickfixgo/quickfix/fix44/marketdatarequestreject" + "github.com/quickfixgo/quickfix/fix44/marketdatasnapshotfullrefresh" + "github.com/quickfixgo/quickfix/fix44/securitydefinition" + "github.com/quickfixgo/quickfix/fix44/securitylist" +) + +func setupRoutes(router *quickfix.MessageRouter) { + router.AddRoute(securitylist.Route(onSecurityList)) + router.AddRoute(securitydefinition.Route(onSecurityDefinition)) + router.AddRoute(marketdatasnapshotfullrefresh.Route(onMarketData)) + router.AddRoute(marketdatarequestreject.Route(onMarketDataReject)) + router.AddRoute(executionreport.Route(onExecutionReport)) +} + +func onExecutionReport(msg executionreport.ExecutionReport, _ quickfix.SessionID) quickfix.MessageRejectError { + log.Println("execution report received:", limitMsg(msg)) + + return nil +} + +func onMarketDataReject( + msg marketdatarequestreject.MarketDataRequestReject, + _ quickfix.SessionID, +) quickfix.MessageRejectError { + log.Println("market data reject received:", limitMsg(msg)) + + return nil +} + +func onMarketData( + msg marketdatasnapshotfullrefresh.MarketDataSnapshotFullRefresh, + _ quickfix.SessionID, +) quickfix.MessageRejectError { + log.Println("market data received:", limitMsg(msg)) + + return nil +} + +func onSecurityDefinition(msg securitydefinition.SecurityDefinition, _ quickfix.SessionID) quickfix.MessageRejectError { + log.Println("security definition received:", limitMsg(msg)) + + return nil +} + +func onSecurityList(msg securitylist.SecurityList, _ quickfix.SessionID) quickfix.MessageRejectError { + log.Println("security list received:", limitMsg(msg)) + + return nil +} + +// ----- + +func limitMsg(msg quickfix.Messagable) string { + const lengthLimit = 20 + + str := msg.ToMessage().String() + + return str[:math.Min(len(str), lengthLimit)] + "..." +} diff --git a/fix/golang/scenario.go b/fix/golang/scenario.go new file mode 100644 index 0000000..2d74c7c --- /dev/null +++ b/fix/golang/scenario.go @@ -0,0 +1,157 @@ +package main + +import ( + "log" + "strconv" + "time" + + "github.com/pkg/errors" + "github.com/quickfixgo/quickfix" + "github.com/quickfixgo/quickfix/enum" + "github.com/quickfixgo/quickfix/field" + "github.com/quickfixgo/quickfix/fix44/marketdatarequest" + "github.com/quickfixgo/quickfix/fix44/newordersingle" + "github.com/quickfixgo/quickfix/fix44/securitydefinitionrequest" + "github.com/quickfixgo/quickfix/fix44/securitylistrequest" + "github.com/shopspring/decimal" +) + +type Sender func(m quickfix.Messagable) error + +// scenario performs imaginary actions sequence. +func scenario(session quickfix.SessionID) error { + const pair = "BTC_USD" + + log.Println("scenario started") + + sender := func(m quickfix.Messagable) error { + err := quickfix.SendToTarget(m, session) + if err == nil { + time.Sleep(time.Second) // since the scenario is way no interactive, and due to async implementation nature + } + + return errors.Wrap(err, "unable to send quickfix msg") + } + + if err := requestSecurityList(sender); err != nil { + return errors.Wrap(err, "security list request") + } + + if err := requestSecurityDefinition(sender, pair); err != nil { + return errors.Wrap(err, "security definition request") + } + + mdRequestID := strconv.Itoa(int(time.Now().UTC().Unix())) + if err := requestMarketData(sender, mdRequestID, pair); err != nil { + return errors.Wrap(err, "market data request") + } + + if err := createBuyOrder(sender, pair); err != nil { + return errors.Wrap(err, "create buy order") + } + + if err := unsubscribeFromMarketData(sender, mdRequestID); err != nil { + return errors.Wrap(err, "market data unsubscription") + } + + if err := createSellOrder(sender, pair); err != nil { + return errors.Wrap(err, "create sell order") + } + + log.Println("scenario finished") + + return nil +} + +func unsubscribeFromMarketData(sender Sender, mdRequestID string) error { + log.Println("market data unsubscription") + + return sender(marketdatarequest.New( + field.NewMDReqID(mdRequestID), + field.NewSubscriptionRequestType(enum.SubscriptionRequestType_DISABLE_PREVIOUS_SNAPSHOT_PLUS_UPDATE_REQUEST), + field.NewMarketDepth(0), // ignored + )) +} + +func createBuyOrder(sender Sender, pair string) error { + log.Println("creating buy order") + + return createOrder(sender, pair, enum.Side_BUY) +} + +func createSellOrder(sender Sender, pair string) error { + log.Println("creating sell order") + + return createOrder(sender, pair, enum.Side_SELL) +} + +func createOrder(sender Sender, pair string, side enum.Side) error { + const ( + quantityScale = 8 + priceScale = 8 + price = 15899 + ) + + orderID := strconv.Itoa(int(time.Now().UTC().Unix())) + + singleOrderRequest := newordersingle.New( + field.NewClOrdID(orderID), + field.NewSide(side), + field.NewTransactTime(time.Now().UTC()), + field.NewOrdType(enum.OrdType_LIMIT), + ) + singleOrderRequest.SetSymbol(pair) + singleOrderRequest.SetOrderQty(decimal.New(1, -2), quantityScale) // 0.01 + singleOrderRequest.SetTimeInForce(enum.TimeInForce_GOOD_TILL_CANCEL) + singleOrderRequest.SetExecInst("") + singleOrderRequest.SetPrice(decimal.NewFromInt(price), priceScale) + + return sender(singleOrderRequest) +} + +func requestMarketData(sender Sender, requestID string, pair string) error { + log.Println("requesting market data") + + mdReq := marketdatarequest.New( + field.NewMDReqID(requestID), + field.NewSubscriptionRequestType(enum.SubscriptionRequestType_SNAPSHOT_PLUS_UPDATES), + field.NewMarketDepth(0), // ignored + ) + mdEntryTypesGrp := marketdatarequest.NewNoMDEntryTypesRepeatingGroup() + mdEntryTypes := mdEntryTypesGrp.Add() + mdEntryTypes.SetMDEntryType(enum.MDEntryType_BID) + mdEntryTypes = mdEntryTypesGrp.Add() + mdEntryTypes.SetMDEntryType(enum.MDEntryType_OFFER) + mdReq.SetNoMDEntryTypes(mdEntryTypesGrp) + + relatedSymGrp := marketdatarequest.NewNoRelatedSymRepeatingGroup() + relatedSym := relatedSymGrp.Add() + relatedSym.SetSymbol(pair) + mdReq.SetNoRelatedSym(relatedSymGrp) + + return sender(mdReq) +} + +func requestSecurityDefinition(sender Sender, pair string) error { + log.Println("requesting security definition") + + requestID := strconv.Itoa(int(time.Now().UTC().Unix())) + securityDefinitionRequest := securitydefinitionrequest.New( + field.NewSecurityReqID(requestID), + field.NewSecurityRequestType(enum.SecurityRequestType_REQUEST_LIST_SECURITIES), + ) + securityDefinitionRequest.SetSymbol(pair) + + return sender(securityDefinitionRequest) +} + +func requestSecurityList(sender Sender) error { + log.Println("requesting security list") + + requestID := strconv.Itoa(int(time.Now().UTC().Unix())) + + return sender(securitylistrequest.New( + field.NewSecurityReqID(requestID), + field.NewSecurityListRequestType(enum.SecurityListRequestType_ALL_SECURITIES)), + ) +} diff --git a/fix/golang/util.go b/fix/golang/util.go new file mode 100644 index 0000000..6de03bf --- /dev/null +++ b/fix/golang/util.go @@ -0,0 +1,5 @@ +package main + +import "log" + +func LogPrintlnErr(err error) { log.Println(err) } From e4544692c924b5539a6d95ac42d0a28cd8bee599 Mon Sep 17 00:00:00 2001 From: Aleksey Mikhaylov Date: Thu, 15 Dec 2022 15:55:37 +0300 Subject: [PATCH 3/3] review --- fix/golang/main.go | 15 ++++++++++---- fix/golang/scenario.go | 44 ++++++++++++++++++++++++++++++------------ fix/golang/util.go | 5 ----- 3 files changed, 43 insertions(+), 21 deletions(-) delete mode 100644 fix/golang/util.go diff --git a/fix/golang/main.go b/fix/golang/main.go index 01fce20..121412c 100644 --- a/fix/golang/main.go +++ b/fix/golang/main.go @@ -13,7 +13,6 @@ import ( "syscall" "github.com/fatih/color" - "github.com/mikhalytch/eggs/try" "github.com/pkg/errors" "github.com/quickfixgo/quickfix" "github.com/quickfixgo/quickfix/config" @@ -45,9 +44,17 @@ func main0() error { go func() { defer func() { scenarioFinishedCh <- struct{}{} }() - try.Trie(awaitLogon(ctx, sessionsCh)). - Proc(scenario). - ProcFailure(LogPrintlnErr) + session, err := awaitLogon(ctx, sessionsCh) + if err != nil { + log.Println(err) + + return + } + + err = scenario(session) + if err != nil { + log.Println(err) + } }() select { diff --git a/fix/golang/scenario.go b/fix/golang/scenario.go index 2d74c7c..a829ca2 100644 --- a/fix/golang/scenario.go +++ b/fix/golang/scenario.go @@ -11,6 +11,7 @@ import ( "github.com/quickfixgo/quickfix/field" "github.com/quickfixgo/quickfix/fix44/marketdatarequest" "github.com/quickfixgo/quickfix/fix44/newordersingle" + "github.com/quickfixgo/quickfix/fix44/ordercancelrequest" "github.com/quickfixgo/quickfix/fix44/securitydefinitionrequest" "github.com/quickfixgo/quickfix/fix44/securitylistrequest" "github.com/shopspring/decimal" @@ -20,7 +21,7 @@ type Sender func(m quickfix.Messagable) error // scenario performs imaginary actions sequence. func scenario(session quickfix.SessionID) error { - const pair = "BTC_USD" + const pair = "BTC_USDT" log.Println("scenario started") @@ -41,20 +42,25 @@ func scenario(session quickfix.SessionID) error { return errors.Wrap(err, "security definition request") } - mdRequestID := strconv.Itoa(int(time.Now().UTC().Unix())) - if err := requestMarketData(sender, mdRequestID, pair); err != nil { + mdRequestID, err := requestMarketData(sender, pair) + if err != nil { return errors.Wrap(err, "market data request") } - if err := createBuyOrder(sender, pair); err != nil { + orderID, err := createBuyOrder(sender, pair) + if err != nil { return errors.Wrap(err, "create buy order") } + if err := cancelOrder(sender, orderID); err != nil { + return errors.Wrap(err, "order cancellation") + } + if err := unsubscribeFromMarketData(sender, mdRequestID); err != nil { return errors.Wrap(err, "market data unsubscription") } - if err := createSellOrder(sender, pair); err != nil { + if _, err := createSellOrder(sender, pair); err != nil { return errors.Wrap(err, "create sell order") } @@ -63,6 +69,19 @@ func scenario(session quickfix.SessionID) error { return nil } +func cancelOrder(sender Sender, orderID string) error { + log.Println("order cancellation") + + cancelOrderRequest := ordercancelrequest.New( + field.NewOrigClOrdID(orderID), + field.NewClOrdID(""), // ignored + field.NewSide(""), // ignored + field.NewTransactTime(time.Now().UTC()), // ignored + ) + + return sender(cancelOrderRequest) +} + func unsubscribeFromMarketData(sender Sender, mdRequestID string) error { log.Println("market data unsubscription") @@ -73,19 +92,19 @@ func unsubscribeFromMarketData(sender Sender, mdRequestID string) error { )) } -func createBuyOrder(sender Sender, pair string) error { +func createBuyOrder(sender Sender, pair string) (string, error) { log.Println("creating buy order") return createOrder(sender, pair, enum.Side_BUY) } -func createSellOrder(sender Sender, pair string) error { +func createSellOrder(sender Sender, pair string) (string, error) { log.Println("creating sell order") return createOrder(sender, pair, enum.Side_SELL) } -func createOrder(sender Sender, pair string, side enum.Side) error { +func createOrder(sender Sender, pair string, side enum.Side) (string, error) { const ( quantityScale = 8 priceScale = 8 @@ -106,14 +125,15 @@ func createOrder(sender Sender, pair string, side enum.Side) error { singleOrderRequest.SetExecInst("") singleOrderRequest.SetPrice(decimal.NewFromInt(price), priceScale) - return sender(singleOrderRequest) + return orderID, sender(singleOrderRequest) } -func requestMarketData(sender Sender, requestID string, pair string) error { +func requestMarketData(sender Sender, pair string) (string, error) { log.Println("requesting market data") + mdRequestID := strconv.Itoa(int(time.Now().UTC().Unix())) mdReq := marketdatarequest.New( - field.NewMDReqID(requestID), + field.NewMDReqID(mdRequestID), field.NewSubscriptionRequestType(enum.SubscriptionRequestType_SNAPSHOT_PLUS_UPDATES), field.NewMarketDepth(0), // ignored ) @@ -129,7 +149,7 @@ func requestMarketData(sender Sender, requestID string, pair string) error { relatedSym.SetSymbol(pair) mdReq.SetNoRelatedSym(relatedSymGrp) - return sender(mdReq) + return mdRequestID, sender(mdReq) } func requestSecurityDefinition(sender Sender, pair string) error { diff --git a/fix/golang/util.go b/fix/golang/util.go deleted file mode 100644 index 6de03bf..0000000 --- a/fix/golang/util.go +++ /dev/null @@ -1,5 +0,0 @@ -package main - -import "log" - -func LogPrintlnErr(err error) { log.Println(err) }