diff --git a/accounting/config.go b/accounting/config.go index 10277ee..81e9b20 100644 --- a/accounting/config.go +++ b/accounting/config.go @@ -85,13 +85,9 @@ type CommonConfig struct { // data api may be down. DisableFiat bool - // FiatBackend is the backend API to be used to for any fiat related - // queries. - FiatBackend fiat.PriceBackend - - // Granularity specifies the level of granularity with which we want to - // get fiat prices. - Granularity *fiat.Granularity + // PriceSourceCfg is the config to be used for initialising the + // PriceSource used for fiat related queries. + PriceSourceCfg *fiat.PriceSourceConfig // Categories is a set of custom categories which should be added to the // report. @@ -104,7 +100,7 @@ type CommonConfig struct { // that fee lookups are not possible in certain cases. func NewOnChainConfig(ctx context.Context, lnd lndclient.LndServices, startTime, endTime time.Time, disableFiat bool, txLookup fees.GetDetailsFunc, - fiatBackend fiat.PriceBackend, granularity *fiat.Granularity, + priceCfg *fiat.PriceSourceConfig, categories []CustomCategory) *OnChainConfig { var getFee func(chainhash.Hash) (btcutil.Amount, error) @@ -131,12 +127,11 @@ func NewOnChainConfig(ctx context.Context, lnd lndclient.LndServices, startTime, return lnd.WalletKit.ListSweeps(ctx) }, CommonConfig: CommonConfig{ - StartTime: startTime, - EndTime: endTime, - DisableFiat: disableFiat, - FiatBackend: fiatBackend, - Granularity: granularity, - Categories: categories, + StartTime: startTime, + EndTime: endTime, + DisableFiat: disableFiat, + Categories: categories, + PriceSourceCfg: priceCfg, }, GetFee: getFee, } @@ -148,7 +143,7 @@ func NewOnChainConfig(ctx context.Context, lnd lndclient.LndServices, startTime, func NewOffChainConfig(ctx context.Context, lnd lndclient.LndServices, maxInvoices, maxPayments, maxForwards uint64, ownPubkey route.Vertex, startTime, endTime time.Time, disableFiat bool, - fiatBackend fiat.PriceBackend, granularity *fiat.Granularity, + priceCfg *fiat.PriceSourceConfig, categories []CustomCategory) *OffChainConfig { return &OffChainConfig{ @@ -177,12 +172,11 @@ func NewOffChainConfig(ctx context.Context, lnd lndclient.LndServices, }, OwnPubKey: ownPubkey, CommonConfig: CommonConfig{ - StartTime: startTime, - EndTime: endTime, - DisableFiat: disableFiat, - FiatBackend: fiatBackend, - Granularity: granularity, - Categories: categories, + StartTime: startTime, + EndTime: endTime, + DisableFiat: disableFiat, + Categories: categories, + PriceSourceCfg: priceCfg, }, } } diff --git a/accounting/conversions.go b/accounting/conversions.go index 5c18897..537e58b 100644 --- a/accounting/conversions.go +++ b/accounting/conversions.go @@ -10,8 +10,8 @@ import ( "github.com/lightninglabs/faraday/utils" ) -// usdPrice is a function which gets the USD price of bitcoin at a given time. -type usdPrice func(timestamp time.Time) (*fiat.USDPrice, error) +// fiatPrice is a function which gets the fiat price of bitcoin at a given time. +type fiatPrice func(timestamp time.Time) (*fiat.Price, error) // satsToMsat converts an amount expressed in sats to msat. func satsToMsat(sats btcutil.Amount) int64 { @@ -33,14 +33,13 @@ func invertMsat(msat int64) int64 { // of price data and returns a convert function which can be used to get // individual price points from this data. func getConversion(ctx context.Context, startTime, endTime time.Time, - disableFiat bool, fiatBackend fiat.PriceBackend, - granularity *fiat.Granularity) (usdPrice, error) { + disableFiat bool, priceCfg *fiat.PriceSourceConfig) (fiatPrice, error) { // If we don't want fiat values, just return a price which will yield // a zero price and timestamp. if disableFiat { - return func(_ time.Time) (*fiat.USDPrice, error) { - return &fiat.USDPrice{}, nil + return func(_ time.Time) (*fiat.Price, error) { + return &fiat.Price{}, nil }, nil } @@ -49,7 +48,7 @@ func getConversion(ctx context.Context, startTime, endTime time.Time, return nil, err } - fiatClient, err := fiat.NewPriceSource(fiatBackend, granularity) + fiatClient, err := fiat.NewPriceSource(priceCfg) if err != nil { return nil, err } @@ -64,7 +63,7 @@ func getConversion(ctx context.Context, startTime, endTime time.Time, // Create a wrapper function which can be used to get individual price // points from our set of price data as we create our report. - return func(ts time.Time) (*fiat.USDPrice, error) { + return func(ts time.Time) (*fiat.Price, error) { return fiat.GetPrice(prices, ts) }, nil } diff --git a/accounting/entries.go b/accounting/entries.go index c5db138..df3f5b0 100644 --- a/accounting/entries.go +++ b/accounting/entries.go @@ -17,9 +17,9 @@ type entryUtils struct { // may be nil. getFee getFeeFunc - // getFiat provides a USD price for the btc value provided at its + // getFiat provides a fiat price for the btc value provided at its // timestamp. - getFiat usdPrice + getFiat fiatPrice // customCategories is a set of custom categories which are set for the // report. diff --git a/accounting/entries_test.go b/accounting/entries_test.go index 36d9087..71818e8 100644 --- a/accounting/entries_test.go +++ b/accounting/entries_test.go @@ -163,7 +163,7 @@ var ( mockPriceTimestamp = time.Unix(1594306589, 0) - mockBTCPrice = &fiat.USDPrice{ + mockBTCPrice = &fiat.Price{ Timestamp: mockPriceTimestamp, Price: decimal.NewFromInt(100000), } @@ -178,7 +178,7 @@ var ( ) // mockPrice is a mocked price function which returns mockPrice * amount. -func mockPrice(_ time.Time) (*fiat.USDPrice, error) { +func mockPrice(_ time.Time) (*fiat.Price, error) { return mockBTCPrice, nil } @@ -213,7 +213,7 @@ func TestChannelOpenEntry(t *testing.T) { return &HarmonyEntry{ Timestamp: transactionTimestamp, Amount: amtMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, amtMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, amtMsat), TxID: openChannelTx, Reference: fmt.Sprintf("%v", channelID), Note: note, @@ -232,7 +232,7 @@ func TestChannelOpenEntry(t *testing.T) { feeEntry := &HarmonyEntry{ Timestamp: transactionTimestamp, Amount: msatAmt, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, msatAmt), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, msatAmt), TxID: openChannelTx, Reference: FeeReference(openChannelTx), Note: channelOpenFeeNote(channelID), @@ -315,7 +315,7 @@ func TestChannelCloseEntry(t *testing.T) { chanEntry := &HarmonyEntry{ Timestamp: closeTimestamp, Amount: amtMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, amtMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, amtMsat), TxID: closeTx, Reference: closeTx, Note: note, @@ -332,7 +332,7 @@ func TestChannelCloseEntry(t *testing.T) { feeEntry := &HarmonyEntry{ Timestamp: closeTimestamp, Amount: mockFeeMSat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, mockFeeMSat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, mockFeeMSat), TxID: closeTx, Reference: FeeReference(closeTx), Note: "", @@ -432,7 +432,7 @@ func TestSweepEntry(t *testing.T) { sweepEntry := &HarmonyEntry{ Timestamp: onChainTimestamp, Amount: amtMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, amtMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, amtMsat), TxID: onChainTxID, Reference: onChainTxID, Note: "", @@ -447,7 +447,7 @@ func TestSweepEntry(t *testing.T) { { Timestamp: onChainTimestamp, Amount: mockFeeMSat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, mockFeeMSat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, mockFeeMSat), TxID: onChainTxID, Reference: FeeReference(onChainTxID), Note: "", @@ -527,7 +527,7 @@ func TestOnChainEntry(t *testing.T) { entry := &HarmonyEntry{ Timestamp: onChainTimestamp, Amount: amtMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, amtMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, amtMsat), TxID: onChainTxID, Reference: onChainTxID, Note: label, @@ -548,7 +548,7 @@ func TestOnChainEntry(t *testing.T) { feeEntry := &HarmonyEntry{ Timestamp: onChainTimestamp, Amount: feeMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, feeMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, feeMsat), TxID: onChainTxID, Reference: FeeReference(onChainTxID), Note: "", @@ -646,7 +646,7 @@ func TestInvoiceEntry(t *testing.T) { expectedEntry := &HarmonyEntry{ Timestamp: invoiceSettleTime, Amount: invoiceOverpaidAmt, - FiatValue: fiat.MsatToUSD( + FiatValue: fiat.MsatToFiat( mockBTCPrice.Price, invoiceOverpaidAmt, ), TxID: invoiceHash, @@ -713,7 +713,7 @@ func TestPaymentEntry(t *testing.T) { paymentEntry := &HarmonyEntry{ Timestamp: paymentTime, Amount: amtMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, amtMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, amtMsat), TxID: paymentHash, Reference: paymentRef, Note: paymentNote(&otherPubkey), @@ -728,7 +728,7 @@ func TestPaymentEntry(t *testing.T) { feeEntry := &HarmonyEntry{ Timestamp: paymentTime, Amount: feeMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, feeMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, feeMsat), TxID: paymentHash, Reference: FeeReference(paymentRef), Note: paymentNote(&otherPubkey), @@ -789,7 +789,7 @@ func TestForwardingEntry(t *testing.T) { fwdEntry := &HarmonyEntry{ Timestamp: forwardTs, Amount: 0, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, 0), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, 0), TxID: txid, Reference: "", Note: note, @@ -802,7 +802,7 @@ func TestForwardingEntry(t *testing.T) { feeEntry := &HarmonyEntry{ Timestamp: forwardTs, Amount: fwdFeeMsat, - FiatValue: fiat.MsatToUSD(mockBTCPrice.Price, fwdFeeMsat), + FiatValue: fiat.MsatToFiat(mockBTCPrice.Price, fwdFeeMsat), TxID: txid, Reference: "", Note: "", diff --git a/accounting/off_chain.go b/accounting/off_chain.go index 557cf21..6e72012 100644 --- a/accounting/off_chain.go +++ b/accounting/off_chain.go @@ -45,7 +45,7 @@ func OffChainReport(ctx context.Context, cfg *OffChainConfig) (Report, error) { // or a no-op function if we do not want prices. getPrice, err := getConversion( ctx, cfg.StartTime, cfg.EndTime, cfg.DisableFiat, - cfg.FiatBackend, cfg.Granularity, + cfg.PriceSourceCfg, ) if err != nil { return nil, err @@ -57,7 +57,7 @@ func OffChainReport(ctx context.Context, cfg *OffChainConfig) (Report, error) { // offChainReportWithPrices produces off chain reports using the getPrice // function provided. This allows testing of our report creation without calling // the actual price API. -func offChainReportWithPrices(cfg *OffChainConfig, getPrice usdPrice) (Report, +func offChainReportWithPrices(cfg *OffChainConfig, getPrice fiatPrice) (Report, error) { invoices, err := cfg.ListInvoices() diff --git a/accounting/on_chain.go b/accounting/on_chain.go index 9e90ae0..cd33e62 100644 --- a/accounting/on_chain.go +++ b/accounting/on_chain.go @@ -21,7 +21,7 @@ func OnChainReport(ctx context.Context, cfg *OnChainConfig) (Report, error) { // or a no-op function if we do not want prices. getPrice, err := getConversion( ctx, cfg.StartTime, cfg.EndTime, cfg.DisableFiat, - cfg.FiatBackend, cfg.Granularity, + cfg.PriceSourceCfg, ) if err != nil { return nil, err @@ -77,7 +77,7 @@ func newChannelInfo(id lnwire.ShortChannelID, chanPoint *wire.OutPoint, // getOnChainInfo queries lnd for all transactions relevant to our on chain // transactions, and produces the set of information that we will need to create // an on chain report. -func getOnChainInfo(cfg *OnChainConfig, getPrice usdPrice) (*onChainInformation, +func getOnChainInfo(cfg *OnChainConfig, getPrice fiatPrice) (*onChainInformation, error) { // Create an info struct to hold all the elements we need. diff --git a/accounting/report.go b/accounting/report.go index f2b15de..156971e 100644 --- a/accounting/report.go +++ b/accounting/report.go @@ -52,7 +52,7 @@ type HarmonyEntry struct { // BTCPrice is the timestamped bitcoin price we used to get our fiat // value. - BTCPrice *fiat.USDPrice + BTCPrice *fiat.Price } // newHarmonyEntry produces a harmony entry. If provided with a negative amount, @@ -63,7 +63,7 @@ type HarmonyEntry struct { // a credit. func newHarmonyEntry(ts time.Time, amountMsat int64, e EntryType, txid, reference, note, category string, onChain bool, - convert usdPrice) (*HarmonyEntry, + convert fiatPrice) (*HarmonyEntry, error) { @@ -86,7 +86,7 @@ func newHarmonyEntry(ts time.Time, amountMsat int64, e EntryType, txid, return &HarmonyEntry{ Timestamp: ts, Amount: amtMsat, - FiatValue: fiat.MsatToUSD(btcPrice.Price, amtMsat), + FiatValue: fiat.MsatToFiat(btcPrice.Price, amtMsat), TxID: txid, Reference: reference, Note: note, diff --git a/cmd/frcli/csv.go b/cmd/frcli/csv.go index d483620..4b6e3cd 100644 --- a/cmd/frcli/csv.go +++ b/cmd/frcli/csv.go @@ -1,19 +1,23 @@ package main import ( + "encoding/csv" + "errors" "fmt" + "os" + "strconv" "time" "github.com/lightninglabs/faraday/frdrpc" ) // CSVHeaders returns the headers used for harmony csv records. -var CSVHeaders = "Timestamp,OnChain,Type,Category,Amount(Msat),Amount(USD),TxID,Reference,BTCPrice,BTCTimestamp,Note" +var CSVHeaders = "Timestamp,OnChain,Type,Category,Amount(Msat),Amount(%v),TxID,Reference,BTCPrice,BTCTimestamp,Note" -// csv returns a csv string of the values contained in a rpc entry. For ease +// writeToCSV returns a csv string of the values contained in a rpc entry. For ease // of use, the credit field is used to set a negative sign (-) on the amount // of an entry when it decreases our balance (credit=false). -func csv(e *frdrpc.ReportEntry) string { +func writeToCSV(e *frdrpc.ReportEntry) string { amountPrefix := "" if !e.Credit { amountPrefix = "-" @@ -26,3 +30,54 @@ func csv(e *frdrpc.ReportEntry) string { amountPrefix, e.Fiat, e.Txid, e.Reference, e.BtcPrice.Price, e.BtcPrice.PriceTimestamp, e.Note) } + +// parsePricesFromCSV reads price point data from the csv at the specified path. +// This function expects the first csv line to be headers and expects the rest +// of the lines to be tuples of the following format: +// 'unix tx seconds, price of 1 BTC in chosen currency'. +func parsePricesFromCSV(path, currency string) ([]*frdrpc.BitcoinPrice, error) { + if path == "" || currency == "" { + return nil, errors.New("custom price csv path and " + + "currency must both be specified") + } + csvFile, err := os.Open(path) + if err != nil { + return nil, err + } + defer csvFile.Close() + + csvLines, err := csv.NewReader(csvFile).ReadAll() + if err != nil { + return nil, err + } + + if len(csvLines) < 2 { + return nil, errors.New("no price points found in CSV") + } + + // Skip the first line in the CSV file since we expect this line + // to contain column headers. + csvLines = csvLines[1:] + + prices := make([]*frdrpc.BitcoinPrice, len(csvLines)) + + for i, line := range csvLines { + if len(line) != 2 { + return nil, errors.New("incorrect csv format. " + + "Two columns items are expected per row") + } + + timestamp, err := strconv.ParseInt(line[0], 10, 64) + if err != nil { + return nil, err + } + + prices[i] = &frdrpc.BitcoinPrice{ + PriceTimestamp: uint64(timestamp), + Price: line[1], + Currency: currency, + } + } + + return prices, nil +} diff --git a/cmd/frcli/fiat_estimate.go b/cmd/frcli/fiat_estimate.go index 8db2917..5256cf5 100644 --- a/cmd/frcli/fiat_estimate.go +++ b/cmd/frcli/fiat_estimate.go @@ -32,6 +32,18 @@ var fiatEstimateCommand = cli.Command{ Usage: "fiat backend to be used. Options include: " + "'coincap' (default) and 'coindesk'", }, + cli.StringFlag{ + Name: "prices_csv_path", + Usage: "Path to a CSV file containing custom fiat " + + "price data. This is only required if " + + "'fiat_backend' is set to 'custom'.", + }, + cli.StringFlag{ + Name: "custom_price_currency", + Usage: "The currency that the custom prices are " + + "quoted in. This is only required if " + + "'fiat_backend' is set to 'custom'.", + }, }, Action: queryFiatEstimate, } @@ -55,11 +67,30 @@ func queryFiatEstimate(ctx *cli.Context) error { return err } + // nolint: prealloc + var filteredPrices []*frdrpc.BitcoinPrice + + if fiatBackend == frdrpc.FiatBackend_CUSTOM { + customPrices, err := parsePricesFromCSV( + ctx.String("prices_csv_path"), + ctx.String("custom_price_currency"), + ) + if err != nil { + return err + } + + filteredPrices, err = filterPrices(customPrices, ts, ts) + if err != nil { + return err + } + } + // Set start and end times from user specified values, defaulting // to zero if they are not set. req := &frdrpc.ExchangeRateRequest{ - Timestamps: []uint64{uint64(ts)}, - FiatBackend: fiatBackend, + Timestamps: []uint64{uint64(ts)}, + FiatBackend: fiatBackend, + CustomPrices: filteredPrices, } rpcCtx := context.Background() @@ -85,10 +116,11 @@ func queryFiatEstimate(ctx *cli.Context) error { return err } - usdVal := fiat.MsatToUSD(bitcoinPrice, lnwire.MilliSatoshi(amt)) + fiatVal := fiat.MsatToFiat(bitcoinPrice, lnwire.MilliSatoshi(amt)) priceTs := time.Unix(int64(estimate.BtcPrice.PriceTimestamp), 0) - fmt.Printf("%v msat = %v USD, priced at %v\n", amt, usdVal, priceTs) + fmt.Printf("%v msat = %v %s, priced at %v\n", + amt, fiatVal, estimate.BtcPrice.Currency, priceTs) return nil } diff --git a/cmd/frcli/node_audit.go b/cmd/frcli/node_audit.go index 38311c7..344db9d 100644 --- a/cmd/frcli/node_audit.go +++ b/cmd/frcli/node_audit.go @@ -96,7 +96,23 @@ var onChainReportCommand = cli.Command{ cli.StringFlag{ Name: "fiat_backend", Usage: "fiat backend to be used. Options include: " + - "'coincap' (default) and 'coindesk'", + "'coincap' (default), 'coindesk' or " + + "'custom' which allows custom price data to " + + "be used. The 'custom' option requires the" + + "'prices_csv_path' and " + + "'custom_price_currency' options to be set.", + }, + cli.StringFlag{ + Name: "prices_csv_path", + Usage: "Path to a CSV file containing custom fiat " + + "price data. This is only required if " + + "'fiat_backend' is set to 'custom'.", + }, + cli.StringFlag{ + Name: "custom_price_currency", + Usage: "The currency that the custom prices are " + + "quoted in. This is only required if " + + "'fiat_backend' is set to 'custom'.", }, }, Action: queryOnChainReport, @@ -111,13 +127,37 @@ func queryOnChainReport(ctx *cli.Context) error { return err } + startTime := ctx.Int64("start_time") + endTime := ctx.Int64("end_time") + + // nolint: prealloc + var filteredPrices []*frdrpc.BitcoinPrice + + if fiatBackend == frdrpc.FiatBackend_CUSTOM { + customPrices, err := parsePricesFromCSV( + ctx.String("prices_csv_path"), + ctx.String("custom_price_currency"), + ) + if err != nil { + return err + } + + filteredPrices, err = filterPrices( + customPrices, startTime, endTime, + ) + if err != nil { + return err + } + } + // Set start and end times from user specified values, defaulting // to zero if they are not set. req := &frdrpc.NodeAuditRequest{ - StartTime: uint64(ctx.Int64("start_time")), - EndTime: uint64(ctx.Int64("end_time")), - DisableFiat: !ctx.IsSet("enable_fiat"), - FiatBackend: fiatBackend, + StartTime: uint64(startTime), + EndTime: uint64(endTime), + DisableFiat: !ctx.IsSet("enable_fiat"), + FiatBackend: fiatBackend, + CustomPrices: filteredPrices, } // If start time is zero, default to a week ago. @@ -194,9 +234,17 @@ func queryOnChainReport(ctx *cli.Context) error { } }() - csvStrs := []string{CSVHeaders} + var headers string + if len(report.Reports) > 0 { + headers = fmt.Sprintf( + CSVHeaders, + report.Reports[0].BtcPrice.Currency, + ) + } + + csvStrs := []string{headers} for _, report := range report.Reports { - csvStrs = append(csvStrs, csv(report)) + csvStrs = append(csvStrs, writeToCSV(report)) } csvString := strings.Join(csvStrs, "\n") diff --git a/cmd/frcli/utils.go b/cmd/frcli/utils.go index 6bb9282..41d6dba 100644 --- a/cmd/frcli/utils.go +++ b/cmd/frcli/utils.go @@ -4,13 +4,18 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io/ioutil" "net" "os" "path/filepath" + "sort" "strconv" "strings" + "time" + + "github.com/lightninglabs/faraday/utils" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -288,9 +293,66 @@ func parseFiatBackend(fiatBackend string) (frdrpc.FiatBackend, error) { case fiat.CoinDeskPriceBackend.String(): return frdrpc.FiatBackend_COINDESK, nil + case fiat.CustomPriceBackend.String(): + return frdrpc.FiatBackend_CUSTOM, nil + default: return frdrpc.FiatBackend_UNKNOWN_FIATBACKEND, fmt.Errorf( "unknown fiat backend", ) } } + +// filterPrices filters a slice of prices based on given start and end +// timestamps. +func filterPrices(prices []*frdrpc.BitcoinPrice, startTime, endTime int64) ( + []*frdrpc.BitcoinPrice, error) { + + // Ensure that startTime is before endTime. + if err := utils.ValidateTimeRange( + time.Unix(startTime, 0), time.Unix(endTime, 0), + utils.DisallowFutureRange, + ); err != nil { + return nil, err + } + + // Sort the prices by timestamp. + sort.SliceStable(prices, func(i, j int) bool { + return prices[i].PriceTimestamp < prices[j].PriceTimestamp + }) + + // Filter out timestamps that are not within the start time to + // end time range but ensure that the timestamp right before + // or equal to the start timestamp is kept. + // + // nolint: prealloc + var ( + filteredPrices []*frdrpc.BitcoinPrice + earliestTimeStamp *frdrpc.BitcoinPrice + ) + for _, p := range prices { + if p.PriceTimestamp <= uint64(startTime) { + if earliestTimeStamp == nil || + earliestTimeStamp.PriceTimestamp < + p.PriceTimestamp { + + earliestTimeStamp = p + } + continue + } + + if p.PriceTimestamp >= uint64(endTime) { + continue + } + + filteredPrices = append(filteredPrices, p) + } + + if earliestTimeStamp == nil { + return nil, errors.New("a price point with a timestamp " + + "earlier than the given start timestamp is required") + } + + return append([]*frdrpc.BitcoinPrice{earliestTimeStamp}, + filteredPrices...), nil +} diff --git a/cmd/frcli/utils_test.go b/cmd/frcli/utils_test.go new file mode 100644 index 0000000..6bf3309 --- /dev/null +++ b/cmd/frcli/utils_test.go @@ -0,0 +1,120 @@ +package main + +import ( + "testing" + + "github.com/lightninglabs/faraday/frdrpc" +) + +// TestFilterPrices checks that the filterPrices function correctly filters +// prices based on given start and end timestamps. +func TestFilterPrices(t *testing.T) { + tests := []struct { + name string + prices []*frdrpc.BitcoinPrice + startTime int64 + endTime int64 + expectedPrices []*frdrpc.BitcoinPrice + expectErr bool + }{ + { + name: "test that prices are sorted correctly", + prices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 200}, + {PriceTimestamp: 300}, + {PriceTimestamp: 100}, + }, + startTime: 100, + endTime: 400, + expectedPrices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 100}, + {PriceTimestamp: 200}, + {PriceTimestamp: 300}, + }, + }, + { + name: "error if end time is before start time", + prices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 100}, + {PriceTimestamp: 200}, + {PriceTimestamp: 300}, + }, + startTime: 200, + endTime: 100, + expectErr: true, + }, + { + name: "error if no timestamp before or equal to start " + + "time is provided", + prices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 100}, + {PriceTimestamp: 200}, + }, + startTime: 50, + endTime: 100, + expectErr: true, + }, + { + name: "check correct filtering of prices", + prices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 100}, + {PriceTimestamp: 200}, + {PriceTimestamp: 300}, + {PriceTimestamp: 400}, + {PriceTimestamp: 500}, + {PriceTimestamp: 600}, + }, + startTime: 250, + endTime: 400, + expectedPrices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 200}, + {PriceTimestamp: 300}, + }, + }, + { + name: "equal start and end timestamps", + prices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 100}, + {PriceTimestamp: 200}, + {PriceTimestamp: 300}, + }, + startTime: 200, + endTime: 200, + expectedPrices: []*frdrpc.BitcoinPrice{ + {PriceTimestamp: 200}, + }, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + prices, err := filterPrices( + test.prices, test.startTime, test.endTime, + ) + if err != nil { + if test.expectErr { + return + } + t.Fatalf("expected no error, got: %v", err) + } + + if len(prices) != len(test.expectedPrices) { + t.Fatalf("expected %d prices, got %d", + len(test.expectedPrices), len(prices)) + } + + for i, p := range prices { + if p.PriceTimestamp != test.expectedPrices[i].PriceTimestamp { + t.Fatalf("expected timestamp "+ + "%d at index %d, got timestamp %d", + test.expectedPrices[i].PriceTimestamp, + i, p.PriceTimestamp) + } + } + }) + } +} diff --git a/docs/accounting.md b/docs/accounting.md index 382f22e..891d687 100644 --- a/docs/accounting.md +++ b/docs/accounting.md @@ -9,7 +9,7 @@ It is strongly recommended that Faraday is run with a connection to a Bitcon nod ## Common Fields For brevity, the following fields which have the same meaning for each entry will be omitted: - Timestamp: The timestamp of the block that the channel open transaction appeared in. -- Fiat: The value of the amount field in USD. Note that values less than one satoshi will be rounded down to zero. +- Fiat: The value of the amount field in specified currency. Note that values less than one satoshi will be rounded down to zero. - OnChain: Whether the transaction occurred off chain, or on chain. - Credit: True when an entry increased our balances, false when an entry decreased our balances. diff --git a/fiat/coincap_api.go b/fiat/coincap_api.go index 497da65..78cad35 100644 --- a/fiat/coincap_api.go +++ b/fiat/coincap_api.go @@ -15,6 +15,10 @@ import ( const ( // coinCapHistoryAPI is the endpoint we hit for historical price data. coinCapHistoryAPI = "https://api.coincap.io/v2/assets/bitcoin/history" + + // coinCapDefaultCurrency is the currency that the price data returned + // by the Coin Cap API is quoted in. + coinCapDefaultCurrency = "USD" ) // ErrQueryTooLong is returned when we cannot get a granularity level for a @@ -127,7 +131,7 @@ type coinCapAPI struct { // convert produces usd prices from the output of the query function. // It is set within the struct so that it can be mocked for testing. - convert func([]byte) ([]*USDPrice, error) + convert func([]byte) ([]*Price, error) } // newCoinCapAPI returns a coin cap api struct which can be used to query @@ -176,13 +180,13 @@ type coinCapDataPoint struct { // parseCoinCapData parses http response data to usc price structs, using // intermediary structs to get around parsing. -func parseCoinCapData(data []byte) ([]*USDPrice, error) { +func parseCoinCapData(data []byte) ([]*Price, error) { var priceEntries coinCapResponse if err := json.Unmarshal(data, &priceEntries); err != nil { return nil, err } - var usdRecords = make([]*USDPrice, len(priceEntries.Data)) + var usdRecords = make([]*Price, len(priceEntries.Data)) // Convert each entry from the api to a usable record with a converted // time and parsed price. @@ -193,9 +197,10 @@ func parseCoinCapData(data []byte) ([]*USDPrice, error) { } ns := time.Duration(entry.Timestamp) * time.Millisecond - usdRecords[i] = &USDPrice{ + usdRecords[i] = &Price{ Timestamp: time.Unix(0, ns.Nanoseconds()), Price: decPrice, + Currency: coinCapDefaultCurrency, } } @@ -206,7 +211,7 @@ func parseCoinCapData(data []byte) ([]*USDPrice, error) { // requested is more than coincap will serve us in a single request, we break // our queries up into multiple chunks. func (c *coinCapAPI) rawPriceData(ctx context.Context, startTime, - endTime time.Time) ([]*USDPrice, error) { + endTime time.Time) ([]*Price, error) { // When we query prices over a range, it is likely that the first data // point we get is after our starting point, since we have discrete @@ -217,7 +222,7 @@ func (c *coinCapAPI) rawPriceData(ctx context.Context, startTime, // so that we do not have overlapping data across queries. startTime = startTime.Add(c.granularity.aggregation * -1) - var historicalRecords []*USDPrice + var historicalRecords []*Price // Create start and end vars to query one maximum length at a time. maxPeriod := c.granularity.maximumQuery diff --git a/fiat/coincap_api_test.go b/fiat/coincap_api_test.go index 2163acc..8cd97d4 100644 --- a/fiat/coincap_api_test.go +++ b/fiat/coincap_api_test.go @@ -74,7 +74,7 @@ func TestCoinCapGetPrices(t *testing.T) { } // Create a mocked convert function. - convert := func([]byte) ([]*USDPrice, error) { + convert := func([]byte) ([]*Price, error) { return nil, nil } @@ -189,14 +189,16 @@ func TestParseCoinCapData(t *testing.T) { prices, err := parseCoinCapData(bytes) require.NoError(t, err) - expectedPrices := []*USDPrice{ + expectedPrices := []*Price{ { Price: price1, Timestamp: time1, + Currency: coinCapDefaultCurrency, }, { Price: price2, Timestamp: time2, + Currency: coinCapDefaultCurrency, }, } diff --git a/fiat/coindesk_api.go b/fiat/coindesk_api.go index 37011e0..894c3f3 100644 --- a/fiat/coindesk_api.go +++ b/fiat/coindesk_api.go @@ -17,6 +17,10 @@ const ( // coinDeskTimeFormat is the date format used by coindesk. coinDeskTimeFormat = "2006-01-02" + + // coinDeskDefaultCurrency is the default currency that the price data + // returned by the Coin Desk API is quoted in. + coinDeskDefaultCurrency = "USD" ) // coinDeskAPI implements the fiatBackend interface. @@ -46,15 +50,15 @@ func queryCoinDesk(start, end time.Time) ([]byte, error) { return ioutil.ReadAll(response.Body) } -// parseCoinDeskData parses http response data from coindesk into USDPrice +// parseCoinDeskData parses http response data from coindesk into Price // structs. -func parseCoinDeskData(data []byte) ([]*USDPrice, error) { +func parseCoinDeskData(data []byte) ([]*Price, error) { var priceEntries coinDeskResponse if err := json.Unmarshal(data, &priceEntries); err != nil { return nil, err } - var usdRecords = make([]*USDPrice, 0, len(priceEntries.Data)) + var usdRecords = make([]*Price, 0, len(priceEntries.Data)) for date, price := range priceEntries.Data { timestamp, err := time.Parse(coinDeskTimeFormat, date) @@ -62,9 +66,10 @@ func parseCoinDeskData(data []byte) ([]*USDPrice, error) { return nil, err } - usdRecords = append(usdRecords, &USDPrice{ + usdRecords = append(usdRecords, &Price{ Timestamp: timestamp, Price: decimal.NewFromFloat(price), + Currency: coinDeskDefaultCurrency, }) } @@ -74,7 +79,7 @@ func parseCoinDeskData(data []byte) ([]*USDPrice, error) { // rawPriceData retrieves price information from coindesks's api for the given // time range. func (c *coinDeskAPI) rawPriceData(ctx context.Context, start, - end time.Time) ([]*USDPrice, error) { + end time.Time) ([]*Price, error) { query := func() ([]byte, error) { return queryCoinDesk(start, end) diff --git a/fiat/coindesk_api_test.go b/fiat/coindesk_api_test.go index 11a5c2e..a648210 100644 --- a/fiat/coindesk_api_test.go +++ b/fiat/coindesk_api_test.go @@ -55,15 +55,17 @@ func TestParseCoinDeskData(t *testing.T) { prices, err := parseCoinDeskData(bytes) require.NoError(t, err) - expectedPrices := []*USDPrice{ + expectedPrices := []*Price{ { Price: price, Timestamp: timestamp, + Currency: coinDeskDefaultCurrency, }, } require.True(t, expectedPrices[0].Price.Equal(prices[0].Price)) require.True(t, expectedPrices[0].Timestamp.Equal(prices[0].Timestamp)) + require.Equal(t, expectedPrices[0].Currency, prices[0].Currency) }) } } diff --git a/fiat/customprices.go b/fiat/customprices.go new file mode 100644 index 0000000..60a3d84 --- /dev/null +++ b/fiat/customprices.go @@ -0,0 +1,18 @@ +package fiat + +import ( + "context" + "time" +) + +// customPrices implements the fiatBackend interface. +type customPrices struct { + entries []*Price +} + +// rawPriceData returns the custom price point entries. +func (c *customPrices) rawPriceData(_ context.Context, _, + _ time.Time) ([]*Price, error) { + + return c.entries, nil +} diff --git a/fiat/fiat.go b/fiat/fiat.go index 87d8450..2fb0edc 100644 --- a/fiat/fiat.go +++ b/fiat/fiat.go @@ -23,13 +23,17 @@ var ( errRetriesFailed = errors.New("could not get data within max retries") ) -// USDPrice represents the Bitcoin price in USD at a certain time. -type USDPrice struct { +// Price represents the Bitcoin price in the given currency at a certain time. +type Price struct { // Timestamp is the time at which the BTC price is quoted. Timestamp time.Time - // Price is the price in USD for 1 BTC at the given timestamp. + // Price is the fiat price for the given currency for 1 BTC at the + // given timestamp. Price decimal.Decimal + + // Currency is the code of the currency that the Price is quoted in. + Currency string } // retryQuery calls an api until it succeeds, or we hit our maximum retries. @@ -37,7 +41,7 @@ type USDPrice struct { // context passed in. It takes query and convert functions as parameters for // testing purposes. func retryQuery(ctx context.Context, queryAPI func() ([]byte, error), - convert func([]byte) ([]*USDPrice, error)) ([]*USDPrice, error) { + convert func([]byte) ([]*Price, error)) ([]*Price, error) { for i := 0; i < maxRetries; i++ { // If our request fails, log the error, sleep for the retry diff --git a/fiat/fiat_test.go b/fiat/fiat_test.go index 3f5b5ee..8de4e67 100644 --- a/fiat/fiat_test.go +++ b/fiat/fiat_test.go @@ -101,7 +101,7 @@ func TestRetryQuery(t *testing.T) { } // Create a mocked parse call which acts as a nop. - parse := func([]byte) ([]*USDPrice, error) { + parse := func([]byte) ([]*Price, error) { return nil, nil } diff --git a/fiat/prices.go b/fiat/prices.go index b92e83c..2103ba1 100644 --- a/fiat/prices.go +++ b/fiat/prices.go @@ -22,13 +22,19 @@ var ( // required fiat prices but the granularity of those prices is not set. errGranularityRequired = errors.New("granularity required when " + "fiat prices are enabled") + + errPricePointsRequired = errors.New("at least one price point " + + "required for a custom price backend") + + errPriceSourceConfigExpected = errors.New("a non-nil " + + "PriceSourceConfig is expected") ) // fiatBackend is an interface that must be implemented by any backend that // is used to fetch fiat price information. type fiatBackend interface { rawPriceData(ctx context.Context, startTime, - endTime time.Time) ([]*USDPrice, error) + endTime time.Time) ([]*Price, error) } // PriceSource holds a fiatBackend that can be used to fetch fiat price @@ -37,11 +43,45 @@ type PriceSource struct { impl fiatBackend } +// PriceSourceConfig is a struct holding various config options used for +// initialising a new PriceSource. +type PriceSourceConfig struct { + // Backend is the PriceBackend to be used for fetching price data. + Backend PriceBackend + + // Granularity specifies the level of granularity with which we want to + // get fiat prices. This option is only used for the CoinCap + // PriceBackend. + Granularity *Granularity + + // PricePoints is a set of price points that is used for fiat related + // queries if the PriceBackend being used is the CustomPriceBackend. + PricePoints []*Price +} + +// validatePriceSourceConfig checks that the PriceSourceConfig fields are valid +// given the chosen price backend. +func (cfg *PriceSourceConfig) validatePriceSourceConfig() error { + switch cfg.Backend { + case UnknownPriceBackend, CoinCapPriceBackend: + if cfg.Granularity == nil { + return errGranularityRequired + } + + case CustomPriceBackend: + if len(cfg.PricePoints) == 0 { + return errPricePointsRequired + } + } + + return nil +} + // GetPrices fetches price information using the given the PriceSource // fiatBackend implementation. GetPrices also validates the time parameters and // sorts the results. func (p PriceSource) GetPrices(ctx context.Context, startTime, - endTime time.Time) ([]*USDPrice, error) { + endTime time.Time) ([]*Price, error) { // First, check that we have a valid start and end time, and that the // range specified is not in the future. @@ -83,12 +123,16 @@ const ( // CoinDeskPriceBackend uses CoinDesk's API for fiat price data. CoinDeskPriceBackend + + // CustomPriceBackend uses user provided fiat price data. + CustomPriceBackend ) var priceBackendNames = map[PriceBackend]string{ UnknownPriceBackend: "unknown", CoinCapPriceBackend: "coincap", CoinDeskPriceBackend: "coindesk", + CustomPriceBackend: "custom", } // String returns the string representation of a price backend. @@ -98,22 +142,32 @@ func (p PriceBackend) String() string { // NewPriceSource returns a PriceSource which can be used to query price // data. -func NewPriceSource(backend PriceBackend, granularity *Granularity) ( - *PriceSource, error) { +func NewPriceSource(cfg *PriceSourceConfig) (*PriceSource, error) { + if cfg == nil { + return nil, errPriceSourceConfigExpected + } - switch backend { + if err := cfg.validatePriceSourceConfig(); err != nil { + return nil, err + } + + switch cfg.Backend { case UnknownPriceBackend, CoinCapPriceBackend: - if granularity == nil { - return nil, errGranularityRequired - } return &PriceSource{ - impl: newCoinCapAPI(*granularity), + impl: newCoinCapAPI(*cfg.Granularity), }, nil case CoinDeskPriceBackend: return &PriceSource{ impl: &coinDeskAPI{}, }, nil + + case CustomPriceBackend: + return &PriceSource{ + impl: &customPrices{ + entries: cfg.PricePoints, + }, + }, nil } return nil, errUnknownPriceBackend @@ -133,8 +187,8 @@ type PriceRequest struct { // GetPrices gets a set of prices for a set of timestamps. func GetPrices(ctx context.Context, timestamps []time.Time, - backend PriceBackend, granularity Granularity) ( - map[time.Time]*USDPrice, error) { + priceCfg *PriceSourceConfig) ( + map[time.Time]*Price, error) { if len(timestamps) == 0 { return nil, nil @@ -152,7 +206,7 @@ func GetPrices(ctx context.Context, timestamps []time.Time, // timestamp if we have 1 entry, but that's ok. start, end := timestamps[0], timestamps[len(timestamps)-1] - client, err := NewPriceSource(backend, &granularity) + client, err := NewPriceSource(priceCfg) if err != nil { return nil, err } @@ -162,8 +216,8 @@ func GetPrices(ctx context.Context, timestamps []time.Time, return nil, err } - // Prices will map transaction timestamps to their USD prices. - var prices = make(map[time.Time]*USDPrice, len(timestamps)) + // Prices will map transaction timestamps to their fiat prices. + var prices = make(map[time.Time]*Price, len(timestamps)) for _, ts := range timestamps { price, err := GetPrice(priceData, ts) @@ -177,9 +231,9 @@ func GetPrices(ctx context.Context, timestamps []time.Time, return prices, nil } -// MsatToUSD converts a msat amount to usd. Note that this function coverts +// MsatToFiat converts a msat amount to fiat. Note that this function converts // values to Bitcoin values, then gets the fiat price for that BTC value. -func MsatToUSD(price decimal.Decimal, amt lnwire.MilliSatoshi) decimal.Decimal { +func MsatToFiat(price decimal.Decimal, amt lnwire.MilliSatoshi) decimal.Decimal { msatDecimal := decimal.NewFromInt(int64(amt)) // We are quoted price per whole bitcoin. We need to scale this price @@ -195,12 +249,12 @@ func MsatToUSD(price decimal.Decimal, amt lnwire.MilliSatoshi) decimal.Decimal { // querying. The last datapoint's timestamp may be before the timestamp we are // querying. If a request lies between two price points, we just return the // earlier price. -func GetPrice(prices []*USDPrice, timestamp time.Time) (*USDPrice, error) { +func GetPrice(prices []*Price, timestamp time.Time) (*Price, error) { if len(prices) == 0 { return nil, errNoPrices } - var lastPrice *USDPrice + var lastPrice *Price // Run through our prices until we find a timestamp that our price // point lies before. Since we always return the previous price, this diff --git a/fiat/prices_test.go b/fiat/prices_test.go index f9bc13b..ca94089 100644 --- a/fiat/prices_test.go +++ b/fiat/prices_test.go @@ -1,6 +1,7 @@ package fiat import ( + "errors" "testing" "time" @@ -18,22 +19,22 @@ func TestGetPrice(t *testing.T) { price10K := decimal.New(10000, 1) price20K := decimal.New(20000, 1) - now10k := &USDPrice{ + now10k := &Price{ Timestamp: now, Price: price10K, } - hourAgo20K := &USDPrice{ + hourAgo20K := &Price{ Timestamp: oneHourAgo, Price: price20K, } tests := []struct { name string - prices []*USDPrice + prices []*Price request time.Time expectedErr error - expectedPrice *USDPrice + expectedPrice *Price }{ { name: "no prices", @@ -43,21 +44,21 @@ func TestGetPrice(t *testing.T) { }, { name: "timestamp before range", - prices: []*USDPrice{now10k}, + prices: []*Price{now10k}, request: oneHourAgo, expectedErr: errPriceOutOfRange, expectedPrice: nil, }, { name: "timestamp equals data point timestamp", - prices: []*USDPrice{hourAgo20K, now10k}, + prices: []*Price{hourAgo20K, now10k}, request: now, expectedErr: nil, expectedPrice: now10k, }, { name: "timestamp after range", - prices: []*USDPrice{ + prices: []*Price{ { Timestamp: twoHoursAgo, Price: price10K, @@ -70,7 +71,7 @@ func TestGetPrice(t *testing.T) { }, { name: "timestamp between prices, pick earlier", - prices: []*USDPrice{hourAgo20K, now10k}, + prices: []*Price{hourAgo20K, now10k}, request: now.Add(time.Minute * -30), expectedErr: nil, expectedPrice: hourAgo20K, @@ -92,8 +93,8 @@ func TestGetPrice(t *testing.T) { } } -// TestMSatToUsd tests conversion of msat to usd. This -func TestMSatToUsd(t *testing.T) { +// TestMSatToFiat tests conversion of msat to fiat. This +func TestMSatToFiat(t *testing.T) { tests := []struct { name string amount lnwire.MilliSatoshi @@ -126,7 +127,7 @@ func TestMSatToUsd(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - amt := MsatToUSD(test.price, test.amount) + amt := MsatToFiat(test.price, test.amount) if !amt.Equals(test.expectedFiat) { t.Fatalf("expected: %v, got: %v", test.expectedFiat, amt) @@ -134,3 +135,78 @@ func TestMSatToUsd(t *testing.T) { }) } } + +// TestValidatePriceSourceConfig tests that the validatePriceSourceConfig +// function correctly validates the fields of PriceSourceConfig given the +// chosen price backend. +func TestValidatePriceSourceConfig(t *testing.T) { + tests := []struct { + name string + cfg *PriceSourceConfig + expectedErr error + }{ + { + name: "valid Coin Cap config", + cfg: &PriceSourceConfig{ + Backend: CoinCapPriceBackend, + Granularity: &GranularityDay, + }, + }, + { + name: "invalid Coin Cap config", + cfg: &PriceSourceConfig{ + Backend: CoinCapPriceBackend, + }, + expectedErr: errGranularityRequired, + }, + { + name: "valid default config", + cfg: &PriceSourceConfig{ + Backend: UnknownPriceBackend, + Granularity: &GranularityDay, + }, + }, + { + name: "invalid default config", + cfg: &PriceSourceConfig{ + Backend: UnknownPriceBackend, + }, + expectedErr: errGranularityRequired, + }, + { + name: "valid custom prices config", + cfg: &PriceSourceConfig{ + Backend: CustomPriceBackend, + PricePoints: []*Price{ + { + Timestamp: time.Now(), + Price: decimal.NewFromInt(10), + Currency: "USD", + }, + }, + }, + }, + { + name: "invalid custom prices config", + cfg: &PriceSourceConfig{ + Backend: CustomPriceBackend, + }, + expectedErr: errPricePointsRequired, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + err := test.cfg.validatePriceSourceConfig() + if !errors.Is(err, test.expectedErr) { + t.Fatalf("expected: %v, got %v", + test.expectedErr, err) + } + }) + + } +} diff --git a/frdrpc/exchange_rate.go b/frdrpc/exchange_rate.go index 18d73ca..7720538 100644 --- a/frdrpc/exchange_rate.go +++ b/frdrpc/exchange_rate.go @@ -70,6 +70,9 @@ func fiatBackendFromRPC(backend FiatBackend) (fiat.PriceBackend, error) { case FiatBackend_COINDESK: return fiat.CoinDeskPriceBackend, nil + case FiatBackend_CUSTOM: + return fiat.CustomPriceBackend, nil + default: return fiat.UnknownPriceBackend, fmt.Errorf("unknown fiat backend: %v", backend) @@ -77,11 +80,10 @@ func fiatBackendFromRPC(backend FiatBackend) (fiat.PriceBackend, error) { } func parseExchangeRateRequest(req *ExchangeRateRequest) ([]time.Time, - fiat.PriceBackend, *fiat.Granularity, error) { + *fiat.PriceSourceConfig, error) { if len(req.Timestamps) == 0 { - return nil, fiat.UnknownPriceBackend, nil, - errors.New("at least one timestamp required") + return nil, nil, errors.New("at least one timestamp required") } timestamps := make([]time.Time, len(req.Timestamps)) @@ -104,18 +106,27 @@ func parseExchangeRateRequest(req *ExchangeRateRequest) ([]time.Time, req.Granularity, false, end.Sub(start), ) if err != nil { - return nil, fiat.UnknownPriceBackend, nil, err + return nil, nil, err } fiatBackend, err := fiatBackendFromRPC(req.FiatBackend) if err != nil { - return nil, fiat.UnknownPriceBackend, nil, err + return nil, nil, err + } + + pricePoints, err := pricePointsFromRPC(req.CustomPrices) + if err != nil { + return nil, nil, err } - return timestamps, fiatBackend, granularity, nil + return timestamps, &fiat.PriceSourceConfig{ + Backend: fiatBackend, + Granularity: granularity, + PricePoints: pricePoints, + }, nil } -func exchangeRateResponse(prices map[time.Time]*fiat.USDPrice) *ExchangeRateResponse { +func exchangeRateResponse(prices map[time.Time]*fiat.Price) *ExchangeRateResponse { fiatVals := make([]*ExchangeRate, 0, len(prices)) for ts, price := range prices { @@ -124,6 +135,7 @@ func exchangeRateResponse(prices map[time.Time]*fiat.USDPrice) *ExchangeRateResp BtcPrice: &BitcoinPrice{ Price: price.Price.String(), PriceTimestamp: uint64(price.Timestamp.Unix()), + Currency: price.Currency, }, }) } diff --git a/frdrpc/faraday.pb.go b/frdrpc/faraday.pb.go index 51cc088..0736ccc 100644 --- a/frdrpc/faraday.pb.go +++ b/frdrpc/faraday.pb.go @@ -105,6 +105,8 @@ const ( // This API is reached through the following URL: // https://api.coindesk.com/v1/bpi/historical/close.json FiatBackend_COINDESK FiatBackend = 2 + // Use custom price data provided in a CSV file for fiat price information. + FiatBackend_CUSTOM FiatBackend = 3 ) // Enum value maps for FiatBackend. @@ -113,11 +115,13 @@ var ( 0: "UNKNOWN_FIATBACKEND", 1: "COINCAP", 2: "COINDESK", + 3: "CUSTOM", } FiatBackend_value = map[string]int32{ "UNKNOWN_FIATBACKEND": 0, "COINCAP": 1, "COINDESK": 2, + "CUSTOM": 3, } ) @@ -1152,6 +1156,8 @@ type ExchangeRateRequest struct { Granularity Granularity `protobuf:"varint,4,opt,name=granularity,proto3,enum=frdrpc.Granularity" json:"granularity,omitempty"` // The api to be used for fiat related queries. FiatBackend FiatBackend `protobuf:"varint,5,opt,name=fiat_backend,json=fiatBackend,proto3,enum=frdrpc.FiatBackend" json:"fiat_backend,omitempty"` + // Custom price points to use if the CUSTOM FiatBackend option is set. + CustomPrices []*BitcoinPrice `protobuf:"bytes,8,rep,name=custom_prices,json=customPrices,proto3" json:"custom_prices,omitempty"` } func (x *ExchangeRateRequest) Reset() { @@ -1207,6 +1213,13 @@ func (x *ExchangeRateRequest) GetFiatBackend() FiatBackend { return FiatBackend_UNKNOWN_FIATBACKEND } +func (x *ExchangeRateRequest) GetCustomPrices() []*BitcoinPrice { + if x != nil { + return x.CustomPrices + } + return nil +} + type ExchangeRateResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1264,6 +1277,8 @@ type BitcoinPrice struct { Price string `protobuf:"bytes,1,opt,name=price,proto3" json:"price,omitempty"` // The timestamp for this price price provided. PriceTimestamp uint64 `protobuf:"varint,2,opt,name=price_timestamp,json=priceTimestamp,proto3" json:"price_timestamp,omitempty"` + // The currency that the price is denoted in. + Currency string `protobuf:"bytes,3,opt,name=currency,proto3" json:"currency,omitempty"` } func (x *BitcoinPrice) Reset() { @@ -1312,6 +1327,13 @@ func (x *BitcoinPrice) GetPriceTimestamp() uint64 { return 0 } +func (x *BitcoinPrice) GetCurrency() string { + if x != nil { + return x.Currency + } + return "" +} + type ExchangeRate struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1395,6 +1417,8 @@ type NodeAuditRequest struct { CustomCategories []*CustomCategory `protobuf:"bytes,6,rep,name=custom_categories,json=customCategories,proto3" json:"custom_categories,omitempty"` // The api to be used for fiat related queries. FiatBackend FiatBackend `protobuf:"varint,7,opt,name=fiat_backend,json=fiatBackend,proto3,enum=frdrpc.FiatBackend" json:"fiat_backend,omitempty"` + // Custom price points to use if the CUSTOM FiatBackend option is set. + CustomPrices []*BitcoinPrice `protobuf:"bytes,8,rep,name=custom_prices,json=customPrices,proto3" json:"custom_prices,omitempty"` } func (x *NodeAuditRequest) Reset() { @@ -1471,6 +1495,13 @@ func (x *NodeAuditRequest) GetFiatBackend() FiatBackend { return FiatBackend_UNKNOWN_FIATBACKEND } +func (x *NodeAuditRequest) GetCustomPrices() []*BitcoinPrice { + if x != nil { + return x.CustomPrices + } + return nil +} + type CustomCategory struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1585,13 +1616,14 @@ type ReportEntry struct { CustomCategory string `protobuf:"bytes,12,opt,name=custom_category,json=customCategory,proto3" json:"custom_category,omitempty"` // The transaction id of the entry. Txid string `protobuf:"bytes,7,opt,name=txid,proto3" json:"txid,omitempty"` - // The fiat amount of the entry's amount in USD. + // The fiat amount of the entry's amount in the currency specified in the + // btc_price field. Fiat string `protobuf:"bytes,8,opt,name=fiat,proto3" json:"fiat,omitempty"` // A unique identifier for the entry, if available. Reference string `protobuf:"bytes,9,opt,name=reference,proto3" json:"reference,omitempty"` // An additional note for the entry, providing additional context. Note string `protobuf:"bytes,10,opt,name=note,proto3" json:"note,omitempty"` - // The bitcoin price and timestamp used to calcualte our fiat value. + // The bitcoin price and timestamp used to calculate our fiat value. BtcPrice *BitcoinPrice `protobuf:"bytes,11,opt,name=btc_price,json=btcPrice,proto3" json:"btc_price,omitempty"` } @@ -2030,7 +2062,7 @@ var file_faraday_proto_rawDesc = []byte{ 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x13, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x22, 0xeb, 0x01, 0x0a, 0x13, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0a, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x73, 0x12, 0x35, 0x0a, @@ -2040,169 +2072,179 @@ var file_faraday_proto_rawDesc = []byte{ 0x72, 0x69, 0x74, 0x79, 0x12, 0x36, 0x0a, 0x0c, 0x66, 0x69, 0x61, 0x74, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x52, - 0x0b, 0x66, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x4a, 0x04, 0x08, 0x01, - 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, 0x10, 0x03, 0x22, 0x48, 0x0a, 0x14, 0x45, 0x78, 0x63, 0x68, - 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x2a, 0x0a, 0x05, 0x72, 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x14, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, - 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x05, 0x72, 0x61, 0x74, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x01, - 0x10, 0x02, 0x22, 0x4d, 0x0a, 0x0c, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, - 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x63, - 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x0e, 0x70, 0x72, 0x69, 0x63, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x22, 0x5f, 0x0a, 0x0c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, - 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, - 0x31, 0x0a, 0x09, 0x62, 0x74, 0x63, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x0b, 0x66, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0d, + 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x69, 0x74, + 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x52, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x50, 0x72, 0x69, 0x63, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, + 0x02, 0x10, 0x03, 0x22, 0x48, 0x0a, 0x14, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x05, 0x72, + 0x61, 0x74, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x66, 0x72, 0x64, + 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x72, 0x61, 0x74, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x22, 0x69, 0x0a, + 0x0c, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x70, 0x72, + 0x69, 0x63, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x70, 0x72, + 0x69, 0x63, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x1a, 0x0a, 0x08, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x22, 0x5f, 0x0a, 0x0c, 0x45, 0x78, 0x63, 0x68, + 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x31, 0x0a, 0x09, 0x62, 0x74, 0x63, 0x5f, 0x70, 0x72, + 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x66, 0x72, 0x64, 0x72, + 0x70, 0x63, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x52, + 0x08, 0x62, 0x74, 0x63, 0x50, 0x72, 0x69, 0x63, 0x65, 0x22, 0xe4, 0x02, 0x0a, 0x10, 0x4e, 0x6f, + 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, + 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, + 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, + 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x46, 0x69, 0x61, 0x74, 0x12, 0x35, 0x0a, 0x0b, 0x67, + 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x13, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x75, 0x6c, + 0x61, 0x72, 0x69, 0x74, 0x79, 0x52, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, + 0x74, 0x79, 0x12, 0x43, 0x0a, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, + 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x12, 0x36, 0x0a, 0x0c, 0x66, 0x69, 0x61, 0x74, 0x5f, + 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, + 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, + 0x6e, 0x64, 0x52, 0x0b, 0x66, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x12, + 0x39, 0x0a, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x73, + 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, + 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x52, 0x0c, 0x63, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x50, 0x72, 0x69, 0x63, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, + 0x22, 0x83, 0x01, 0x0a, 0x0e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6f, 0x6e, 0x43, 0x68, 0x61, + 0x69, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x66, 0x66, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, + 0x25, 0x0a, 0x0e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, + 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x50, 0x61, + 0x74, 0x74, 0x65, 0x72, 0x6e, 0x73, 0x22, 0xe9, 0x02, 0x0a, 0x0b, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, + 0x16, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x64, 0x69, + 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x12, + 0x14, 0x0a, 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x12, 0x25, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, + 0x01, 0x28, 0x0e, 0x32, 0x11, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, + 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, + 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x69, 0x61, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x69, 0x61, 0x74, 0x12, 0x1c, 0x0a, + 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, + 0x6f, 0x74, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, + 0x31, 0x0a, 0x09, 0x62, 0x74, 0x63, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x52, 0x08, 0x62, 0x74, 0x63, 0x50, 0x72, 0x69, - 0x63, 0x65, 0x22, 0xa9, 0x02, 0x0a, 0x10, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x54, 0x69, 0x6d, - 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x66, 0x69, 0x61, - 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, - 0x46, 0x69, 0x61, 0x74, 0x12, 0x35, 0x0a, 0x0b, 0x67, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, - 0x69, 0x74, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x66, 0x72, 0x64, 0x72, - 0x70, 0x63, 0x2e, 0x47, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x52, 0x0b, - 0x67, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x11, 0x63, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, - 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, - 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x52, 0x10, - 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, - 0x12, 0x36, 0x0a, 0x0c, 0x66, 0x69, 0x61, 0x74, 0x5f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, - 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x52, 0x0b, 0x66, 0x69, 0x61, - 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x22, 0x83, - 0x01, 0x0a, 0x0e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, - 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, - 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, - 0x12, 0x1b, 0x0a, 0x09, 0x6f, 0x66, 0x66, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x08, 0x6f, 0x66, 0x66, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x25, 0x0a, - 0x0e, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x5f, 0x70, 0x61, 0x74, 0x74, 0x65, 0x72, 0x6e, 0x73, 0x18, - 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x50, 0x61, 0x74, 0x74, - 0x65, 0x72, 0x6e, 0x73, 0x22, 0xe9, 0x02, 0x0a, 0x0b, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x6e, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6f, 0x6e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x0a, - 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x61, - 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x63, 0x72, 0x65, 0x64, 0x69, 0x74, 0x12, 0x14, 0x0a, - 0x05, 0x61, 0x73, 0x73, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x12, 0x25, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x11, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x61, 0x74, 0x65, 0x67, - 0x6f, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x78, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x74, 0x78, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x69, 0x61, 0x74, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x69, 0x61, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x72, - 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x6f, 0x74, - 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x6f, 0x74, 0x65, 0x12, 0x31, 0x0a, - 0x09, 0x62, 0x74, 0x63, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x14, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, - 0x6e, 0x50, 0x72, 0x69, 0x63, 0x65, 0x52, 0x08, 0x62, 0x74, 0x63, 0x50, 0x72, 0x69, 0x63, 0x65, - 0x22, 0x42, 0x0a, 0x11, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, - 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x72, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x73, 0x22, 0x39, 0x0a, 0x12, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, - 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, - 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x22, - 0xdd, 0x01, 0x0a, 0x13, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, 0x6e, 0x6e, - 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2b, 0x0a, 0x11, - 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, - 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, - 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, - 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, - 0x6c, 0x6f, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, 0x6f, 0x73, - 0x65, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x6c, - 0x6f, 0x73, 0x65, 0x54, 0x78, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x70, 0x65, 0x6e, 0x5f, - 0x66, 0x65, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x65, 0x6e, 0x46, - 0x65, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x66, 0x65, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x46, 0x65, 0x65, 0x2a, - 0xa1, 0x01, 0x0a, 0x0b, 0x47, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x12, - 0x17, 0x0a, 0x13, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x47, 0x52, 0x41, 0x4e, 0x55, - 0x4c, 0x41, 0x52, 0x49, 0x54, 0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x49, 0x4e, 0x55, - 0x54, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x49, 0x56, 0x45, 0x5f, 0x4d, 0x49, 0x4e, - 0x55, 0x54, 0x45, 0x53, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x49, 0x46, 0x54, 0x45, 0x45, - 0x4e, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, 0x0e, 0x54, - 0x48, 0x49, 0x52, 0x54, 0x59, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x04, 0x12, - 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x55, 0x52, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x49, 0x58, - 0x5f, 0x48, 0x4f, 0x55, 0x52, 0x53, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x57, 0x45, 0x4c, - 0x56, 0x45, 0x5f, 0x48, 0x4f, 0x55, 0x52, 0x53, 0x10, 0x07, 0x12, 0x07, 0x0a, 0x03, 0x44, 0x41, - 0x59, 0x10, 0x08, 0x2a, 0x41, 0x0a, 0x0b, 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, 0x6b, 0x65, - 0x6e, 0x64, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x46, 0x49, - 0x41, 0x54, 0x42, 0x41, 0x43, 0x4b, 0x45, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x43, - 0x4f, 0x49, 0x4e, 0x43, 0x41, 0x50, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, 0x49, 0x4e, - 0x44, 0x45, 0x53, 0x4b, 0x10, 0x02, 0x2a, 0xa2, 0x02, 0x0a, 0x09, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, - 0x00, 0x12, 0x16, 0x0a, 0x12, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, - 0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x4d, - 0x4f, 0x54, 0x45, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, - 0x10, 0x02, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4f, 0x50, - 0x45, 0x4e, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x41, 0x4e, - 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x52, - 0x45, 0x43, 0x45, 0x49, 0x50, 0x54, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x41, 0x59, 0x4d, - 0x45, 0x4e, 0x54, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x46, 0x45, 0x45, 0x10, 0x07, 0x12, 0x14, - 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x52, 0x45, 0x43, 0x45, 0x49, - 0x50, 0x54, 0x10, 0x08, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10, - 0x09, 0x12, 0x0f, 0x0a, 0x0b, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x5f, 0x46, 0x45, 0x45, - 0x10, 0x0a, 0x12, 0x14, 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x50, - 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x0b, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x49, 0x52, 0x43, - 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0c, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x57, - 0x45, 0x45, 0x50, 0x10, 0x0d, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x46, - 0x45, 0x45, 0x10, 0x0e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, - 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0f, 0x32, 0xd8, 0x04, 0x0a, 0x0d, - 0x46, 0x61, 0x72, 0x61, 0x64, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x65, 0x0a, - 0x16, 0x4f, 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, - 0x2e, 0x4f, 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, - 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x63, - 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x69, 0x0a, 0x18, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, + 0x63, 0x65, 0x22, 0x42, 0x0a, 0x11, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2d, 0x0a, 0x07, 0x72, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x72, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x22, 0x39, 0x0a, 0x12, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x0d, + 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, + 0x74, 0x22, 0xdd, 0x01, 0x0a, 0x13, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x63, 0x68, 0x61, + 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x2b, + 0x0a, 0x11, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x63, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x1d, 0x0a, 0x0a, 0x63, + 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x6c, + 0x6f, 0x73, 0x65, 0x5f, 0x74, 0x78, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x54, 0x78, 0x69, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x70, 0x65, + 0x6e, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6f, 0x70, 0x65, + 0x6e, 0x46, 0x65, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x5f, 0x66, 0x65, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x46, 0x65, + 0x65, 0x2a, 0xa1, 0x01, 0x0a, 0x0b, 0x47, 0x72, 0x61, 0x6e, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, + 0x79, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, 0x47, 0x52, 0x41, + 0x4e, 0x55, 0x4c, 0x41, 0x52, 0x49, 0x54, 0x59, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x49, + 0x4e, 0x55, 0x54, 0x45, 0x10, 0x01, 0x12, 0x10, 0x0a, 0x0c, 0x46, 0x49, 0x56, 0x45, 0x5f, 0x4d, + 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x46, 0x49, 0x46, 0x54, + 0x45, 0x45, 0x4e, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, 0x03, 0x12, 0x12, 0x0a, + 0x0e, 0x54, 0x48, 0x49, 0x52, 0x54, 0x59, 0x5f, 0x4d, 0x49, 0x4e, 0x55, 0x54, 0x45, 0x53, 0x10, + 0x04, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x4f, 0x55, 0x52, 0x10, 0x05, 0x12, 0x0d, 0x0a, 0x09, 0x53, + 0x49, 0x58, 0x5f, 0x48, 0x4f, 0x55, 0x52, 0x53, 0x10, 0x06, 0x12, 0x10, 0x0a, 0x0c, 0x54, 0x57, + 0x45, 0x4c, 0x56, 0x45, 0x5f, 0x48, 0x4f, 0x55, 0x52, 0x53, 0x10, 0x07, 0x12, 0x07, 0x0a, 0x03, + 0x44, 0x41, 0x59, 0x10, 0x08, 0x2a, 0x4d, 0x0a, 0x0b, 0x46, 0x69, 0x61, 0x74, 0x42, 0x61, 0x63, + 0x6b, 0x65, 0x6e, 0x64, 0x12, 0x17, 0x0a, 0x13, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x5f, + 0x46, 0x49, 0x41, 0x54, 0x42, 0x41, 0x43, 0x4b, 0x45, 0x4e, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, + 0x07, 0x43, 0x4f, 0x49, 0x4e, 0x43, 0x41, 0x50, 0x10, 0x01, 0x12, 0x0c, 0x0a, 0x08, 0x43, 0x4f, + 0x49, 0x4e, 0x44, 0x45, 0x53, 0x4b, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x43, 0x55, 0x53, 0x54, + 0x4f, 0x4d, 0x10, 0x03, 0x2a, 0xa2, 0x02, 0x0a, 0x09, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, + 0x16, 0x0a, 0x12, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, + 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x4d, 0x4f, 0x54, + 0x45, 0x5f, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, 0x10, 0x02, + 0x12, 0x14, 0x0a, 0x10, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x4f, 0x50, 0x45, 0x4e, + 0x5f, 0x46, 0x45, 0x45, 0x10, 0x03, 0x12, 0x11, 0x0a, 0x0d, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, + 0x4c, 0x5f, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x04, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x43, + 0x45, 0x49, 0x50, 0x54, 0x10, 0x05, 0x12, 0x0b, 0x0a, 0x07, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, + 0x54, 0x10, 0x06, 0x12, 0x07, 0x0a, 0x03, 0x46, 0x45, 0x45, 0x10, 0x07, 0x12, 0x14, 0x0a, 0x10, + 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x52, 0x45, 0x43, 0x45, 0x49, 0x50, 0x54, + 0x10, 0x08, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10, 0x09, 0x12, + 0x0f, 0x0a, 0x0b, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0a, + 0x12, 0x14, 0x0a, 0x10, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x50, 0x41, 0x59, + 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x0b, 0x12, 0x10, 0x0a, 0x0c, 0x43, 0x49, 0x52, 0x43, 0x55, 0x4c, + 0x41, 0x52, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0c, 0x12, 0x09, 0x0a, 0x05, 0x53, 0x57, 0x45, 0x45, + 0x50, 0x10, 0x0d, 0x12, 0x0d, 0x0a, 0x09, 0x53, 0x57, 0x45, 0x45, 0x50, 0x5f, 0x46, 0x45, 0x45, + 0x10, 0x0e, 0x12, 0x15, 0x0a, 0x11, 0x43, 0x48, 0x41, 0x4e, 0x4e, 0x45, 0x4c, 0x5f, 0x43, 0x4c, + 0x4f, 0x53, 0x45, 0x5f, 0x46, 0x45, 0x45, 0x10, 0x0f, 0x32, 0xd8, 0x04, 0x0a, 0x0d, 0x46, 0x61, + 0x72, 0x61, 0x64, 0x61, 0x79, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x12, 0x65, 0x0a, 0x16, 0x4f, + 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4f, + 0x75, 0x74, 0x6c, 0x69, 0x65, 0x72, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x66, + 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x6d, + 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x69, 0x0a, 0x18, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x52, + 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x27, + 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, 0x64, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x12, 0x27, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, - 0x6f, 0x6c, 0x64, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x66, 0x72, 0x64, 0x72, - 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4c, 0x0a, 0x0d, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, - 0x12, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, - 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, + 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, + 0x0d, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1c, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, - 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, - 0x0f, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, - 0x12, 0x1e, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x1f, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, - 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x49, 0x0a, 0x0c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, - 0x65, 0x12, 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, - 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, - 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, - 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, - 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12, 0x18, 0x2e, 0x66, 0x72, 0x64, 0x72, - 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, - 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, - 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x2e, - 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, - 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, - 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, - 0x62, 0x73, 0x2f, 0x66, 0x61, 0x72, 0x61, 0x64, 0x61, 0x79, 0x2f, 0x66, 0x72, 0x64, 0x72, 0x70, - 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x66, + 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x76, 0x65, 0x6e, 0x75, 0x65, 0x52, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x52, 0x0a, 0x0f, 0x43, + 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x12, 0x1e, + 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, + 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, + 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, + 0x6e, 0x73, 0x69, 0x67, 0x68, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x49, 0x0a, 0x0c, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, 0x74, 0x65, 0x12, + 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, + 0x65, 0x52, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x66, + 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x0a, 0x09, 0x4e, 0x6f, + 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x12, 0x18, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, + 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x19, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x41, + 0x75, 0x64, 0x69, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0b, + 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x2e, 0x66, 0x72, + 0x64, 0x72, 0x70, 0x63, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, + 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, + 0x2f, 0x66, 0x61, 0x72, 0x61, 0x64, 0x61, 0x79, 0x2f, 0x66, 0x72, 0x64, 0x72, 0x70, 0x63, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2258,34 +2300,36 @@ var file_faraday_proto_depIdxs = []int32{ 15, // 6: frdrpc.ChannelInsightsResponse.channel_insights:type_name -> frdrpc.ChannelInsight 0, // 7: frdrpc.ExchangeRateRequest.granularity:type_name -> frdrpc.Granularity 1, // 8: frdrpc.ExchangeRateRequest.fiat_backend:type_name -> frdrpc.FiatBackend - 19, // 9: frdrpc.ExchangeRateResponse.rates:type_name -> frdrpc.ExchangeRate - 18, // 10: frdrpc.ExchangeRate.btc_price:type_name -> frdrpc.BitcoinPrice - 0, // 11: frdrpc.NodeAuditRequest.granularity:type_name -> frdrpc.Granularity - 21, // 12: frdrpc.NodeAuditRequest.custom_categories:type_name -> frdrpc.CustomCategory - 1, // 13: frdrpc.NodeAuditRequest.fiat_backend:type_name -> frdrpc.FiatBackend - 2, // 14: frdrpc.ReportEntry.type:type_name -> frdrpc.EntryType - 18, // 15: frdrpc.ReportEntry.btc_price:type_name -> frdrpc.BitcoinPrice - 22, // 16: frdrpc.NodeAuditResponse.reports:type_name -> frdrpc.ReportEntry - 12, // 17: frdrpc.RevenueReport.PairReportsEntry.value:type_name -> frdrpc.PairReport - 5, // 18: frdrpc.FaradayServer.OutlierRecommendations:input_type -> frdrpc.OutlierRecommendationsRequest - 6, // 19: frdrpc.FaradayServer.ThresholdRecommendations:input_type -> frdrpc.ThresholdRecommendationsRequest - 9, // 20: frdrpc.FaradayServer.RevenueReport:input_type -> frdrpc.RevenueReportRequest - 13, // 21: frdrpc.FaradayServer.ChannelInsights:input_type -> frdrpc.ChannelInsightsRequest - 16, // 22: frdrpc.FaradayServer.ExchangeRate:input_type -> frdrpc.ExchangeRateRequest - 20, // 23: frdrpc.FaradayServer.NodeAudit:input_type -> frdrpc.NodeAuditRequest - 24, // 24: frdrpc.FaradayServer.CloseReport:input_type -> frdrpc.CloseReportRequest - 7, // 25: frdrpc.FaradayServer.OutlierRecommendations:output_type -> frdrpc.CloseRecommendationsResponse - 7, // 26: frdrpc.FaradayServer.ThresholdRecommendations:output_type -> frdrpc.CloseRecommendationsResponse - 10, // 27: frdrpc.FaradayServer.RevenueReport:output_type -> frdrpc.RevenueReportResponse - 14, // 28: frdrpc.FaradayServer.ChannelInsights:output_type -> frdrpc.ChannelInsightsResponse - 17, // 29: frdrpc.FaradayServer.ExchangeRate:output_type -> frdrpc.ExchangeRateResponse - 23, // 30: frdrpc.FaradayServer.NodeAudit:output_type -> frdrpc.NodeAuditResponse - 25, // 31: frdrpc.FaradayServer.CloseReport:output_type -> frdrpc.CloseReportResponse - 25, // [25:32] is the sub-list for method output_type - 18, // [18:25] is the sub-list for method input_type - 18, // [18:18] is the sub-list for extension type_name - 18, // [18:18] is the sub-list for extension extendee - 0, // [0:18] is the sub-list for field type_name + 18, // 9: frdrpc.ExchangeRateRequest.custom_prices:type_name -> frdrpc.BitcoinPrice + 19, // 10: frdrpc.ExchangeRateResponse.rates:type_name -> frdrpc.ExchangeRate + 18, // 11: frdrpc.ExchangeRate.btc_price:type_name -> frdrpc.BitcoinPrice + 0, // 12: frdrpc.NodeAuditRequest.granularity:type_name -> frdrpc.Granularity + 21, // 13: frdrpc.NodeAuditRequest.custom_categories:type_name -> frdrpc.CustomCategory + 1, // 14: frdrpc.NodeAuditRequest.fiat_backend:type_name -> frdrpc.FiatBackend + 18, // 15: frdrpc.NodeAuditRequest.custom_prices:type_name -> frdrpc.BitcoinPrice + 2, // 16: frdrpc.ReportEntry.type:type_name -> frdrpc.EntryType + 18, // 17: frdrpc.ReportEntry.btc_price:type_name -> frdrpc.BitcoinPrice + 22, // 18: frdrpc.NodeAuditResponse.reports:type_name -> frdrpc.ReportEntry + 12, // 19: frdrpc.RevenueReport.PairReportsEntry.value:type_name -> frdrpc.PairReport + 5, // 20: frdrpc.FaradayServer.OutlierRecommendations:input_type -> frdrpc.OutlierRecommendationsRequest + 6, // 21: frdrpc.FaradayServer.ThresholdRecommendations:input_type -> frdrpc.ThresholdRecommendationsRequest + 9, // 22: frdrpc.FaradayServer.RevenueReport:input_type -> frdrpc.RevenueReportRequest + 13, // 23: frdrpc.FaradayServer.ChannelInsights:input_type -> frdrpc.ChannelInsightsRequest + 16, // 24: frdrpc.FaradayServer.ExchangeRate:input_type -> frdrpc.ExchangeRateRequest + 20, // 25: frdrpc.FaradayServer.NodeAudit:input_type -> frdrpc.NodeAuditRequest + 24, // 26: frdrpc.FaradayServer.CloseReport:input_type -> frdrpc.CloseReportRequest + 7, // 27: frdrpc.FaradayServer.OutlierRecommendations:output_type -> frdrpc.CloseRecommendationsResponse + 7, // 28: frdrpc.FaradayServer.ThresholdRecommendations:output_type -> frdrpc.CloseRecommendationsResponse + 10, // 29: frdrpc.FaradayServer.RevenueReport:output_type -> frdrpc.RevenueReportResponse + 14, // 30: frdrpc.FaradayServer.ChannelInsights:output_type -> frdrpc.ChannelInsightsResponse + 17, // 31: frdrpc.FaradayServer.ExchangeRate:output_type -> frdrpc.ExchangeRateResponse + 23, // 32: frdrpc.FaradayServer.NodeAudit:output_type -> frdrpc.NodeAuditResponse + 25, // 33: frdrpc.FaradayServer.CloseReport:output_type -> frdrpc.CloseReportResponse + 27, // [27:34] is the sub-list for method output_type + 20, // [20:27] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_faraday_proto_init() } diff --git a/frdrpc/faraday.proto b/frdrpc/faraday.proto index bec204d..378a90d 100644 --- a/frdrpc/faraday.proto +++ b/frdrpc/faraday.proto @@ -335,6 +335,9 @@ enum FiatBackend { // This API is reached through the following URL: // https://api.coindesk.com/v1/bpi/historical/close.json COINDESK = 2; + + // Use custom price data provided in a CSV file for fiat price information. + CUSTOM = 3; } message ExchangeRateRequest { @@ -350,6 +353,9 @@ message ExchangeRateRequest { // The api to be used for fiat related queries. FiatBackend fiat_backend = 5; + + // Custom price points to use if the CUSTOM FiatBackend option is set. + repeated BitcoinPrice custom_prices = 8; } message ExchangeRateResponse { @@ -365,6 +371,9 @@ message BitcoinPrice { // The timestamp for this price price provided. uint64 price_timestamp = 2; + + // The currency that the price is denoted in. + string currency = 3; } message ExchangeRate { @@ -407,6 +416,9 @@ message NodeAuditRequest { // The api to be used for fiat related queries. FiatBackend fiat_backend = 7; + + // Custom price points to use if the CUSTOM FiatBackend option is set. + repeated BitcoinPrice custom_prices = 8; } message CustomCategory { @@ -522,7 +534,8 @@ message ReportEntry { // The transaction id of the entry. string txid = 7; - // The fiat amount of the entry's amount in USD. + // The fiat amount of the entry's amount in the currency specified in the + // btc_price field. string fiat = 8; // A unique identifier for the entry, if available. @@ -531,7 +544,7 @@ message ReportEntry { // An additional note for the entry, providing additional context. string note = 10; - // The bitcoin price and timestamp used to calcualte our fiat value. + // The bitcoin price and timestamp used to calculate our fiat value. BitcoinPrice btc_price = 11; } diff --git a/frdrpc/faraday.swagger.json b/frdrpc/faraday.swagger.json index d2fbd1c..41d93a2 100644 --- a/frdrpc/faraday.swagger.json +++ b/frdrpc/faraday.swagger.json @@ -102,14 +102,15 @@ }, { "name": "fiat_backend", - "description": "The api to be used for fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json", + "description": "The api to be used for fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json\n - CUSTOM: Use custom price data provided in a CSV file for fiat price information.", "in": "query", "required": false, "type": "string", "enum": [ "UNKNOWN_FIATBACKEND", "COINCAP", - "COINDESK" + "COINDESK", + "CUSTOM" ], "default": "UNKNOWN_FIATBACKEND" } @@ -207,14 +208,15 @@ }, { "name": "fiat_backend", - "description": "The api to be used for fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json", + "description": "The api to be used for fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json\n - CUSTOM: Use custom price data provided in a CSV file for fiat price information.", "in": "query", "required": false, "type": "string", "enum": [ "UNKNOWN_FIATBACKEND", "COINCAP", - "COINDESK" + "COINDESK", + "CUSTOM" ], "default": "UNKNOWN_FIATBACKEND" } @@ -416,6 +418,10 @@ "type": "string", "format": "uint64", "description": "The timestamp for this price price provided." + }, + "currency": { + "type": "string", + "description": "The currency that the price is denoted in." } } }, @@ -617,10 +623,11 @@ "enum": [ "UNKNOWN_FIATBACKEND", "COINCAP", - "COINDESK" + "COINDESK", + "CUSTOM" ], "default": "UNKNOWN_FIATBACKEND", - "description": "FiatBackend is the API endpoint to be used for any fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json" + "description": "FiatBackend is the API endpoint to be used for any fiat related queries.\n\n - COINCAP: Use the CoinCap API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coincap.io/v2/assets/bitcoin/history\n - COINDESK: Use the CoinDesk API for fiat price information.\nThis API is reached through the following URL:\nhttps://api.coindesk.com/v1/bpi/historical/close.json\n - CUSTOM: Use custom price data provided in a CSV file for fiat price information." }, "frdrpcGranularity": { "type": "string", @@ -732,7 +739,7 @@ }, "fiat": { "type": "string", - "description": "The fiat amount of the entry's amount in USD." + "description": "The fiat amount of the entry's amount in the currency specified in the\nbtc_price field." }, "reference": { "type": "string", @@ -744,7 +751,7 @@ }, "btc_price": { "$ref": "#/definitions/frdrpcBitcoinPrice", - "description": "The bitcoin price and timestamp used to calcualte our fiat value." + "description": "The bitcoin price and timestamp used to calculate our fiat value." } } }, diff --git a/frdrpc/node_audit.go b/frdrpc/node_audit.go index bae9ea6..62cd1c4 100644 --- a/frdrpc/node_audit.go +++ b/frdrpc/node_audit.go @@ -5,11 +5,14 @@ import ( "errors" "fmt" "sort" + "time" "github.com/lightningnetwork/lnd/routing/route" + "github.com/shopspring/decimal" "github.com/lightninglabs/faraday/accounting" "github.com/lightninglabs/faraday/fees" + "github.com/lightninglabs/faraday/fiat" ) var ( @@ -52,6 +55,32 @@ func parseNodeAuditRequest(ctx context.Context, cfg *Config, return nil, nil, err } + if len(req.CustomPrices) > 0 && req.FiatBackend != FiatBackend_CUSTOM { + return nil, nil, errors.New( + "custom price points provided but custom fiat " + + "backend not set", + ) + } + + pricePoints, err := pricePointsFromRPC(req.CustomPrices) + if err != nil { + return nil, nil, err + } + + if req.FiatBackend == FiatBackend_CUSTOM { + if err := validateCustomPricePoints( + pricePoints, time.Unix(int64(req.StartTime), 0), + ); err != nil { + return nil, nil, err + } + } + + priceSourceCfg := &fiat.PriceSourceConfig{ + Backend: fiatBackend, + Granularity: granularity, + PricePoints: pricePoints, + } + pubkey, err := route.NewVertexFromBytes(info.IdentityPubkey[:]) if err != nil { return nil, nil, err @@ -71,7 +100,7 @@ func parseNodeAuditRequest(ctx context.Context, cfg *Config, offChain := accounting.NewOffChainConfig( ctx, cfg.Lnd, uint64(maxInvoiceQueries), uint64(maxPaymentQueries), uint64(maxForwardQueries), - pubkey, start, end, req.DisableFiat, fiatBackend, granularity, + pubkey, start, end, req.DisableFiat, priceSourceCfg, offChainCategories, ) @@ -87,7 +116,7 @@ func parseNodeAuditRequest(ctx context.Context, cfg *Config, onChain := accounting.NewOnChainConfig( ctx, cfg.Lnd, start, end, req.DisableFiat, - feeLookup, fiatBackend, granularity, onChainCategories, + feeLookup, priceSourceCfg, onChainCategories, ) return onChain, offChain, nil @@ -122,6 +151,40 @@ func validateCustomCategories(categories []*CustomCategory) error { return nil } +func pricePointsFromRPC(prices []*BitcoinPrice) ([]*fiat.Price, error) { + res := make([]*fiat.Price, len(prices)) + + for i, p := range prices { + price, err := decimal.NewFromString(p.Price) + if err != nil { + return nil, err + } + + res[i] = &fiat.Price{ + Timestamp: time.Unix(int64(p.PriceTimestamp), 0), + Price: price, + Currency: p.Currency, + } + } + + return res, nil +} + +// validateCustomPricePoints checks that there is at lease one price point +// in the set before the given start time. +func validateCustomPricePoints(prices []*fiat.Price, + startTime time.Time) error { + + for _, price := range prices { + if price.Timestamp.Before(startTime) { + return nil + } + } + + return errors.New("expected at least one price point with a " + + "timestamp preceding the given start time") +} + func getCategories(categories []*CustomCategory) ([]accounting.CustomCategory, []accounting.CustomCategory, error) { @@ -165,7 +228,8 @@ func rpcReportResponse(report accounting.Report) (*NodeAuditResponse, Reference: entry.Reference, Note: entry.Note, BtcPrice: &BitcoinPrice{ - Price: entry.BTCPrice.Price.String(), + Price: entry.BTCPrice.Price.String(), + Currency: entry.BTCPrice.Currency, }, } diff --git a/frdrpc/rpcserver.go b/frdrpc/rpcserver.go index 9a0eca9..9746a3b 100644 --- a/frdrpc/rpcserver.go +++ b/frdrpc/rpcserver.go @@ -443,16 +443,12 @@ func (s *RPCServer) ExchangeRate(ctx context.Context, log.Debugf("[FiatEstimate]: %v requests", len(req.Timestamps)) - timestamps, fiatBackend, granularity, err := parseExchangeRateRequest( - req, - ) + timestamps, priceCfg, err := parseExchangeRateRequest(req) if err != nil { return nil, err } - prices, err := fiat.GetPrices( - ctx, timestamps, fiatBackend, *granularity, - ) + prices, err := fiat.GetPrices(ctx, timestamps, priceCfg) if err != nil { return nil, err }