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 ) func init() { log.Printf("ynab-portfolio-monitor init") // Load 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 { if value == "" { log.Fatalf("shell environment variable %s is not set", key) } } // 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) if err != nil { log.Fatalf("failed to convert environment variable questrade_account_%d with value of '%s' to integer: %v", i, questradeAccountIDString, err) } 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) } // ynab client is static and has no persistent data so is initialized here and not in main program loop 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) } } 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 } 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 }