Cointracker app with Golang and Vue3.js

April 11, 2022

This article is about creating a fullstack asset/portfolio tracker application using Golang for the backend, Vue3.js for the frontend with authentication handled by Firebase.

Let’s get started, this article is only for the backend setup.

For the backend, i took reference from crud app video as it was a simple and powerful set up with Golang, performant database with postgres + gorm for sql driver, Gorilla Mux for a quick out of the box setting up a backend router to build CRUD APIs. I also took inspiration for financial data structures from finance go.

These are the main project components are being used:

  1. Database - postgresql
  2. gorm
  3. Json marshall, unmarshall
  4. Project folder structure
  5. Gorilla Mux
  6. Cors

START

Setting up Postgres DB on Mac

create a go project

go mod init github.com/<your-github-user>/<name-of-project>

To start, initialise the project, it should create go.mod file

Set up project folder structure

  • Cmd
    • main.go
  • pkg
    • config
      • app.go
    • controllers
      • transaction-controller.go
    • models
      • assets.go
      • transaction.go
      • db.go
    • routes
      • portfolioasets-routes.go
    • utils
      • utils.go

API routes

Table listing API routes for application API Routes table

Set up routes.

var RegisterPortfolioAssetsRoutes = func(r *mux.Router) {

    // portfolio endpoint
    r.HandleFunc("/assets", controllers.GetAssets).Methods("GET").Queries("uuid", "{uuid}")

    // Txns endpoint
    r.HandleFunc("/transactions", controllers.CreateTransaction).Methods("POST")
    r.HandleFunc("/transactions/{transactionId}", controllers.UpdateTransaction).Methods("PUT")
    r.HandleFunc("/transactions/{transactionId}", controllers.DeleteTransaction).Methods("DELETE")
}

Setup struct type

At this application’s core, the data model is the ‘Transaction’ struct where details such as type of transaction, name, price per coin, quantity, etc, are provided.

type Transaction struct {
    gorm.Model
    ID           uint      `gorm:"primaryKey" json:"-"`
    Type         string    `json:"type"`
    Symbol       string    `json:"symbol"`
    Name         string    `json:"name"`
    Quantity     float64   `json:"quantity"`
    Total        float64   `json:"total"`
    PricePerCoin float64   `json:"pricePerCoin"`
    DateTimeTxn  time.Time `json:"dateTimeTxn"`
    Fees         float64   `json:"fees"`
    UserUUID     string    `json:"-"`
}

The frontend should receive a json object of ‘User Assets’ to display upon. This would consist namely of multiple Asset types owned by the user (i,e BTC, ETH, LUNA) and total asset value. Each asset type would be a collection of transactions for the particular asset type.

type Portfolio struct {
    TotalAssetsBalance float64          `json:"totalAssetsBalance"`
    UserId             string           `json:"userid"`
    Assets             map[string]Asset `json:"assets"`
    AssetCount         int              `json:"-"`
}

type AssetTransaction struct {
    TxnId        uint      `json:"txnId"`
    Type         string    `json:"type"`
    Symbol       string    `json:"symbol"`
    Name         string    `json:"name"`
    Quantity     float64   `json:"quantity"`
    Total        float64   `json:"total"`
    PricePerCoin float64   `json:"pricePerCoin"`
    DateTimeTxn  time.Time `json:"dateTimeTxn"`
    Fees         float64   `json:"fees"`
}

type Asset struct {
    TotalValue   float64            `json:"totalValue"`
    TotalHolding float64            `json:"totalHolding"`
    AveragePrice float64            `json:"averagePrice"`
    Name         string             `json:"name"`
    Symbol       string             `json:"symbol"`
    Transactions []AssetTransaction `json:"transactions"`
}

Set up controllers. On the controller methods, it would mainly be handling HTTP request and response. It would call upon the model to process and handle the database CRUD logic.

Set up Model methods.

This section consists of retrieving from PostGres DB all transactions from ‘userId’. Thereafter mapping each unique asset type to its relevant transaction records. Other values such as total holdings, average price and total value will be calculated and declared onto the data object.

func GetUserAssets(userId string) (Portfolio, error) {
    log.Info("GetUserAssets", zap.String("userId", userId))
    var portfolio Portfolio
    portfolio.Assets = make(map[string]Asset)
    portfolio.UserId = userId
    // Get all transcations for userId
    txns, db := getTransactionsByUserId(strings.ToLower(userId))
    if db.Error != nil {
        return portfolio, db.Error
    }
    for _, txn := range txns {
        if a, found := portfolio.Assets[txn.Symbol]; !found {
            // var asset Assetass
            asset := Asset{
                Name:   txn.Name,
                Symbol: txn.Symbol,
            }
            asset.Transactions = append(asset.Transactions, txn)
            portfolio.Assets[txn.Symbol] = asset
        } else {
            a.Transactions = append(a.Transactions, txn)
            portfolio.Assets[txn.Symbol] = a
        }
    }

    for sym, asset := range portfolio.Assets {
        var totalVal, totalHoldings float64
        for _, t := range asset.Transactions {
            totalVal = totalVal + t.Total
            totalHoldings = totalHoldings + t.Quantity
        }
        asset.TotalValue = totalVal
        asset.TotalHolding = totalHoldings
        asset.AveragePrice = asset.TotalValue / asset.TotalHolding
        portfolio.Assets[sym] = asset
        portfolio.TotalAssetsBalance = portfolio.TotalAssetsBalance + asset.TotalValue
    }

    portfolio.AssetCount = len(portfolio.Assets)
    return portfolio, nil
}

Testing on postman

{
  "ETH": {
    "totalValue": 3000.0,
    "totalHolding": 1,
    "averagePrice": 3100,
    "holdings": [
      {
        "type": "B",
        "pricePerCoin": 3100.0,
        "totalSpent": 1500.0,
        "fees": 0.0,
        "quantity": 0.5,
        "dateTimeTxn": "2022-03-31T02:38:27.927783+08:00"
      },
      {
        "type": "B",
        "pricePerCoin": 3100.0,
        "totalSpent": 1500.0,
        "fees": 0.0,
        "quantity": 0.5,
        "dateTimeTxn": "2022-03-31T02:38:27.927783+08:00"
      }
    ]
  },
  "FTM": {
    "totalValue": 1200.0,
    "totalHolding": 650.0,
    "averagePrice": 1.846,
    "holdings": [
      {
        "type": "B",
        "pricePerCoin": 3100.0,
        "totalSpent": 1500.0,
        "fees": 0.0,
        "quantity": 0.5,
        "dateTimeTxn": "2022-03-31T02:38:27.927783+08:00"
      }
    ]
  }
}

Formatting date-time should always be in ISO8601 format for ease of usage through the application.

iso8601 format ISO 8601 saves lives

This covers the backend setup to retrieve, create, update and delete transactions. I will be creating the frontend in Vue. Do stay tuned!

Postgres CHEAT SHEET:

https://gist.github.com/Kartones/dd3ff5ec5ea238d4c546

references: