add support for bitcoin
	
		
			
	
		
	
	
		
	
		
			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:
		
							
								
								
									
										37
									
								
								bitcoin/address.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								bitcoin/address.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					package bitcoin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Address struct {
 | 
				
			||||||
 | 
						Address    string `json:"address"`
 | 
				
			||||||
 | 
						ChainStats struct {
 | 
				
			||||||
 | 
							FundedTxoCount int `json:"funded_txo_count"`
 | 
				
			||||||
 | 
							FundedTxoSum   int `json:"funded_txo_sum"`
 | 
				
			||||||
 | 
							SpentTxoCount  int `json:"spent_txo_count"`
 | 
				
			||||||
 | 
							SpentTxoSum    int `json:"spent_txo_sum"`
 | 
				
			||||||
 | 
							TxCount        int `json:"tx_count"`
 | 
				
			||||||
 | 
						} `json:"chain_stats"`
 | 
				
			||||||
 | 
						MempoolStats struct {
 | 
				
			||||||
 | 
							FundedTxoCount int `json:"funded_txo_count"`
 | 
				
			||||||
 | 
							FundedTxoSum   int `json:"funded_txo_sum"`
 | 
				
			||||||
 | 
							SpentTxoCount  int `json:"spent_txo_count"`
 | 
				
			||||||
 | 
							SpentTxoSum    int `json:"spent_txo_sum"`
 | 
				
			||||||
 | 
							TxCount        int `json:"tx_count"`
 | 
				
			||||||
 | 
						} `json:"mempool_stats"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetAddress returns an Address struct populated with data from blockstream.info
 | 
				
			||||||
 | 
					// for a given BTC address
 | 
				
			||||||
 | 
					func (c *Client) GetAddress(address string) (*Address, error) {
 | 
				
			||||||
 | 
						addressResponse := &Address{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err := c.get(fmt.Sprintf("address/%s", address), addressResponse, url.Values{})
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return addressResponse, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										81
									
								
								bitcoin/client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								bitcoin/client.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
				
			|||||||
 | 
					package bitcoin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"io"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"net/url"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const apiBaseURL = "https://blockstream.info/api/"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A client is the structure that will be used to consume the API
 | 
				
			||||||
 | 
					// endpoints. It holds the login credentials, http client/transport,
 | 
				
			||||||
 | 
					// rate limit information, and the login session timer.
 | 
				
			||||||
 | 
					type Client struct {
 | 
				
			||||||
 | 
						httpClient *http.Client
 | 
				
			||||||
 | 
						transport  *http.Transport
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Send an HTTP GET request, and return the processed response
 | 
				
			||||||
 | 
					func (c *Client) get(endpoint string, out interface{}, query url.Values) error {
 | 
				
			||||||
 | 
						req, err := http.NewRequest("GET", apiBaseURL+endpoint+"?"+query.Encode(), nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to create new GET request: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res, err := c.httpClient.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("http GET request failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = c.processResponse(res, out)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to process response: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// processResponse takes the body of an HTTP response, and either returns
 | 
				
			||||||
 | 
					// the error code, or unmarshalls the JSON response, extracts
 | 
				
			||||||
 | 
					// rate limit info, and places it into the object
 | 
				
			||||||
 | 
					// output parameter. This function closes the response body after reading it.
 | 
				
			||||||
 | 
					func (c *Client) processResponse(res *http.Response, out interface{}) error {
 | 
				
			||||||
 | 
						body, err := io.ReadAll(res.Body)
 | 
				
			||||||
 | 
						res.Body.Close()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to read response body: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if res.StatusCode != 200 && res.StatusCode != 201 {
 | 
				
			||||||
 | 
							return fmt.Errorf("unexpected http status code '%d': %v", res.StatusCode, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = json.Unmarshal(body, out)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to unmarshal response body: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// NewClient is the factory function for clients
 | 
				
			||||||
 | 
					func NewClient() (*Client, error) {
 | 
				
			||||||
 | 
						transport := &http.Transport{
 | 
				
			||||||
 | 
							ResponseHeaderTimeout: 5 * time.Second,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						client := &http.Client{
 | 
				
			||||||
 | 
							Transport: transport,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Create a new client
 | 
				
			||||||
 | 
						c := &Client{
 | 
				
			||||||
 | 
							httpClient: client,
 | 
				
			||||||
 | 
							transport:  transport,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return c, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										43
									
								
								bitcoin/fiat.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								bitcoin/fiat.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
				
			|||||||
 | 
					package bitcoin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// example: https://api.coinconvert.net/convert/btc/cad?amount=1
 | 
				
			||||||
 | 
					const fiatConvertURL = "https://api.coinconvert.net/convert/btc/cad"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type FiatConversion struct {
 | 
				
			||||||
 | 
						Status string  `json:"status"`
 | 
				
			||||||
 | 
						BTC    int     `json:"BTC"`
 | 
				
			||||||
 | 
						CAD    float64 `json:"CAD"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BTC to CAD FIAT conversion - accepts an
 | 
				
			||||||
 | 
					// amount in satoshi's and returns a CAD amount * 1000
 | 
				
			||||||
 | 
					func (c *Client) ConvertBTCToCAD(amount int) (int, error) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fiatConversion := &FiatConversion{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						req, err := http.NewRequest("GET", fiatConvertURL+"?amount=1", nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("failed to create new GET request: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						res, err := c.httpClient.Do(req)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("http GET request failed: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = c.processResponse(res, fiatConversion)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("failed to process response: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if fiatConversion.Status != "success" {
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("fiat conversion status was '%s' but expected 'success'", fiatConversion.Status)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return (amount * int(fiatConversion.CAD*1000)) / 100000000, nil // one BTC = one hundred million satoshi's
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										93
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								main.go
									
									
									
									
									
								
							@@ -7,6 +7,7 @@ import (
 | 
				
			|||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"deadbeef.codes/steven/ynab-portfolio-monitor/bitcoin"
 | 
				
			||||||
	"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
 | 
						"deadbeef.codes/steven/ynab-portfolio-monitor/questrade"
 | 
				
			||||||
	"deadbeef.codes/steven/ynab-portfolio-monitor/ynab"
 | 
						"deadbeef.codes/steven/ynab-portfolio-monitor/ynab"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -15,8 +16,11 @@ var (
 | 
				
			|||||||
	persistentData          *PersistentData
 | 
						persistentData          *PersistentData
 | 
				
			||||||
	questradeClient         *questrade.Client
 | 
						questradeClient         *questrade.Client
 | 
				
			||||||
	ynabClient              *ynab.Client
 | 
						ynabClient              *ynab.Client
 | 
				
			||||||
 | 
						bitcoinClient           *bitcoin.Client
 | 
				
			||||||
	questradeAccountIDs     []int
 | 
						questradeAccountIDs     []int
 | 
				
			||||||
	ynabAccountIDs      []string
 | 
						questradeYnabAccountIDs []string
 | 
				
			||||||
 | 
						bitcoinAddresses        []string
 | 
				
			||||||
 | 
						bitcoinYnabAccountID    string
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func init() {
 | 
					func init() {
 | 
				
			||||||
@@ -27,6 +31,7 @@ func init() {
 | 
				
			|||||||
	envVars["questrade_refresh_token"] = os.Getenv("questrade_refresh_token")
 | 
						envVars["questrade_refresh_token"] = os.Getenv("questrade_refresh_token")
 | 
				
			||||||
	envVars["ynab_secret"] = os.Getenv("ynab_secret")
 | 
						envVars["ynab_secret"] = os.Getenv("ynab_secret")
 | 
				
			||||||
	envVars["ynab_budget_id"] = os.Getenv("ynab_budget_id")
 | 
						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
 | 
						// Validate that all required environment variables are set
 | 
				
			||||||
	for key, value := range envVars {
 | 
						for key, value := range envVars {
 | 
				
			||||||
@@ -35,6 +40,9 @@ func init() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// questrade
 | 
				
			||||||
 | 
						questradeAccountIDs = make([]int, 0)
 | 
				
			||||||
 | 
						questradeYnabAccountIDs = make([]string, 0)
 | 
				
			||||||
	for i := 0; true; i++ {
 | 
						for i := 0; true; i++ {
 | 
				
			||||||
		questradeAccountIDString := os.Getenv(fmt.Sprintf("questrade_account_%d", i))
 | 
							questradeAccountIDString := os.Getenv(fmt.Sprintf("questrade_account_%d", i))
 | 
				
			||||||
		ynabAccountID := os.Getenv(fmt.Sprintf("questrade_ynab_account_%d", i))
 | 
							ynabAccountID := os.Getenv(fmt.Sprintf("questrade_ynab_account_%d", i))
 | 
				
			||||||
@@ -47,9 +55,20 @@ func init() {
 | 
				
			|||||||
			log.Fatalf("failed to convert environment variable questrade_account_%d with value of '%s' to integer: %v", i, questradeAccountIDString, err)
 | 
								log.Fatalf("failed to convert environment variable questrade_account_%d with value of '%s' to integer: %v", i, questradeAccountIDString, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		questradeAccountIDs = append(questradeAccountIDs, questradeAccountID)
 | 
							questradeAccountIDs = append(questradeAccountIDs, questradeAccountID)
 | 
				
			||||||
		ynabAccountIDs = append(ynabAccountIDs, ynabAccountID)
 | 
							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
 | 
						// Load persistent data
 | 
				
			||||||
	var err error
 | 
						var err error
 | 
				
			||||||
	persistentData, err = loadPersistentData()
 | 
						persistentData, err = loadPersistentData()
 | 
				
			||||||
@@ -62,6 +81,12 @@ func init() {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Fatalf("failed to create ynab client: %v", err)
 | 
							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() {
 | 
					func main() {
 | 
				
			||||||
@@ -90,8 +115,12 @@ func main() {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Update Bitcoin account
 | 
							// Update Bitcoin account
 | 
				
			||||||
 | 
							err = syncBitoin()
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								log.Fatalf("failed to sync bitcoin to ynab: %v", err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Update ComputerShare account
 | 
							// TBD: Update ComputerShare account
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		log.Print("Sleeping for 6 hours...")
 | 
							log.Print("Sleeping for 6 hours...")
 | 
				
			||||||
		time.Sleep(time.Hour * 6)
 | 
							time.Sleep(time.Hour * 6)
 | 
				
			||||||
@@ -108,40 +137,36 @@ func syncQuestrade() error {
 | 
				
			|||||||
			return fmt.Errorf("failed to get questrade account balance for account ID '%d': %v", questradeAccountID, err)
 | 
								return fmt.Errorf("failed to get questrade account balance for account ID '%d': %v", questradeAccountID, err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		ynabTransactionID, ynabTransactionAmount, err := ynabClient.GetTodayYnabCapitalGainsTransaction(ynabAccountIDs[i])
 | 
							err = ynabClient.SetAccountBalance(questradeYnabAccountIDs[i], questradeBalance)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return fmt.Errorf("failed to get ynab capital gains transaction ID: %v", err)
 | 
								return fmt.Errorf("failed to set YNAB account balance: %v", err)
 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		ynabAccount, err := ynabClient.GetAccount(ynabAccountIDs[i])
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			return fmt.Errorf("failed to get ynab account with id '%s': %v", ynabAccountIDs[i], err)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		balanceDelta := questradeBalance - ynabAccount.Data.Account.Balance
 | 
					 | 
				
			||||||
		balanceDelta += ynabTransactionAmount // Take into account the existing transaction
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if balanceDelta == 0 {
 | 
					 | 
				
			||||||
			continue // If balanceDelta is 0 do not create a transaction i.e. market is closed today
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if ynabTransactionID == "" {
 | 
					 | 
				
			||||||
			// there is no transaction - so create a new one
 | 
					 | 
				
			||||||
			err = ynabClient.CreateTodayYNABCapitalGainsTransaction(ynabAccountIDs[i], balanceDelta)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return fmt.Errorf("failed to create YNAB capital gains transaction for account ID '%s': %v", ynabAccountIDs[i], err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			log.Printf("Creating new capital gains transaction for YNAB account '%s' for amount: %d", ynabAccountIDs[i], balanceDelta)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		} else {
 | 
					 | 
				
			||||||
			// there is an existing transaction - so update the existing one
 | 
					 | 
				
			||||||
			err = ynabClient.UpdateTodayYNABCapitalGainsTransaction(ynabAccountIDs[i], ynabTransactionID, balanceDelta)
 | 
					 | 
				
			||||||
			if err != nil {
 | 
					 | 
				
			||||||
				return fmt.Errorf("failed to update YNAB capital gains transaction for account ID '%s': %v", ynabAccountIDs[i], err)
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
			log.Printf("Updating existing capital gains transaction for YNAB account '%s' for amount: %d", ynabAccountIDs[i], balanceDelta)
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return nil
 | 
						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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ package ynab
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
	"net/url"
 | 
						"net/url"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,3 +39,43 @@ func (c *Client) GetAccount(accountID string) (*Accounts, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return &response, nil
 | 
						return &response, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Creates a "Capital Gains or Losses" adjustment transaction or updates an existing one if it exists to ensure the account balance
 | 
				
			||||||
 | 
					// for the accountID provided equals the balance provided
 | 
				
			||||||
 | 
					func (c *Client) SetAccountBalance(accountID string, balance int) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ynabTransactionID, ynabTransactionAmount, err := c.getTodayYnabCapitalGainsTransaction(accountID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to get ynab capital gains transaction ID: %v", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ynabAccount, err := c.GetAccount(accountID)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return fmt.Errorf("failed to get ynab account with id '%s': %v", accountID, err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						balanceDelta := balance - ynabAccount.Data.Account.Balance
 | 
				
			||||||
 | 
						balanceDelta += ynabTransactionAmount // Take into account the existing transaction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if balanceDelta == 0 {
 | 
				
			||||||
 | 
							return nil // If balanceDelta is 0 do not create a transaction i.e. market is closed today
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ynabTransactionID == "" {
 | 
				
			||||||
 | 
							// there is no transaction - so create a new one
 | 
				
			||||||
 | 
							err = c.createTodayYNABCapitalGainsTransaction(accountID, balanceDelta)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to create YNAB capital gains transaction for account ID '%s': %v", accountID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.Printf("Creating new capital gains transaction for YNAB account '%s' for amount: %d", accountID, balanceDelta)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// there is an existing transaction - so update the existing one
 | 
				
			||||||
 | 
							err = c.updateTodayYNABCapitalGainsTransaction(accountID, ynabTransactionID, balanceDelta)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return fmt.Errorf("failed to update YNAB capital gains transaction for account ID '%s': %v", accountID, err)
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							log.Printf("Updating existing capital gains transaction for YNAB account '%s' for amount: %d", accountID, balanceDelta)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -70,7 +70,7 @@ func (c *Client) GetAccountTransactions(accountID string, sinceDate time.Time) (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Accepts a YNAB account ID and returns the transaction ID, amount and an error for the
 | 
					// Accepts a YNAB account ID and returns the transaction ID, amount and an error for the
 | 
				
			||||||
// the first transaction found with Payee Name "Capital Gains or Losses"
 | 
					// the first transaction found with Payee Name "Capital Gains or Losses"
 | 
				
			||||||
func (c *Client) GetTodayYnabCapitalGainsTransaction(accountID string) (string, int, error) {
 | 
					func (c *Client) getTodayYnabCapitalGainsTransaction(accountID string) (string, int, error) {
 | 
				
			||||||
	ynabTransactions, err := c.GetAccountTransactions(accountID, time.Now())
 | 
						ynabTransactions, err := c.GetAccountTransactions(accountID, time.Now())
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", 0, fmt.Errorf("failed to get ynab transactions: %v", err)
 | 
							return "", 0, fmt.Errorf("failed to get ynab transactions: %v", err)
 | 
				
			||||||
@@ -87,7 +87,7 @@ func (c *Client) GetTodayYnabCapitalGainsTransaction(accountID string) (string,
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Accepts a YNAB account ID and transaction amount and creates a new YNAB transaction
 | 
					// Accepts a YNAB account ID and transaction amount and creates a new YNAB transaction
 | 
				
			||||||
func (c *Client) CreateTodayYNABCapitalGainsTransaction(accountID string, amount int) error {
 | 
					func (c *Client) createTodayYNABCapitalGainsTransaction(accountID string, amount int) error {
 | 
				
			||||||
	transaction := TransactionRequest{}
 | 
						transaction := TransactionRequest{}
 | 
				
			||||||
	transaction.Transaction.AccountID = accountID
 | 
						transaction.Transaction.AccountID = accountID
 | 
				
			||||||
	transaction.Transaction.Amount = amount
 | 
						transaction.Transaction.Amount = amount
 | 
				
			||||||
@@ -106,7 +106,7 @@ func (c *Client) CreateTodayYNABCapitalGainsTransaction(accountID string, amount
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Accepts a YNAB account ID, transaction ID and transaction amount and updates the YNAB transaction with the matching ID
 | 
					// Accepts a YNAB account ID, transaction ID and transaction amount and updates the YNAB transaction with the matching ID
 | 
				
			||||||
func (c *Client) UpdateTodayYNABCapitalGainsTransaction(accountID string, transactionID string, amount int) error {
 | 
					func (c *Client) updateTodayYNABCapitalGainsTransaction(accountID string, transactionID string, amount int) error {
 | 
				
			||||||
	transaction := TransactionRequest{}
 | 
						transaction := TransactionRequest{}
 | 
				
			||||||
	transaction.Transaction.AccountID = accountID
 | 
						transaction.Transaction.AccountID = accountID
 | 
				
			||||||
	transaction.Transaction.ID = transactionID
 | 
						transaction.Transaction.ID = transactionID
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user