Skip to content

Commit

Permalink
Accept either decimal/quote types for pricing streams (#15117)
Browse files Browse the repository at this point in the history
* Accept either decimal/quote types for pricing streams

* Fix litner

* Fix even more lint problems
  • Loading branch information
samsondav authored Nov 8, 2024
1 parent f4565c5 commit 95bf36b
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 24 deletions.
64 changes: 44 additions & 20 deletions core/services/llo/evm/report_codec_premium_legacy.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ func (r ReportCodecPremiumLegacy) Encode(ctx context.Context, report llo.Report,
rf := v3.ReportFields{
ValidFromTimestamp: report.ValidAfterSeconds + 1,
Timestamp: report.ObservationTimestampSeconds,
NativeFee: CalculateFee(nativePrice.Decimal(), opts.BaseUSDFee),
LinkFee: CalculateFee(linkPrice.Decimal(), opts.BaseUSDFee),
NativeFee: CalculateFee(nativePrice, opts.BaseUSDFee),
LinkFee: CalculateFee(linkPrice, opts.BaseUSDFee),
ExpiresAt: report.ObservationTimestampSeconds + opts.ExpirationWindow,
BenchmarkPrice: quote.Benchmark.Mul(multiplier).BigInt(),
Bid: quote.Bid.Mul(multiplier).BigInt(),
Expand Down Expand Up @@ -124,34 +124,58 @@ func (r ReportCodecPremiumLegacy) Pack(digest types.ConfigDigest, seqNr uint64,
return payload, nil
}

// TODO: Test this
// MERC-3524
func ExtractReportValues(report llo.Report) (nativePrice, linkPrice *llo.Decimal, quote *llo.Quote, err error) {
// ExtractReportValues extracts the native price, link price and quote from the report
// Can handle either *Decimal or *Quote types for native/link prices
func ExtractReportValues(report llo.Report) (nativePrice, linkPrice decimal.Decimal, quote *llo.Quote, err error) {
if len(report.Values) != 3 {
return nil, nil, nil, fmt.Errorf("ReportCodecPremiumLegacy requires exactly 3 values (NativePrice, LinkPrice, Quote{Bid, Mid, Ask}); got report.Values: %#v", report.Values)
err = fmt.Errorf("ReportCodecPremiumLegacy requires exactly 3 values (NativePrice, LinkPrice, Quote{Bid, Mid, Ask}); got report.Values: %v", report.Values)
return
}
var is bool
nativePrice, is = report.Values[0].(*llo.Decimal)
if nativePrice == nil {
// Missing price median will cause a zero fee
nativePrice = llo.ToDecimal(decimal.Zero)
} else if !is {
return nil, nil, nil, fmt.Errorf("ReportCodecPremiumLegacy expects first value to be of type *Decimal; got: %T", report.Values[0])
nativePrice, err = extractPrice(report.Values[0])
if err != nil {
err = fmt.Errorf("ReportCodecPremiumLegacy failed to extract native price: %w", err)
return
}
linkPrice, is = report.Values[1].(*llo.Decimal)
if linkPrice == nil {
// Missing price median will cause a zero fee
linkPrice = llo.ToDecimal(decimal.Zero)
} else if !is {
return nil, nil, nil, fmt.Errorf("ReportCodecPremiumLegacy expects second value to be of type *Decimal; got: %T", report.Values[1])
linkPrice, err = extractPrice(report.Values[1])
if err != nil {
err = fmt.Errorf("ReportCodecPremiumLegacy failed to extract link price: %w", err)
return
}
var is bool
quote, is = report.Values[2].(*llo.Quote)
if !is {
return nil, nil, nil, fmt.Errorf("ReportCodecPremiumLegacy expects third value to be of type *Quote; got: %T", report.Values[2])
err = fmt.Errorf("ReportCodecPremiumLegacy expects third stream value to be of type *Quote; got: %T", report.Values[2])
return
}
if quote == nil {
err = errors.New("ReportCodecPremiumLegacy expects third stream value to be non-nil")
return
}
return nativePrice, linkPrice, quote, nil
}

func extractPrice(price llo.StreamValue) (decimal.Decimal, error) {
switch p := price.(type) {
case *llo.Decimal:
if p == nil {
// Missing price will cause a zero fee
return decimal.Zero, nil
}
return p.Decimal(), nil
case *llo.Quote:
// in case of quote feed, use the benchmark price
if p == nil {
return decimal.Zero, nil
}
return p.Benchmark, nil

case nil:
return decimal.Zero, nil
default:
return decimal.Zero, fmt.Errorf("expected *Decimal or *Quote; got: %T", price)
}
}

// TODO: Consider embedding the DON ID here?
// MERC-3524
var LLOExtraHash = common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000001")
Expand Down
99 changes: 95 additions & 4 deletions core/services/llo/evm/report_codec_premium_legacy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func Test_ReportCodecPremiumLegacy(t *testing.T) {
_, err := rc.Encode(ctx, llo.Report{}, cd)
require.Error(t, err)

assert.Contains(t, err.Error(), "ReportCodecPremiumLegacy cannot encode; got unusable report; ReportCodecPremiumLegacy requires exactly 3 values (NativePrice, LinkPrice, Quote{Bid, Mid, Ask}); got report.Values: []llo.StreamValue(nil)")
assert.Contains(t, err.Error(), "ReportCodecPremiumLegacy cannot encode; got unusable report; ReportCodecPremiumLegacy requires exactly 3 values (NativePrice, LinkPrice, Quote{Bid, Mid, Ask}); got report.Values: []")
})

t.Run("does not encode specimen reports", func(t *testing.T) {
Expand All @@ -52,7 +52,7 @@ func Test_ReportCodecPremiumLegacy(t *testing.T) {

_, err := rc.Encode(ctx, report, cd)
require.Error(t, err)
assert.EqualError(t, err, "ReportCodecPremiumLegacy does not support encoding specimen reports")
require.EqualError(t, err, "ReportCodecPremiumLegacy does not support encoding specimen reports")
})

t.Run("Encode constructs a report from observations", func(t *testing.T) {
Expand Down Expand Up @@ -123,13 +123,104 @@ func Test_ReportCodecPremiumLegacy(t *testing.T) {

t.Run("Decode errors on invalid report", func(t *testing.T) {
_, err := rc.Decode([]byte{1, 2, 3})
assert.EqualError(t, err, "failed to decode report: abi: cannot marshal in to go type: length insufficient 3 require 32")
require.EqualError(t, err, "failed to decode report: abi: cannot marshal in to go type: length insufficient 3 require 32")

longBad := make([]byte, 64)
for i := 0; i < len(longBad); i++ {
longBad[i] = byte(i)
}
_, err = rc.Decode(longBad)
assert.EqualError(t, err, "failed to decode report: abi: improperly encoded uint32 value")
require.EqualError(t, err, "failed to decode report: abi: improperly encoded uint32 value")
})
}

type UnhandledStreamValue struct{}

var _ llo.StreamValue = &UnhandledStreamValue{}

func (sv *UnhandledStreamValue) MarshalBinary() (data []byte, err error) { return }
func (sv *UnhandledStreamValue) UnmarshalBinary(data []byte) error { return nil }
func (sv *UnhandledStreamValue) MarshalText() (text []byte, err error) { return }
func (sv *UnhandledStreamValue) UnmarshalText(text []byte) error { return nil }
func (sv *UnhandledStreamValue) Type() llo.LLOStreamValue_Type { return 0 }

func Test_ExtractReportValues(t *testing.T) {
t.Run("with wrong number of stream values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{llo.ToDecimal(decimal.NewFromInt(35)), llo.ToDecimal(decimal.NewFromInt(36))}}
_, _, _, err := ExtractReportValues(report)
require.EqualError(t, err, "ReportCodecPremiumLegacy requires exactly 3 values (NativePrice, LinkPrice, Quote{Bid, Mid, Ask}); got report.Values: [35 36]")
})
t.Run("with (nil, nil, nil) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{nil, nil, nil}}
_, _, _, err := ExtractReportValues(report)

require.EqualError(t, err, "ReportCodecPremiumLegacy expects third stream value to be of type *Quote; got: <nil>")
})
t.Run("with ((*llo.Quote)(nil), nil, (*llo.Quote)(nil)) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{(*llo.Quote)(nil), nil, (*llo.Quote)(nil)}}
nativePrice, linkPrice, quote, err := ExtractReportValues(report)

require.EqualError(t, err, "ReportCodecPremiumLegacy expects third stream value to be non-nil")
assert.Equal(t, decimal.Zero, nativePrice)
assert.Equal(t, decimal.Zero, linkPrice)
assert.Nil(t, quote)
})
t.Run("with (*llo.Decimal, *llo.Decimal, *llo.Decimal) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{llo.ToDecimal(decimal.NewFromInt(35)), llo.ToDecimal(decimal.NewFromInt(36)), llo.ToDecimal(decimal.NewFromInt(37))}}
_, _, _, err := ExtractReportValues(report)

require.EqualError(t, err, "ReportCodecPremiumLegacy expects third stream value to be of type *Quote; got: *llo.Decimal")
})
t.Run("with ((*llo.Quote)(nil), nil, *llo.Quote) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{(*llo.Quote)(nil), nil, &llo.Quote{Bid: decimal.NewFromInt(37), Benchmark: decimal.NewFromInt(38), Ask: decimal.NewFromInt(39)}}}
nativePrice, linkPrice, quote, err := ExtractReportValues(report)

require.NoError(t, err)
assert.Equal(t, decimal.Zero, nativePrice)
assert.Equal(t, decimal.Zero, linkPrice)
assert.Equal(t, &llo.Quote{Bid: decimal.NewFromInt(37), Benchmark: decimal.NewFromInt(38), Ask: decimal.NewFromInt(39)}, quote)
})
t.Run("with unrecognized types", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{&UnhandledStreamValue{}, &UnhandledStreamValue{}, &UnhandledStreamValue{}}}
_, _, _, err := ExtractReportValues(report)

require.EqualError(t, err, "ReportCodecPremiumLegacy failed to extract native price: expected *Decimal or *Quote; got: *evm.UnhandledStreamValue")

report = llo.Report{Values: []llo.StreamValue{llo.ToDecimal(decimal.NewFromInt(35)), &UnhandledStreamValue{}, &UnhandledStreamValue{}}}
_, _, _, err = ExtractReportValues(report)

require.EqualError(t, err, "ReportCodecPremiumLegacy failed to extract link price: expected *Decimal or *Quote; got: *evm.UnhandledStreamValue")

report = llo.Report{Values: []llo.StreamValue{llo.ToDecimal(decimal.NewFromInt(35)), llo.ToDecimal(decimal.NewFromInt(36)), &UnhandledStreamValue{}}}
_, _, _, err = ExtractReportValues(report)

require.EqualError(t, err, "ReportCodecPremiumLegacy expects third stream value to be of type *Quote; got: *evm.UnhandledStreamValue")
})
t.Run("with (*llo.Decimal, *llo.Decimal, *llo.Quote) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{llo.ToDecimal(decimal.NewFromInt(35)), llo.ToDecimal(decimal.NewFromInt(36)), &llo.Quote{Bid: decimal.NewFromInt(37), Benchmark: decimal.NewFromInt(38), Ask: decimal.NewFromInt(39)}}}
nativePrice, linkPrice, quote, err := ExtractReportValues(report)

require.NoError(t, err)
assert.Equal(t, decimal.NewFromInt(35), nativePrice)
assert.Equal(t, decimal.NewFromInt(36), linkPrice)
assert.Equal(t, &llo.Quote{Bid: decimal.NewFromInt(37), Benchmark: decimal.NewFromInt(38), Ask: decimal.NewFromInt(39)}, quote)
})
t.Run("with (*llo.Quote, *llo.Quote, *llo.Quote) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{&llo.Quote{Bid: decimal.NewFromInt(35), Benchmark: decimal.NewFromInt(36), Ask: decimal.NewFromInt(37)}, &llo.Quote{Bid: decimal.NewFromInt(38), Benchmark: decimal.NewFromInt(39), Ask: decimal.NewFromInt(40)}, &llo.Quote{Bid: decimal.NewFromInt(41), Benchmark: decimal.NewFromInt(42), Ask: decimal.NewFromInt(43)}}}
nativePrice, linkPrice, quote, err := ExtractReportValues(report)

require.NoError(t, err)
assert.Equal(t, decimal.NewFromInt(36), nativePrice)
assert.Equal(t, decimal.NewFromInt(39), linkPrice)
assert.Equal(t, &llo.Quote{Bid: decimal.NewFromInt(41), Benchmark: decimal.NewFromInt(42), Ask: decimal.NewFromInt(43)}, quote)
})
t.Run("with (nil, nil, *llo.Quote) values", func(t *testing.T) {
report := llo.Report{Values: []llo.StreamValue{nil, nil, &llo.Quote{Bid: decimal.NewFromInt(37), Benchmark: decimal.NewFromInt(38), Ask: decimal.NewFromInt(39)}}}
nativePrice, linkPrice, quote, err := ExtractReportValues(report)

require.NoError(t, err)
assert.Equal(t, decimal.Zero, nativePrice)
assert.Equal(t, decimal.Zero, linkPrice)
assert.Equal(t, &llo.Quote{Bid: decimal.NewFromInt(37), Benchmark: decimal.NewFromInt(38), Ask: decimal.NewFromInt(39)}, quote)
})
}

0 comments on commit 95bf36b

Please sign in to comment.