abstract providers behind a common interface
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
158
main.go
158
main.go
@@ -1,37 +1,26 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin"
|
||||
"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
|
||||
"deadbeef.codes/steven/ynab-portfolio-monitor/ynab"
|
||||
)
|
||||
|
||||
var (
|
||||
persistentData *PersistentData
|
||||
questradeClient *questrade.Client
|
||||
ynabClient *ynab.Client
|
||||
bitcoinClient *bitcoin.Client
|
||||
questradeAccountIDs []int
|
||||
questradeYnabAccountIDs []string
|
||||
bitcoinAddresses []string
|
||||
bitcoinYnabAccountID string
|
||||
configuredProviders []AccountProvider // Any providers that are successfully configured get added to this slice
|
||||
ynabClient *ynab.Client // YNAB HTTP client
|
||||
)
|
||||
|
||||
// Called at program startup or if SIGHUP is received
|
||||
func init() {
|
||||
log.Printf("ynab-portfolio-monitor init")
|
||||
|
||||
// Load application configuration from environment variables
|
||||
// Load mandatory application configuration from environment variables
|
||||
envVars := make(map[string]string)
|
||||
envVars["questrade_refresh_token"] = os.Getenv("questrade_refresh_token")
|
||||
envVars["ynab_secret"] = os.Getenv("ynab_secret")
|
||||
envVars["ynab_budget_id"] = os.Getenv("ynab_budget_id")
|
||||
envVars["bitcoin_ynab_account"] = os.Getenv("bitcoin_ynab_account")
|
||||
|
||||
// Validate that all required environment variables are set
|
||||
for key, value := range envVars {
|
||||
@@ -40,133 +29,48 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// questrade
|
||||
questradeAccountIDs = make([]int, 0)
|
||||
questradeYnabAccountIDs = make([]string, 0)
|
||||
for i := 0; true; i++ {
|
||||
questradeAccountIDString := os.Getenv(fmt.Sprintf("questrade_account_%d", i))
|
||||
ynabAccountID := os.Getenv(fmt.Sprintf("questrade_ynab_account_%d", i))
|
||||
if questradeAccountIDString == "" || ynabAccountID == "" {
|
||||
break
|
||||
}
|
||||
|
||||
questradeAccountID, err := strconv.Atoi(questradeAccountIDString)
|
||||
// Loop through all providers and attempt to configure them
|
||||
configuredProviders = make([]AccountProvider, 0)
|
||||
for _, p := range allProviders {
|
||||
err := p.Configure()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to convert environment variable questrade_account_%d with value of '%s' to integer: %v", i, questradeAccountIDString, err)
|
||||
log.Printf("skipping provider '%s': %v", p.Name(), err)
|
||||
continue
|
||||
}
|
||||
questradeAccountIDs = append(questradeAccountIDs, questradeAccountID)
|
||||
questradeYnabAccountIDs = append(questradeYnabAccountIDs, ynabAccountID)
|
||||
}
|
||||
|
||||
// bitcoin
|
||||
bitcoinAddresses = make([]string, 0)
|
||||
for i := 0; true; i++ {
|
||||
bitcoinAddress := os.Getenv(fmt.Sprintf("bitcoin_address_%d", i))
|
||||
if bitcoinAddress == "" {
|
||||
break
|
||||
}
|
||||
bitcoinAddresses = append(bitcoinAddresses, bitcoinAddress)
|
||||
}
|
||||
bitcoinYnabAccountID = envVars["bitcoin_ynab_account"]
|
||||
|
||||
// Load persistent data
|
||||
var err error
|
||||
persistentData, err = loadPersistentData()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load persistent data: %v", err)
|
||||
configuredProviders = append(configuredProviders, p)
|
||||
log.Printf("enabled provider '%s'", p.Name())
|
||||
}
|
||||
|
||||
// ynab client is static and has no persistent data so is initialized here and not in main program loop
|
||||
var err error
|
||||
ynabClient, err = ynab.NewClient(envVars["ynab_budget_id"], envVars["ynab_secret"])
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create ynab client: %v", err)
|
||||
}
|
||||
|
||||
bitcoinClient, err = bitcoin.NewClient()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create bitcoin client: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Main program loop
|
||||
func main() {
|
||||
for {
|
||||
var err error
|
||||
|
||||
// Questrade authentication needs to be refreshed and persistentData written to disk in case app restarts
|
||||
questradeClient, err = questrade.NewClient(persistentData.QuestradeRefreshToken)
|
||||
if err != nil {
|
||||
log.Printf("failed to create questrade client: %v", err)
|
||||
time.Sleep(time.Minute * 5) // prevent multiple fast login failures
|
||||
continue
|
||||
// Loop through each configured account provider and attempt to get the account balances, and update YNAB
|
||||
for _, p := range configuredProviders {
|
||||
balances, accountIDs, err := p.GetBalances()
|
||||
if err != nil {
|
||||
log.Printf("failed to get balances with provider '%s': %v", p.Name(), err)
|
||||
continue
|
||||
}
|
||||
if len(balances) != len(accountIDs) {
|
||||
log.Printf("mismatched balance and accountID slice lengths - expected the same: balances length = %d, accountIDs length = %d", len(balances), len(accountIDs))
|
||||
continue
|
||||
}
|
||||
for i := range balances {
|
||||
err = ynabClient.SetAccountBalance(accountIDs[i], balances[i])
|
||||
if err != nil {
|
||||
log.Printf("failed to update ynab account '%s' balance: %v", accountIDs[i], err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
persistentData.QuestradeRefreshToken = questradeClient.Credentials.RefreshToken
|
||||
|
||||
err = savePersistentData(*persistentData)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to save persistent data: %v", err)
|
||||
}
|
||||
|
||||
// Update Questrade accounts
|
||||
err = syncQuestrade()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to sync questrade to ynab: %v", err)
|
||||
}
|
||||
|
||||
// Update Bitcoin account
|
||||
err = syncBitoin()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to sync bitcoin to ynab: %v", err)
|
||||
}
|
||||
|
||||
// TBD: Update ComputerShare account
|
||||
|
||||
log.Print("Sleeping for 6 hours...")
|
||||
time.Sleep(time.Hour * 6)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func syncQuestrade() error {
|
||||
|
||||
for i, questradeAccountID := range questradeAccountIDs {
|
||||
questradeBalance, err := questradeClient.GetQuestradeAccountBalance(questradeAccountID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get questrade account balance for account ID '%d': %v", questradeAccountID, err)
|
||||
}
|
||||
|
||||
err = ynabClient.SetAccountBalance(questradeYnabAccountIDs[i], questradeBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set YNAB account balance: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncBitoin() error {
|
||||
|
||||
var satoshiBalance int
|
||||
for _, bitcoinAddress := range bitcoinAddresses {
|
||||
addressResponse, err := bitcoinClient.GetAddress(bitcoinAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get bitcoin address '%s': %v", bitcoinAddress, err)
|
||||
}
|
||||
|
||||
satoshiBalance += addressResponse.ChainStats.FundedTxoSum - addressResponse.ChainStats.SpentTxoSum
|
||||
}
|
||||
|
||||
fiatBalance, err := bitcoinClient.ConvertBTCToCAD(satoshiBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert satoshi balance to fiat balance: %v", err)
|
||||
}
|
||||
|
||||
err = ynabClient.SetAccountBalance(bitcoinYnabAccountID, fiatBalance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set YNAB account balance: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user