From 9665106d8d5a0b2c591857bf8ef513ff3b566fc6 Mon Sep 17 00:00:00 2001 From: pharr117 <24580777+pharr117@users.noreply.github.com> Date: Sun, 21 Jan 2024 13:30:08 -0500 Subject: [PATCH] Patch/fee processing other bugs various csv (#520) * Fix fee DB statement to preload Tx.Block * Add fee additions for cointracker to add fees to all TXs instead of separate row sends * Add personal parsers for IBC Ack and Recv message types to handle missing denoms * Rework start date and end date processing to fix index bugs * gofumpt --- csv/parsers/accointing/accointing.go | 109 ++++++--- csv/parsers/cointracker/cointracker.go | 214 ++++++++++-------- .../cryptotaxcalculator.go | 116 +++++++--- csv/parsers/cryptotaxcalculator/osmosis.go | 2 +- csv/parsers/cryptotaxcalculator/rows.go | 12 +- csv/parsers/cryptotaxcalculator/types.go | 4 +- csv/parsers/koinly/koinly.go | 113 +++++---- csv/parsers/taxbit/taxbit.go | 49 ++-- db/search.go | 2 +- 9 files changed, 377 insertions(+), 244 deletions(-) diff --git a/csv/parsers/accointing/accointing.go b/csv/parsers/accointing/accointing.go index 42870aaf..1ac83255 100644 --- a/csv/parsers/accointing/accointing.go +++ b/csv/parsers/accointing/accointing.go @@ -15,6 +15,7 @@ import ( "github.com/DefiantLabs/cosmos-tax-cli/db" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/gamm" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/poolmanager" + "github.com/DefiantLabs/cosmos-tax-cli/util" ) func (p *Parser) TimeLayout() string { @@ -108,46 +109,25 @@ func (p *Parser) GetRows(address string, startDate, endDate *time.Time) ([]parse }) // Now that we are sorted, if we have a start date, drop everything from before it, if end date is set, drop everything after it - var firstToKeep *int - var lastToKeep *int + var rowsToKeep []*Row for i := range accointingRows { - if startDate != nil && firstToKeep == nil { - rowDate, err := time.Parse(TimeLayout, accointingRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*startDate) { - continue - } - startIdx := i - firstToKeep = &startIdx - } else if endDate != nil && lastToKeep == nil { - rowDate, err := time.Parse(TimeLayout, accointingRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*endDate) { - continue - } else if i > 0 { - endIdx := i - 1 - lastToKeep = &endIdx - break - } + rowDate, err := time.Parse(TimeLayout, accointingRows[i].Date) + if err != nil { + config.Log.Error("Error parsing row date.", err) + return nil, err } - } - if firstToKeep != nil && lastToKeep != nil { // nolint:gocritic - accointingRows = accointingRows[*firstToKeep:*lastToKeep] - } else if firstToKeep != nil { - accointingRows = accointingRows[*firstToKeep:] - } else if lastToKeep != nil { - accointingRows = accointingRows[:*lastToKeep] + if startDate != nil && rowDate.Before(*startDate) { + continue + } + if endDate != nil && rowDate.After(*endDate) { + break + } + rowsToKeep = append(rowsToKeep, &accointingRows[i]) } // Copy AccointingRows into csvRows for return val - csvRows := make([]parsers.CsvRow, len(accointingRows)) - for i, v := range accointingRows { + csvRows := make([]parsers.CsvRow, len(rowsToKeep)) + for i, v := range rowsToKeep { v.Comments = fmt.Sprintf("Address: %s %s", address, v.Comments) csvRows[i] = v } @@ -265,9 +245,9 @@ func ParseTx(address string, events []db.TaxableTransaction) (rows []parsers.Csv case ibc.MsgTransfer: newRow, err = ParseMsgTransfer(address, event) case ibc.MsgAcknowledgement: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgAcknowledgement(address, event) case ibc.MsgRecvPacket: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgRecvPacket(address, event) case poolmanager.MsgSplitRouteSwapExactAmountIn, poolmanager.MsgSwapExactAmountIn, poolmanager.MsgSwapExactAmountOut: newRow, err = ParsePoolManagerSwap(event) default: @@ -384,6 +364,61 @@ func ParseMsgTransfer(address string, event db.TaxableTransaction) (Row, error) return *row, err } +func ParseMsgAcknowledgement(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationSent + amountToUse := event.AmountSent + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgAcknowledgement.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s (classification: deposit)", row.OperationID) + } + + if event.ReceiverAddress.Address == address { + row.InBuyAmount = conversionAmount.Text('f', -1) + row.InBuyAsset = conversionSymbol + row.TransactionType = Deposit + } else if event.SenderAddress.Address == address { // withdrawal + row.OutSellAmount = conversionAmount.Text('f', -1) + row.OutSellAsset = conversionSymbol + row.TransactionType = Withdraw + } + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + row.OperationID = event.Message.Tx.Hash + + return *row, err +} + +func ParseMsgRecvPacket(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationReceived + amountToUse := event.AmountReceived + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgRecvPacket.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s (classification: deposit)", row.OperationID) + } + + if event.ReceiverAddress.Address == address { + row.InBuyAmount = conversionAmount.Text('f', -1) + row.InBuyAsset = conversionSymbol + row.TransactionType = Deposit + } else if event.SenderAddress.Address == address { // withdrawal + row.OutSellAmount = conversionAmount.Text('f', -1) + row.OutSellAsset = conversionSymbol + row.TransactionType = Withdraw + } + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + row.OperationID = event.Message.Tx.Hash + return *row, err +} + func ParseOsmosisReward(event db.TaxableEvent) (Row, error) { row := &Row{} err := row.EventParseBasic(event) diff --git a/csv/parsers/cointracker/cointracker.go b/csv/parsers/cointracker/cointracker.go index b18fb948..025d1d55 100644 --- a/csv/parsers/cointracker/cointracker.go +++ b/csv/parsers/cointracker/cointracker.go @@ -1,6 +1,7 @@ package cointracker import ( + "fmt" "sort" "time" @@ -14,6 +15,7 @@ import ( "github.com/DefiantLabs/cosmos-tax-cli/db" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/gamm" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/poolmanager" + "github.com/DefiantLabs/cosmos-tax-cli/util" ) func (p *Parser) TimeLayout() string { @@ -24,6 +26,13 @@ func (p *Parser) ProcessTaxableTx(address string, taxableTxs []db.TaxableTransac // Build a map, so we know which TX go with which messages txMap := parsers.MakeTXMap(taxableTxs) + feesWithoutTx := []db.Fee{} + for _, fee := range taxableFees { + if _, ok := txMap[fee.Tx.ID]; !ok { + feesWithoutTx = append(feesWithoutTx, fee) + } + } + // Pull messages out of txMap that must be grouped together parsers.SeparateParsingGroups(txMap, p.ParsingGroups) @@ -31,9 +40,14 @@ func (p *Parser) ProcessTaxableTx(address string, taxableTxs []db.TaxableTransac for _, txGroup := range txMap { // All messages have been removed into a parsing group if len(txGroup) != 0 { + var fees []db.Fee + + if len(txGroup) > 0 { + fees = txGroup[0].Message.Tx.Fees + } // For the current transaction group, generate the rows for the CSV. // Usually (but not always) a transaction will only have a single row in the CSV. - txRows, err := ParseTx(address, txGroup) + txRows, err := ParseTx(address, txGroup, fees) if err != nil { return err } @@ -51,15 +65,15 @@ func (p *Parser) ProcessTaxableTx(address string, taxableTxs []db.TaxableTransac } } - // Handle fees on all taxableTxs at once, we don't do this in the regular parser or in the parsing groups - // This requires HandleFees to process the fees into unique mappings of tx -> fees (since we gather Taxable Messages in the taxableTxs) - // If we move it into the ParseTx function or into the ParseGroup function, we may be able to reduce the logic in the HandleFees func - feeRows, err := HandleFees(address, taxableTxs, taxableFees) - if err != nil { - return err - } + for _, fee := range feesWithoutTx { + row := Row{} + err := row.ParseFee(fee.Tx, fee) + if err != nil { + return err + } - p.Rows = append(p.Rows, feeRows...) + p.Rows = append(p.Rows, row) + } return nil } @@ -107,46 +121,26 @@ func (p *Parser) GetRows(address string, startDate, endDate *time.Time) ([]parse }) // Now that we are sorted, if we have a start date, drop everything from before it, if end date is set, drop everything after it - var firstToKeep *int - var lastToKeep *int + // Now that we are sorted, if we have a start date, drop everything from before it, if end date is set, drop everything after it + var rowsToKeep []*Row for i := range cointrackerRows { - if startDate != nil && firstToKeep == nil { - rowDate, err := time.Parse(TimeLayout, cointrackerRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*startDate) { - continue - } - startIdx := i - firstToKeep = &startIdx - } else if endDate != nil && lastToKeep == nil { - rowDate, err := time.Parse(TimeLayout, cointrackerRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*endDate) { - continue - } else if i > 0 { - endIdx := i - 1 - lastToKeep = &endIdx - break - } + rowDate, err := time.Parse(TimeLayout, cointrackerRows[i].Date) + if err != nil { + config.Log.Error("Error parsing row date.", err) + return nil, err } - } - if firstToKeep != nil && lastToKeep != nil { // nolint:gocritic - cointrackerRows = cointrackerRows[*firstToKeep:*lastToKeep] - } else if firstToKeep != nil { - cointrackerRows = cointrackerRows[*firstToKeep:] - } else if lastToKeep != nil { - cointrackerRows = cointrackerRows[:*lastToKeep] + if startDate != nil && rowDate.Before(*startDate) { + continue + } + if endDate != nil && rowDate.After(*endDate) { + break + } + rowsToKeep = append(rowsToKeep, &cointrackerRows[i]) } // Copy cointrackerRows into csvRows for return val - csvRows := make([]parsers.CsvRow, len(cointrackerRows)) - for i, v := range cointrackerRows { + csvRows := make([]parsers.CsvRow, len(rowsToKeep)) + for i, v := range rowsToKeep { csvRows[i] = v } @@ -157,52 +151,6 @@ func (p Parser) GetHeaders() []string { return []string{"Date", "Received Quantity", "Received Currency", "Sent Quantity", "Sent Currency", "Fee Amount", "Fee Currency", "Tag"} } -// HandleFees: -// If the transaction lists the same amount of fees as there are rows in the CSV, -// then we spread the fees out one per row. Otherwise we add a line for the fees, -// where each fee has a separate line. -func HandleFees(address string, events []db.TaxableTransaction, allFees []db.Fee) (rows []Row, err error) { - // No events -- This address didn't pay any fees - if len(events) == 0 && len(allFees) == 0 { - return rows, nil - } - - // We need to gather all unique fees, but we are receiving Messages not Txes - // Make a map from TX hash to fees array to keep unique - txToFeesMap := make(map[uint][]db.Fee) - txIdsToTx := make(map[uint]db.Tx) - for _, event := range events { - txID := event.Message.Tx.ID - feeStore := event.Message.Tx.Fees - txToFeesMap[txID] = feeStore - txIdsToTx[txID] = event.Message.Tx - } - - // Due to the way we are parsing, we may have fees for TX that we don't have events for - for _, fee := range allFees { - txID := fee.Tx.ID - if _, ok := txToFeesMap[txID]; !ok { - txToFeesMap[txID] = []db.Fee{fee} - txIdsToTx[txID] = fee.Tx - } - } - - for id, txFees := range txToFeesMap { - for _, fee := range txFees { - if fee.PayerAddress.Address == address { - newRow := Row{} - err = newRow.ParseFee(txIdsToTx[id], fee) - if err != nil { - return nil, err - } - rows = append(rows, newRow) - } - } - } - - return rows, nil -} - // ParseEvent: Parse the potentially taxable event func ParseEvent(event db.TaxableEvent) (rows []Row, err error) { if event.Source == db.OsmosisRewardDistribution { @@ -220,7 +168,8 @@ func ParseEvent(event db.TaxableEvent) (rows []Row, err error) { // ParseTx: Parse the potentially taxable TX and Messages // This function is used for parsing a single TX that will not need to relate to any others // Use TX Parsing Groups to parse txes as a group -func ParseTx(address string, events []db.TaxableTransaction) (rows []parsers.CsvRow, err error) { +func ParseTx(address string, events []db.TaxableTransaction, fees []db.Fee) (rows []parsers.CsvRow, err error) { + currFeeIndex := 0 for _, event := range events { var newRow Row var err error @@ -258,9 +207,9 @@ func ParseTx(address string, events []db.TaxableTransaction) (rows []parsers.Csv case ibc.MsgTransfer: newRow, err = ParseMsgTransfer(address, event) case ibc.MsgAcknowledgement: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgAcknowledgement(address, event) case ibc.MsgRecvPacket: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgRecvPacket(address, event) case poolmanager.MsgSplitRouteSwapExactAmountIn, poolmanager.MsgSwapExactAmountIn, poolmanager.MsgSwapExactAmountOut: newRow, err = ParsePoolManagerSwap(event) default: @@ -273,8 +222,39 @@ func ParseTx(address string, events []db.TaxableTransaction) (rows []parsers.Csv continue } + // Attach fees to the transaction events + if currFeeIndex < len(fees) { + if fees[currFeeIndex].PayerAddress.Address == address { + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(fees[currFeeIndex].Amount), fees[currFeeIndex].Denomination) + if err != nil { + config.Log.Errorf("error parsing fee: %v", err) + } else { + newRow.FeeAmount = conversionAmount.String() + newRow.FeeCurrency = conversionSymbol + } + } + currFeeIndex++ + } + rows = append(rows, newRow) } + + // Check if fees have all been processed based on last processed index + if currFeeIndex < len(fees) { + // Create empty row for the fees that weren't processed + for i := currFeeIndex; i < len(fees); i++ { + if fees[i].PayerAddress.Address == address { + newRow := Row{} + err = newRow.ParseFee(fees[i].Tx, fees[i]) + if err != nil { + config.Log.Errorf("error parsing fee: %v", err) + continue + } + rows = append(rows, newRow) + } + } + } + return rows, nil } @@ -359,6 +339,54 @@ func ParseMsgTransfer(address string, event db.TaxableTransaction) (Row, error) return *row, err } +func ParseMsgAcknowledgement(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationSent + amountToUse := event.AmountSent + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgAcknowledgement.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s (classification: withdrawal)", event.Message.Tx.Hash) + } + + if event.ReceiverAddress.Address == address { + row.ReceivedAmount = conversionAmount.Text('f', -1) + row.ReceivedCurrency = conversionSymbol + } else if event.SenderAddress.Address == address { // withdrawal + row.SentAmount = conversionAmount.Text('f', -1) + row.SentCurrency = conversionSymbol + } + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + return *row, err +} + +func ParseMsgRecvPacket(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationReceived + amountToUse := event.AmountReceived + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgAcknowledgement.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s (classification: withdrawal)", event.Message.Tx.Hash) + } + + if event.ReceiverAddress.Address == address { + row.ReceivedAmount = conversionAmount.Text('f', -1) + row.ReceivedCurrency = conversionSymbol + } else if event.SenderAddress.Address == address { // withdrawal + row.SentAmount = conversionAmount.Text('f', -1) + row.SentCurrency = conversionSymbol + } + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + return *row, err +} + func ParseMsgSubmitProposal(address string, event db.TaxableTransaction) (Row, error) { row := &Row{} err := row.ParseBasic(address, event) diff --git a/csv/parsers/cryptotaxcalculator/cryptotaxcalculator.go b/csv/parsers/cryptotaxcalculator/cryptotaxcalculator.go index 792b8d69..3a0e66f5 100644 --- a/csv/parsers/cryptotaxcalculator/cryptotaxcalculator.go +++ b/csv/parsers/cryptotaxcalculator/cryptotaxcalculator.go @@ -1,6 +1,7 @@ package cryptotaxcalculator import ( + "fmt" "sort" "time" @@ -14,6 +15,7 @@ import ( "github.com/DefiantLabs/cosmos-tax-cli/db" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/gamm" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/poolmanager" + "github.com/DefiantLabs/cosmos-tax-cli/util" ) func (p *Parser) TimeLayout() string { @@ -96,40 +98,38 @@ func (p *Parser) GetRows(address string, startDate, endDate *time.Time) ([]parse // Sort by date sort.Slice(cryptoRows, func(i int, j int) bool { - return cryptoRows[i].Date.Before(cryptoRows[j].Date) + leftDate, err := time.Parse(TimeLayout, cryptoRows[i].Date) + if err != nil { + config.Log.Error("Error sorting left date.", err) + return false + } + rightDate, err := time.Parse(TimeLayout, cryptoRows[j].Date) + if err != nil { + config.Log.Error("Error sorting right date.", err) + return false + } + return leftDate.Before(rightDate) }) - // Now that we are sorted, if we have a start date, drop everything from before it, if end date is set, drop everything after it - var firstToKeep *int - var lastToKeep *int + var rowsToKeep []*Row for i := range cryptoRows { - if startDate != nil && firstToKeep == nil { - if cryptoRows[i].Date.Before(*startDate) { - continue - } - startIdx := i - firstToKeep = &startIdx - } else if endDate != nil && lastToKeep == nil { - if cryptoRows[i].Date.Before(*endDate) { - continue - } else if i > 0 { - endIdx := i - 1 - lastToKeep = &endIdx - break - } + rowDate, err := time.Parse(TimeLayout, cryptoRows[i].Date) + if err != nil { + config.Log.Error("Error parsing row date.", err) + return nil, err } - } - if firstToKeep != nil && lastToKeep != nil { // nolint:gocritic - cryptoRows = cryptoRows[*firstToKeep:*lastToKeep] - } else if firstToKeep != nil { - cryptoRows = cryptoRows[*firstToKeep:] - } else if lastToKeep != nil { - cryptoRows = cryptoRows[:*lastToKeep] + if startDate != nil && rowDate.Before(*startDate) { + continue + } + if endDate != nil && rowDate.After(*endDate) { + break + } + rowsToKeep = append(rowsToKeep, &cryptoRows[i]) } // Copy AccointingRows into csvRows for return val - csvRows := make([]parsers.CsvRow, len(cryptoRows)) - for i, v := range cryptoRows { + csvRows := make([]parsers.CsvRow, len(rowsToKeep)) + for i, v := range rowsToKeep { csvRows[i] = v } @@ -208,9 +208,9 @@ func ParseTx(address string, events []db.TaxableTransaction) (rows []parsers.Csv case ibc.MsgTransfer: newRow, err = ParseMsgTransfer(address, event) case ibc.MsgAcknowledgement: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgAcknowledgement(address, event) case ibc.MsgRecvPacket: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgRecvPacket(address, event) case poolmanager.MsgSplitRouteSwapExactAmountIn, poolmanager.MsgSwapExactAmountIn, poolmanager.MsgSwapExactAmountOut: newRow, err = ParsePoolManagerSwap(address, event) default: @@ -307,6 +307,64 @@ func ParseMsgTransfer(address string, event db.TaxableTransaction) (Row, error) return *row, err } +func ParseMsgAcknowledgement(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationSent + amountToUse := event.AmountSent + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgAcknowledgement.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s", event.Message.Tx.Hash) + } + + row.BaseAmount = conversionAmount.Text('f', -1) + row.BaseCurrency = conversionSymbol + + if event.ReceiverAddress.Address == address { + row.Type = Buy + } else if event.SenderAddress.Address == address { // withdrawal + row.Type = Sell + } + + row.From = event.SenderAddress.Address + row.To = event.ReceiverAddress.Address + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + row.ID = event.Message.Tx.Hash + return *row, err +} + +func ParseMsgRecvPacket(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationReceived + amountToUse := event.AmountReceived + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgAcknowledgement.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s", event.Message.Tx.Hash) + } + + row.BaseAmount = conversionAmount.Text('f', -1) + row.BaseCurrency = conversionSymbol + + if event.ReceiverAddress.Address == address { + row.Type = Buy + } else if event.SenderAddress.Address == address { // withdrawal + row.Type = Sell + } + + row.From = event.SenderAddress.Address + row.To = event.ReceiverAddress.Address + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + row.ID = event.Message.Tx.Hash + return *row, err +} + func (p *Parser) InitializeParsingGroups() { p.ParsingGroups = append(p.ParsingGroups, parsers.GetOsmosisTxParsingGroups()...) } diff --git a/csv/parsers/cryptotaxcalculator/osmosis.go b/csv/parsers/cryptotaxcalculator/osmosis.go index 59b2eba4..b72649eb 100644 --- a/csv/parsers/cryptotaxcalculator/osmosis.go +++ b/csv/parsers/cryptotaxcalculator/osmosis.go @@ -12,7 +12,7 @@ func ParseGroup(sf *parsers.WrapperLpTxGroup) error { for _, message := range txMessages { row := Row{} row.ID = message.Message.Tx.Hash - row.Date = message.Message.Tx.Block.TimeStamp + row.Date = message.Message.Tx.Block.TimeStamp.Format(TimeLayout) if message.Message.MessageType.MessageType == gamm.MsgJoinSwapExternAmountIn || message.Message.MessageType.MessageType == gamm.MsgExitSwapShareAmountIn || diff --git a/csv/parsers/cryptotaxcalculator/rows.go b/csv/parsers/cryptotaxcalculator/rows.go index 487eddd5..4b4705c7 100644 --- a/csv/parsers/cryptotaxcalculator/rows.go +++ b/csv/parsers/cryptotaxcalculator/rows.go @@ -9,7 +9,7 @@ import ( func (row Row) GetRowForCsv() []string { return []string{ - row.Date.Format(TimeLayout), + row.Date, row.Type, row.BaseCurrency, row.BaseAmount, @@ -28,11 +28,11 @@ func (row Row) GetRowForCsv() []string { } func (row Row) GetDate() string { - return row.Date.Format(TimeLayout) + return row.Date } func (row *Row) EventParseBasic(event db.TaxableEvent) error { - row.Date = event.Block.TimeStamp + row.Date = event.Block.TimeStamp.Format(TimeLayout) conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(event.Amount), event.Denomination) if err == nil { @@ -48,7 +48,7 @@ func (row *Row) EventParseBasic(event db.TaxableEvent) error { // ParseBasic: Handles the fields that are shared between most types. func (row *Row) ParseBasic(address string, event db.TaxableTransaction) error { - row.Date = event.Message.Tx.Block.TimeStamp + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) row.ID = event.Message.Tx.Hash // deposit @@ -88,7 +88,7 @@ func (row *Row) ParseBasic(address string, event db.TaxableTransaction) error { } func (row *Row) ParseFee(address string, fee db.Fee) error { - row.Date = fee.Tx.Block.TimeStamp + row.Date = fee.Tx.Block.TimeStamp.Format(TimeLayout) row.ID = fee.Tx.Hash row.Type = Fee @@ -104,7 +104,7 @@ func (row *Row) ParseFee(address string, fee db.Fee) error { } func (row *Row) ParseSwap(event db.TaxableTransaction, address, eventType string) error { - row.Date = event.Message.Tx.Block.TimeStamp + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) row.ID = event.Message.Tx.Hash row.Type = eventType diff --git a/csv/parsers/cryptotaxcalculator/types.go b/csv/parsers/cryptotaxcalculator/types.go index c365ba4e..400c1368 100644 --- a/csv/parsers/cryptotaxcalculator/types.go +++ b/csv/parsers/cryptotaxcalculator/types.go @@ -1,8 +1,6 @@ package cryptotaxcalculator import ( - "time" - "github.com/DefiantLabs/cosmos-tax-cli/csv/parsers" ) @@ -20,7 +18,7 @@ type Parser struct { } type Row struct { - Date time.Time + Date string Type string BaseCurrency string BaseAmount string diff --git a/csv/parsers/koinly/koinly.go b/csv/parsers/koinly/koinly.go index c02f22d8..18778922 100644 --- a/csv/parsers/koinly/koinly.go +++ b/csv/parsers/koinly/koinly.go @@ -17,6 +17,7 @@ import ( "github.com/DefiantLabs/cosmos-tax-cli/db" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/gamm" "github.com/DefiantLabs/cosmos-tax-cli/osmosis/modules/poolmanager" + "github.com/DefiantLabs/cosmos-tax-cli/util" ) var unsupportedCoins = []string{ @@ -150,50 +151,28 @@ func (p *Parser) GetRows(address string, startDate, endDate *time.Time) ([]parse } return leftDate.Before(rightDate) }) + mapUnsupportedCoints(koinlyRows) // Now that we are sorted, if we have a start date, drop everything from before it, if end date is set, drop everything after it - var firstToKeep *int - var lastToKeep *int + var rowsToKeep []*Row for i := range koinlyRows { - if startDate != nil && firstToKeep == nil { - rowDate, err := time.Parse(TimeLayout, koinlyRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*startDate) { - continue - } - startIdx := i - firstToKeep = &startIdx - } else if endDate != nil && lastToKeep == nil { - rowDate, err := time.Parse(TimeLayout, koinlyRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*endDate) { - continue - } else if i > 0 { - endIdx := i - 1 - lastToKeep = &endIdx - break - } + rowDate, err := time.Parse(TimeLayout, koinlyRows[i].Date) + if err != nil { + config.Log.Error("Error parsing row date.", err) + return nil, err } - } - if firstToKeep != nil && lastToKeep != nil { // nolint:gocritic - koinlyRows = koinlyRows[*firstToKeep:*lastToKeep] - } else if firstToKeep != nil { - koinlyRows = koinlyRows[*firstToKeep:] - } else if lastToKeep != nil { - koinlyRows = koinlyRows[:*lastToKeep] + if startDate != nil && rowDate.Before(*startDate) { + continue + } + if endDate != nil && rowDate.After(*endDate) { + break + } + rowsToKeep = append(rowsToKeep, &koinlyRows[i]) } - mapUnsupportedCoints(koinlyRows) - // Copy AccointingRows into csvRows for return val - csvRows := make([]parsers.CsvRow, len(koinlyRows)) - for i, v := range koinlyRows { + csvRows := make([]parsers.CsvRow, len(rowsToKeep)) + for i, v := range rowsToKeep { if _, isUnsuppored := coinReplacementMap[v.ReceivedCurrency]; isUnsuppored { v.ReceivedCurrency = coinReplacementMap[v.ReceivedCurrency] } @@ -332,9 +311,9 @@ func ParseTx(address string, events []db.TaxableTransaction) (rows []parsers.Csv case gov.MsgDeposit, gov.MsgDepositV1: newRow, err = ParseMsgDeposit(address, event) case ibc.MsgAcknowledgement: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgAcknowledgement(address, event) case ibc.MsgRecvPacket: - newRow, err = ParseMsgTransfer(address, event) + newRow, err = ParseMsgRecvPacket(address, event) case poolmanager.MsgSplitRouteSwapExactAmountIn, poolmanager.MsgSwapExactAmountIn, poolmanager.MsgSwapExactAmountOut: newRow, err = ParsePoolManagerSwap(event) default: @@ -433,6 +412,62 @@ func ParseMsgTransfer(address string, event db.TaxableTransaction) (Row, error) return *row, err } +func ParseMsgAcknowledgement(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationSent + amountToUse := event.AmountSent + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgRecvPacket.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s (classification: deposit)", event.Message.Tx.Hash) + } + + if event.ReceiverAddress.Address == address { + row.ReceivedAmount = conversionAmount.Text('f', -1) + row.ReceivedCurrency = conversionSymbol + row.Label = Income + } else if event.SenderAddress.Address == address { // withdrawal + row.SentAmount = conversionAmount.Text('f', -1) + row.SentCurrency = conversionSymbol + row.Label = Cost + } + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + row.TxHash = event.Message.Tx.Hash + + return *row, err +} + +func ParseMsgRecvPacket(address string, event db.TaxableTransaction) (Row, error) { + row := &Row{} + + denomToUse := event.DenominationReceived + amountToUse := event.AmountReceived + + conversionAmount, conversionSymbol, err := db.ConvertUnits(util.FromNumeric(amountToUse), denomToUse) + if err != nil { + config.Log.Error("Error with ParseMsgRecvPacket.", err) + return *row, fmt.Errorf("cannot parse denom units for TX %s (classification: deposit)", event.Message.Tx.Hash) + } + + if event.ReceiverAddress.Address == address { + row.ReceivedAmount = conversionAmount.Text('f', -1) + row.ReceivedCurrency = conversionSymbol + row.Label = Income + } else if event.SenderAddress.Address == address { // withdrawal + row.SentAmount = conversionAmount.Text('f', -1) + row.SentCurrency = conversionSymbol + row.Label = Cost + } + + row.Date = event.Message.Tx.Block.TimeStamp.Format(TimeLayout) + row.TxHash = event.Message.Tx.Hash + + return *row, err +} + func ParseMsgSubmitProposal(address string, event db.TaxableTransaction) (Row, error) { row := &Row{} err := row.ParseBasic(address, event) diff --git a/csv/parsers/taxbit/taxbit.go b/csv/parsers/taxbit/taxbit.go index b4864a5f..d460dc72 100644 --- a/csv/parsers/taxbit/taxbit.go +++ b/csv/parsers/taxbit/taxbit.go @@ -107,46 +107,25 @@ func (p *Parser) GetRows(address string, startDate, endDate *time.Time) ([]parse }) // Now that we are sorted, if we have a start date, drop everything from before it, if end date is set, drop everything after it - var firstToKeep *int - var lastToKeep *int + var rowsToKeep []*Row for i := range taxbitRows { - if startDate != nil && firstToKeep == nil { - rowDate, err := time.Parse(TimeLayout, taxbitRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*startDate) { - continue - } - startIdx := i - firstToKeep = &startIdx - } else if endDate != nil && lastToKeep == nil { - rowDate, err := time.Parse(TimeLayout, taxbitRows[i].Date) - if err != nil { - config.Log.Error("Error parsing row date.", err) - return nil, err - } - if rowDate.Before(*endDate) { - continue - } else if i > 0 { - endIdx := i - 1 - lastToKeep = &endIdx - break - } + rowDate, err := time.Parse(TimeLayout, taxbitRows[i].Date) + if err != nil { + config.Log.Error("Error parsing row date.", err) + return nil, err } - } - if firstToKeep != nil && lastToKeep != nil { // nolint:gocritic - taxbitRows = taxbitRows[*firstToKeep:*lastToKeep] - } else if firstToKeep != nil { - taxbitRows = taxbitRows[*firstToKeep:] - } else if lastToKeep != nil { - taxbitRows = taxbitRows[:*lastToKeep] + if startDate != nil && rowDate.Before(*startDate) { + continue + } + if endDate != nil && rowDate.After(*endDate) { + break + } + rowsToKeep = append(rowsToKeep, &taxbitRows[i]) } // Copy cointrackerRows into csvRows for return val - csvRows := make([]parsers.CsvRow, len(taxbitRows)) - for i, v := range taxbitRows { + csvRows := make([]parsers.CsvRow, len(rowsToKeep)) + for i, v := range rowsToKeep { csvRows[i] = v } diff --git a/db/search.go b/db/search.go index 450a56a6..352b4bd2 100644 --- a/db/search.go +++ b/db/search.go @@ -23,7 +23,7 @@ func GetTaxableTransactions(address string, db *gorm.DB) ([]TaxableTransaction, func GetTaxableFees(address string, db *gorm.DB) ([]Fee, error) { var fees []Fee result := db.Joins("JOIN addresses ON addresses.id = fees.payer_address_id"). - Where("addresses.address = ?", address).Preload("PayerAddress").Preload("Denomination").Preload("Tx").Find(&fees) + Where("addresses.address = ?", address).Preload("PayerAddress").Preload("Denomination").Preload("Tx").Preload("Tx.Block").Find(&fees) return fees, result.Error }