Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 21 additions & 15 deletions go/report/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,20 +517,23 @@ var v13Data = v13.Data{
}

var v14Data = v14.Data{
FeedID: [32]uint8{00, 14, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58, 163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114},
ValidFromTimestamp: time.Unix(1700000000, 0),
ObservationsTimestamp: time.Unix(1700000000, 0),
NativeFee: big.NewInt(10),
LinkFee: big.NewInt(10),
ExpiresAt: time.Unix(1700000100, 0),
MidPrice: big.NewInt(100),
BidPrice: big.NewInt(99),
AskPrice: big.NewInt(101),
ExpiryTime: time.Unix(0, 1700000010000000000),
FirstDayOfNotice: time.Unix(0, 1700000005000000000),
LastSeenTimestampNs: time.Unix(0, 1700000000000000000),
MarketStatus: common.MarketStatusOpen,
ContractMonth: "F",
FeedID: [32]uint8{00, 14, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58, 163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114},
ValidFromTimestamp: time.Unix(1700000000, 0),
ObservationsTimestamp: time.Unix(1700000000, 0),
NativeFee: big.NewInt(10),
LinkFee: big.NewInt(10),
ExpiresAt: time.Unix(1700000100, 0),
MidPrice: big.NewInt(100),
BidPrice: big.NewInt(99),
AskPrice: big.NewInt(101),
ExpiryTime: "2026-09-22",
FirstDayOfNotice: time.Unix(0, 1700000005000000000),
LastSeenTimestampNs: time.Unix(0, 1700000000000000000),
MarketStatus: common.MarketStatusOpen,
ContractMonth: 1,
GoldmanRollPrice: big.NewInt(102),
CurrentBusinessDay: 3,
InterpolatedGoldmanRollPrice: big.NewInt(103),
}

func mustPackData(d interface{}) []byte {
Expand Down Expand Up @@ -729,11 +732,14 @@ func mustPackData(d interface{}) []byte {
v.MidPrice,
v.BidPrice,
v.AskPrice,
uint64(v.ExpiryTime.UnixNano()),
v.ExpiryTime,
uint64(v.FirstDayOfNotice.UnixNano()),
uint64(v.LastSeenTimestampNs.UnixNano()),
v.MarketStatus,
v.ContractMonth,
v.GoldmanRollPrice,
v.CurrentBusinessDay,
v.InterpolatedGoldmanRollPrice,
}
default:
panic(fmt.Sprintf("invalid type to pack: %#v", v))
Expand Down
93 changes: 55 additions & 38 deletions go/report/v14/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import (

var schema = Schema()

// expiryTimeLayout is the date layout of the expiryTime field, e.g. "2026-09-22".
const expiryTimeLayout = "2006-01-02"

// Schema returns this data version schema
func Schema() abi.Arguments {
mustNewType := func(t string) abi.Type {
Expand All @@ -32,11 +35,14 @@ func Schema() abi.Arguments {
{Name: "midPrice", Type: mustNewType("int192")},
{Name: "bidPrice", Type: mustNewType("int192")},
{Name: "askPrice", Type: mustNewType("int192")},
{Name: "expiryTime", Type: mustNewType("uint64")},
{Name: "expiryTime", Type: mustNewType("string")},
{Name: "firstDayOfNotice", Type: mustNewType("uint64")},
{Name: "lastSeenTimestampNs", Type: mustNewType("uint64")},
{Name: "marketStatus", Type: mustNewType("uint32")},
{Name: "contractMonth", Type: mustNewType("string")},
{Name: "contractMonth", Type: mustNewType("uint32")},
{Name: "goldmanRollPrice", Type: mustNewType("int192")},
{Name: "currentBusinessDay", Type: mustNewType("uint32")},
{Name: "interpolatedGoldmanRollPrice", Type: mustNewType("int192")},
})
}

Expand All @@ -49,14 +55,17 @@ type Data struct {
LinkFee *big.Int
ExpiresAt time.Time

MidPrice *big.Int
BidPrice *big.Int
AskPrice *big.Int
ExpiryTime time.Time // nanoseconds precision
FirstDayOfNotice time.Time // roll_date converted to UNIX timestamp, nanoseconds precision
LastSeenTimestampNs time.Time // Should reflect the timestamp of the last update from the DP, nanoseconds precision
MarketStatus uint32
ContractMonth string // A single capital letter F to Z for Jan to Dec.
MidPrice *big.Int
BidPrice *big.Int
AskPrice *big.Int
ExpiryTime string // Contract expiry date, formatted as YYYY-MM-DD, e.g. "2026-09-22"
FirstDayOfNotice time.Time // roll_date converted to UNIX timestamp, nanoseconds precision
LastSeenTimestampNs time.Time // Should reflect the timestamp of the last update from the DP, nanoseconds precision
MarketStatus uint32
ContractMonth uint32 // The contract month, from 1 (Jan) to 12 (Dec).
GoldmanRollPrice *big.Int // The Goldman roll price (18 decimal precision)
CurrentBusinessDay uint32 // The current business day, numbered
InterpolatedGoldmanRollPrice *big.Int // The interpolated Goldman roll price (18 decimal precision)
}

// rawData is used internally for ABI decoding - types must match ABI schema
Expand All @@ -68,14 +77,17 @@ type rawData struct {
LinkFee *big.Int
ExpiresAt uint64

MidPrice *big.Int
BidPrice *big.Int
AskPrice *big.Int
ExpiryTime uint64
FirstDayOfNotice uint64
LastSeenTimestampNs uint64
MarketStatus uint32
ContractMonth string
MidPrice *big.Int
BidPrice *big.Int
AskPrice *big.Int
ExpiryTime string
FirstDayOfNotice uint64
LastSeenTimestampNs uint64
MarketStatus uint32
ContractMonth uint32
GoldmanRollPrice *big.Int
CurrentBusinessDay uint32
InterpolatedGoldmanRollPrice *big.Int
}

// Schema returns this data version schema
Expand All @@ -94,16 +106,18 @@ func Decode(data []byte) (*Data, error) {
return nil, fmt.Errorf("failed to copy report values to struct: %w", err)
}

// contractMonth must be a single letter from F to Z (Jan to Dec).
if len(raw.ContractMonth) != 1 || raw.ContractMonth[0] < 'F' || raw.ContractMonth[0] > 'Z' {
return nil, fmt.Errorf("invalid contractMonth %q: must be a single letter from F to Z", raw.ContractMonth)
// contractMonth must be a number from 1 (Jan) to 12 (Dec).
if raw.ContractMonth < 1 || raw.ContractMonth > 12 {
return nil, fmt.Errorf("invalid contractMonth %d: must be a number from 1 to 12", raw.ContractMonth)
}

// expiryTime must be a valid calendar date formatted as YYYY-MM-DD.
if _, err := time.Parse(expiryTimeLayout, raw.ExpiryTime); err != nil {
return nil, fmt.Errorf("invalid expiryTime %q: must be a date formatted as YYYY-MM-DD: %w", raw.ExpiryTime, err)
}

// Validate uint64 nanosecond timestamps do not overflow int64
const maxInt64Ns = int64(^uint64(0) >> 1) // 2^63 - 1
if raw.ExpiryTime > uint64(maxInt64Ns) {
return nil, fmt.Errorf("ExpiryTime overflow: %d exceeds maximum nanosecond timestamp", raw.ExpiryTime)
}
if raw.FirstDayOfNotice > uint64(maxInt64Ns) {
return nil, fmt.Errorf("FirstDayOfNotice overflow: %d exceeds maximum nanosecond timestamp", raw.FirstDayOfNotice)
}
Expand All @@ -114,20 +128,23 @@ func Decode(data []byte) (*Data, error) {
res := raw.FeedID.Resolution()

decoded := &Data{
FeedID: raw.FeedID,
ValidFromTimestamp: feed.ParseTimestamp(raw.ValidFromTimestamp, res),
ObservationsTimestamp: feed.ParseTimestamp(raw.ObservationsTimestamp, res),
NativeFee: raw.NativeFee,
LinkFee: raw.LinkFee,
ExpiresAt: feed.ParseTimestamp(raw.ExpiresAt, res),
MidPrice: raw.MidPrice,
BidPrice: raw.BidPrice,
AskPrice: raw.AskPrice,
ExpiryTime: time.Unix(0, int64(raw.ExpiryTime)),
FirstDayOfNotice: time.Unix(0, int64(raw.FirstDayOfNotice)),
LastSeenTimestampNs: time.Unix(0, int64(raw.LastSeenTimestampNs)),
MarketStatus: raw.MarketStatus,
ContractMonth: raw.ContractMonth,
FeedID: raw.FeedID,
ValidFromTimestamp: feed.ParseTimestamp(raw.ValidFromTimestamp, res),
ObservationsTimestamp: feed.ParseTimestamp(raw.ObservationsTimestamp, res),
NativeFee: raw.NativeFee,
LinkFee: raw.LinkFee,
ExpiresAt: feed.ParseTimestamp(raw.ExpiresAt, res),
MidPrice: raw.MidPrice,
BidPrice: raw.BidPrice,
AskPrice: raw.AskPrice,
ExpiryTime: raw.ExpiryTime,
FirstDayOfNotice: time.Unix(0, int64(raw.FirstDayOfNotice)),
LastSeenTimestampNs: time.Unix(0, int64(raw.LastSeenTimestampNs)),
MarketStatus: raw.MarketStatus,
ContractMonth: raw.ContractMonth,
GoldmanRollPrice: raw.GoldmanRollPrice,
CurrentBusinessDay: raw.CurrentBusinessDay,
InterpolatedGoldmanRollPrice: raw.InterpolatedGoldmanRollPrice,
}

return decoded, nil
Expand Down
70 changes: 61 additions & 9 deletions go/report/v14/data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,14 @@ func TestData(t *testing.T) {
midPrice := big.NewInt(100)
bidPrice := big.NewInt(99)
askPrice := big.NewInt(101)
expiryTime := uint64(time.Now().UnixNano()) + 100
expiryTime := "2026-09-22"
firstDayOfNotice := uint64(time.Now().UnixNano()) + 50
lastSeenTimestampNs := uint64(time.Now().UnixNano()) - 100
marketStatus := uint32(1)
contractMonth := "N"
contractMonth := uint32(7)
goldmanRollPrice := big.NewInt(102)
currentBusinessDay := uint32(3)
interpolatedGoldmanRollPrice := big.NewInt(103)

b, err := schema.Pack(
feedID,
Expand All @@ -38,6 +41,9 @@ func TestData(t *testing.T) {
lastSeenTimestampNs,
marketStatus,
contractMonth,
goldmanRollPrice,
currentBusinessDay,
interpolatedGoldmanRollPrice,
)

if err != nil {
Expand Down Expand Up @@ -77,8 +83,8 @@ func TestData(t *testing.T) {
if d.AskPrice.Cmp(askPrice) != 0 {
t.Errorf("AskPrice mismatch: expected %v, got %v", askPrice, d.AskPrice)
}
if d.ExpiryTime.UnixNano() != int64(expiryTime) {
t.Errorf("ExpiryTime mismatch: expected %d, got %d", expiryTime, d.ExpiryTime.UnixNano())
if d.ExpiryTime != expiryTime {
t.Errorf("ExpiryTime mismatch: expected %s, got %s", expiryTime, d.ExpiryTime)
}
if d.FirstDayOfNotice.UnixNano() != int64(firstDayOfNotice) {
t.Errorf("FirstDayOfNotice mismatch: expected %d, got %d", firstDayOfNotice, d.FirstDayOfNotice.UnixNano())
Expand All @@ -90,15 +96,24 @@ func TestData(t *testing.T) {
t.Errorf("MarketStatus mismatch: expected %d, got %d", marketStatus, d.MarketStatus)
}
if d.ContractMonth != contractMonth {
t.Errorf("ContractMonth mismatch: expected %s, got %s", contractMonth, d.ContractMonth)
t.Errorf("ContractMonth mismatch: expected %d, got %d", contractMonth, d.ContractMonth)
}
if d.GoldmanRollPrice.Cmp(goldmanRollPrice) != 0 {
t.Errorf("GoldmanRollPrice mismatch: expected %v, got %v", goldmanRollPrice, d.GoldmanRollPrice)
}
if d.CurrentBusinessDay != currentBusinessDay {
t.Errorf("CurrentBusinessDay mismatch: expected %d, got %d", currentBusinessDay, d.CurrentBusinessDay)
}
if d.InterpolatedGoldmanRollPrice.Cmp(interpolatedGoldmanRollPrice) != 0 {
t.Errorf("InterpolatedGoldmanRollPrice mismatch: expected %v, got %v", interpolatedGoldmanRollPrice, d.InterpolatedGoldmanRollPrice)
}
}

func TestDecodeInvalidContractMonth(t *testing.T) {
feedID := [32]uint8{00, 14, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58, 163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114}

// Values that are outside the valid single-letter F..Z range must be rejected.
for _, cm := range []string{"", "A", "E", "AB", "n", "1"} {
// Values that are outside the valid 1..12 range must be rejected.
for _, cm := range []uint32{0, 13, 100, ^uint32(0)} {
b, err := schema.Pack(
feedID,
uint64(time.Now().Unix()),
Expand All @@ -109,18 +124,55 @@ func TestDecodeInvalidContractMonth(t *testing.T) {
big.NewInt(100),
big.NewInt(99),
big.NewInt(101),
uint64(time.Now().UnixNano()),
"2026-09-22",
uint64(time.Now().UnixNano()),
uint64(time.Now().UnixNano()),
uint32(1),
cm,
big.NewInt(102),
uint32(3),
big.NewInt(103),
)
if err != nil {
t.Fatalf("failed to serialize report: %s", err)
}

if _, err := Decode(b); err == nil {
t.Errorf("expected error decoding contractMonth %d, got nil", cm)
}
}
}

func TestDecodeInvalidExpiryTime(t *testing.T) {
feedID := [32]uint8{00, 14, 107, 74, 167, 229, 124, 167, 182, 138, 225, 191, 69, 101, 63, 86, 182, 86, 253, 58, 163, 53, 239, 127, 174, 105, 107, 102, 63, 27, 132, 114}

// Values that are not a valid YYYY-MM-DD calendar date must be rejected.
for _, et := range []string{"", "2026-9-22", "22-09-2026", "2026/09/22", "2026-13-01", "2026-09-31", "not-a-date", "2026-09-22T00:00:00Z"} {
b, err := schema.Pack(
feedID,
uint64(time.Now().Unix()),
uint64(time.Now().Unix()),
big.NewInt(10),
big.NewInt(10),
uint64(time.Now().Unix())+100,
big.NewInt(100),
big.NewInt(99),
big.NewInt(101),
et,
uint64(time.Now().UnixNano()),
uint64(time.Now().UnixNano()),
uint32(1),
uint32(7),
big.NewInt(102),
uint32(3),
big.NewInt(103),
)
if err != nil {
t.Fatalf("failed to serialize report: %s", err)
}

if _, err := Decode(b); err == nil {
t.Errorf("expected error decoding contractMonth %q, got nil", cm)
t.Errorf("expected error decoding expiryTime %q, got nil", et)
}
}
}
38 changes: 33 additions & 5 deletions rust/crates/report/src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,12 @@ mod tests {
pub const MOCK_LAST_TRADED_PRICE: isize = 228;
pub const MOCK_MID: isize = 228;
pub const MOCK_MARKET_STATUS: u32 = 2;
pub const MOCK_EXPIRY_TIME: u64 = 1718885872000000000;
pub const MOCK_EXPIRY_TIME: &str = "2026-09-22";
pub const MOCK_FIRST_DAY_OF_NOTICE: u64 = 1718885822000000000;
pub const MOCK_CONTRACT_MONTH: &str = "F";
pub const MOCK_CONTRACT_MONTH: u32 = 1;
pub const MOCK_GOLDMAN_ROLL_PRICE: isize = 230;
pub const MOCK_CURRENT_BUSINESS_DAY: u32 = 3;
pub const MOCK_INTERPOLATED_GOLDMAN_ROLL_PRICE: isize = 231;

pub fn generate_mock_report_data_v1() -> ReportDataV1 {
let report_data = ReportDataV1 {
Expand Down Expand Up @@ -470,11 +473,18 @@ mod tests {
mid_price: BigInt::from(MOCK_MID).checked_mul(&multiplier).unwrap(),
bid_price: BigInt::from(MOCK_BID).checked_mul(&multiplier).unwrap(),
ask_price: BigInt::from(MOCK_ASK).checked_mul(&multiplier).unwrap(),
expiry_time: MOCK_EXPIRY_TIME,
expiry_time: MOCK_EXPIRY_TIME.to_string(),
first_day_of_notice: MOCK_FIRST_DAY_OF_NOTICE,
last_seen_timestamp_ns: MOCK_LAST_SEEN_TIMESTAMP_NS,
market_status: MOCK_MARKET_STATUS,
contract_month: MOCK_CONTRACT_MONTH.to_string(),
contract_month: MOCK_CONTRACT_MONTH,
goldman_roll_price: BigInt::from(MOCK_GOLDMAN_ROLL_PRICE)
.checked_mul(&multiplier)
.unwrap(),
current_business_day: MOCK_CURRENT_BUSINESS_DAY,
interpolated_goldman_roll_price: BigInt::from(MOCK_INTERPOLATED_GOLDMAN_ROLL_PRICE)
.checked_mul(&multiplier)
.unwrap(),
};

report_data
Expand Down Expand Up @@ -942,7 +952,7 @@ mod tests {

let decoded_report = ReportDataV14::decode(&report_blob).unwrap();

// V14 carries a dynamic `contractMonth` string, so assert the decoded values
// V14 carries a dynamic `expiryTime` string, so assert the decoded values
// round-trip through the full-report path rather than comparing a fixed hex blob.
assert_eq!(decoded_report.feed_id, V14_FEED_ID);
assert_eq!(decoded_report.valid_from_timestamp, MOCK_TIMESTAMP);
Expand All @@ -956,5 +966,23 @@ mod tests {
);
assert_eq!(decoded_report.market_status, MOCK_MARKET_STATUS);
assert_eq!(decoded_report.contract_month, MOCK_CONTRACT_MONTH);

let multiplier: BigInt = "1000000000000000000".parse::<BigInt>().unwrap(); // 1.0 with 18 decimals
assert_eq!(
decoded_report.goldman_roll_price,
BigInt::from(MOCK_GOLDMAN_ROLL_PRICE)
.checked_mul(&multiplier)
.unwrap()
);
assert_eq!(
decoded_report.current_business_day,
MOCK_CURRENT_BUSINESS_DAY
);
Comment thread
Copilot marked this conversation as resolved.
assert_eq!(
decoded_report.interpolated_goldman_roll_price,
BigInt::from(MOCK_INTERPOLATED_GOLDMAN_ROLL_PRICE)
.checked_mul(&"1000000000000000000".parse::<BigInt>().unwrap())
.unwrap()
);
}
}
Loading
Loading