Skip to content

Commit 0a95ca5

Browse files
committed
Add support for V13.
1 parent 0bef226 commit 0a95ca5

File tree

14 files changed

+606
-23
lines changed

14 files changed

+606
-23
lines changed

go/report/v13/data.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package v10
2+
3+
import (
4+
"fmt"
5+
"math/big"
6+
7+
"github.com/ethereum/go-ethereum/accounts/abi"
8+
9+
"github.com/smartcontractkit/data-streams-sdk/go/feed"
10+
)
11+
12+
var schema = Schema()
13+
14+
// Schema returns this data version schema
15+
func Schema() abi.Arguments {
16+
mustNewType := func(t string) abi.Type {
17+
result, err := abi.NewType(t, "", []abi.ArgumentMarshaling{})
18+
if err != nil {
19+
panic(fmt.Sprintf("Unexpected error during abi.NewType: %s", err))
20+
}
21+
return result
22+
}
23+
return abi.Arguments([]abi.Argument{
24+
{Name: "feedId", Type: mustNewType("bytes32")},
25+
{Name: "validFromTimestamp", Type: mustNewType("uint32")},
26+
{Name: "observationsTimestamp", Type: mustNewType("uint32")},
27+
{Name: "nativeFee", Type: mustNewType("uint192")},
28+
{Name: "linkFee", Type: mustNewType("uint192")},
29+
{Name: "expiresAt", Type: mustNewType("uint32")},
30+
{Name: "lastUpdateTimestamp", Type: mustNewType("uint64")},
31+
{Name: "bestAsk", Type: mustNewType("int192")},
32+
{Name: "bestBid", Type: mustNewType("int192")},
33+
{Name: "askVolume", Type: mustNewType("uint64")},
34+
{Name: "bidVolume", Type: mustNewType("uint64")},
35+
{Name: "lastTradedPrice", Type: mustNewType("int192")},
36+
})
37+
}
38+
39+
// Data is the container for this schema's attributes
40+
type Data struct {
41+
FeedID feed.ID `abi:"feedId"`
42+
ValidFromTimestamp uint32
43+
ObservationsTimestamp uint32
44+
NativeFee *big.Int
45+
LinkFee *big.Int
46+
ExpiresAt uint32
47+
LastUpdateTimestamp uint64
48+
BestAsk *big.Int
49+
BestBid *big.Int
50+
AskVolume uint64
51+
BidVolume uint64
52+
LastTradedPrice *big.Int
53+
}
54+
55+
// Schema returns this data version schema
56+
func (Data) Schema() abi.Arguments {
57+
return Schema()
58+
}
59+
60+
// Decode decodes the serialized data bytes
61+
func Decode(data []byte) (*Data, error) {
62+
values, err := schema.Unpack(data)
63+
if err != nil {
64+
return nil, fmt.Errorf("failed to decode report: %w", err)
65+
}
66+
decoded := new(Data)
67+
if err = schema.Copy(decoded, values); err != nil {
68+
return nil, fmt.Errorf("failed to copy report values to struct: %w", err)
69+
}
70+
return decoded, nil
71+
}

go/report/v13/data_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package v10
2+
3+
import (
4+
"math/big"
5+
"reflect"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestData(t *testing.T) {
11+
r := &Data{
12+
FeedID: [32]uint8{0, 13, 19, 169, 185, 197, 227, 122, 9, 159, 55, 78, 146, 195, 121, 20, 175, 92, 38, 143, 58, 138, 151, 33, 241, 114, 81, 53, 191, 180, 203, 184},
13+
ValidFromTimestamp: uint32(time.Now().Unix()),
14+
ObservationsTimestamp: uint32(time.Now().Unix()),
15+
NativeFee: big.NewInt(10),
16+
LinkFee: big.NewInt(10),
17+
ExpiresAt: uint32(time.Now().Unix()) + 100,
18+
LastUpdateTimestamp: uint64(time.Now().UnixNano()) - 100,
19+
BestAsk: big.NewInt(105),
20+
BestBid: big.NewInt(101),
21+
AskVolume: 10001,
22+
BidVolume: 10002,
23+
LastTradedPrice: big.NewInt(103),
24+
}
25+
26+
b, err := schema.Pack(
27+
r.FeedID,
28+
r.ValidFromTimestamp,
29+
r.ObservationsTimestamp,
30+
r.NativeFee,
31+
r.LinkFee,
32+
r.ExpiresAt,
33+
r.LastUpdateTimestamp,
34+
r.BestAsk,
35+
r.BestBid,
36+
r.AskVolume,
37+
r.BidVolume,
38+
r.LastTradedPrice,
39+
)
40+
41+
if err != nil {
42+
t.Errorf("failed to serialize report: %s", err)
43+
}
44+
45+
d, err := Decode(b)
46+
if err != nil {
47+
t.Errorf("failed to deserialize report: %s", err)
48+
}
49+
50+
if !reflect.DeepEqual(r, d) {
51+
t.Errorf("expected: %#v, got %#v", r, d)
52+
}
53+
}

rust/crates/report/src/report.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ use serde::{Deserialize, Serialize};
3838
/// observations_timestamp: 1718885772,
3939
/// full_report: "00016b4aa7e57ca7b68ae1bf45653f56b656fd3aa335ef7fae696b663f1b84720000000000000000000000000000000000000000000000000000000066741d8c00000000000000000000000000000000000000000000000000000000000000640000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000640000070407020401522602090605060802080505a335ef7fae696b663f1b840100000000000000000000000000000000000000000000000000000000000bbbda0000000000000000000000000000000000000000000000000000000066741d8c".to_string(),
4040
/// };
41-
/// ```
41+
/// ```
4242
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4343
pub struct Report {
4444
#[serde(rename = "feedID")]
@@ -307,7 +307,7 @@ mod tests {
307307
pub fn generate_mock_report_data_v9() -> ReportDataV9 {
308308
const MOCK_NAV_PER_SHARE: isize = 1;
309309
const MOCK_AUM: isize = 1000;
310-
const RIPCORD_NORMAL: u32 = 0;
310+
const RIPCORD_NORMAL: u32 = 0;
311311

312312
let report_data = ReportDataV9 {
313313
feed_id: V9_FEED_ID,
@@ -347,6 +347,27 @@ mod tests {
347347
report_data
348348
}
349349

350+
pub fn generate_mock_report_data_v13() -> ReportDataV10 {
351+
const MOCK_MULTIPLIER: isize = 1000000000000000000; // 1.0 with 18 decimals
352+
353+
let report_data = ReportDataV10 {
354+
feed_id: V10_FEED_ID,
355+
valid_from_timestamp: MOCK_TIMESTAMP,
356+
observations_timestamp: MOCK_TIMESTAMP,
357+
native_fee: BigInt::from(MOCK_FEE),
358+
link_fee: BigInt::from(MOCK_FEE),
359+
expires_at: MOCK_TIMESTAMP + 100,
360+
last_update_timestamp: MOCK_TIMESTAMP as u64,
361+
best_ask: BigInt::from(MOCK_BEST_ASK),
362+
best_bid: BigInt::from(MOCK_BEST_BID),
363+
ask_volume: MARKET_ASK_VOLUME,
364+
bid_volume: MARKET_BID_VOLUME,
365+
last_traded_price: BigInt::from(MOCK_LAST_TRADED_PRICE),
366+
};
367+
368+
report_data
369+
}
370+
350371
fn generate_mock_report(encoded_report_data: &[u8]) -> Vec<u8> {
351372
let mut payload = Vec::new();
352373

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use crate::feed_id::ID;
2+
use crate::report::base::{ReportBase, ReportError};
3+
4+
use num_bigint::BigInt;
5+
6+
/// Represents a Report Data V13 Schema.
7+
///
8+
/// This schema provides the best bid/ask prices, bid/ask volume and last traded price.
9+
///
10+
/// # Parameters
11+
/// - `feed_id`: The feed ID the report has data for.
12+
/// - `valid_from_timestamp`: Earliest timestamp for which price is applicable.
13+
/// - `observations_timestamp`: Latest timestamp for which price is applicable.
14+
/// - `native_fee`: Base cost to validate a transaction using the report, denominated in the chain's native token (e.g., WETH/ETH).
15+
/// - `link_fee`: Base cost to validate a transaction using the report, denominated in LINK.
16+
/// - `expires_at`: Latest timestamp where the report can be verified onchain.
17+
/// - `last_update_timestamp`: Timestamp of the last valid price update.
18+
/// - `best_ask`: The best (lowest) ask price (18 decimal precision).
19+
/// - `best_bid`: The best (highest) bid price (18 decimal precision).
20+
/// - `ask_volume`: Total volume of current ask positions.
21+
/// - `bid_volume`: Total volume of current bid positions.
22+
/// - `last_traded_price`: The price at which the latest transaction was completed (18 decimal precision).
23+
///
24+
/// # Solidity Equivalent
25+
/// ```solidity
26+
/// struct ReportDataV13 {
27+
/// bytes32 feedId;
28+
/// uint32 validFromTimestamp;
29+
/// uint32 observationsTimestamp;
30+
/// uint192 nativeFee;
31+
/// uint192 linkFee;
32+
/// uint32 expiresAt;
33+
/// uint64 lastUpdateTimestamp;
34+
/// int192 best_ask;
35+
/// int192 best_bid;
36+
/// uint64 ask_volume;
37+
/// uint64 bid_volume;
38+
/// int192 last_traded_price;
39+
/// }
40+
/// ```
41+
#[derive(Debug)]
42+
pub struct ReportDataV13 {
43+
pub feed_id: ID,
44+
pub valid_from_timestamp: u32,
45+
pub observations_timestamp: u32,
46+
pub native_fee: BigInt,
47+
pub link_fee: BigInt,
48+
pub expires_at: u32,
49+
pub last_update_timestamp: u64,
50+
pub best_ask: BigInt,
51+
pub best_bid: BigInt,
52+
pub ask_volume: u64,
53+
pub bid_volume: u64,
54+
pub last_traded_price: BigInt,
55+
}
56+
57+
impl ReportDataV13 {
58+
/// Decodes an ABI-encoded `ReportDataV13` from bytes.
59+
///
60+
/// # Parameters
61+
///
62+
/// - `data`: The encoded report data.
63+
///
64+
/// # Returns
65+
///
66+
/// The decoded `ReportDataV13`.
67+
///
68+
/// # Errors
69+
///
70+
/// Returns a `ReportError` if the data is too short or if the data is invalid.
71+
pub fn decode(data: &[u8]) -> Result<Self, ReportError> {
72+
if data.len() < 12 * ReportBase::WORD_SIZE {
73+
return Err(ReportError::DataTooShort("ReportDataV13"));
74+
}
75+
76+
let feed_id = ID(data[..ReportBase::WORD_SIZE]
77+
.try_into()
78+
.map_err(|_| ReportError::InvalidLength("feed_id (bytes32)"))?);
79+
80+
let valid_from_timestamp = ReportBase::read_uint32(data, ReportBase::WORD_SIZE)?;
81+
let observations_timestamp = ReportBase::read_uint32(data, 2 * ReportBase::WORD_SIZE)?;
82+
let native_fee = ReportBase::read_uint192(data, 3 * ReportBase::WORD_SIZE)?;
83+
let link_fee = ReportBase::read_uint192(data, 4 * ReportBase::WORD_SIZE)?;
84+
let expires_at = ReportBase::read_uint32(data, 5 * ReportBase::WORD_SIZE)?;
85+
let last_update_timestamp = ReportBase::read_uint64(data, 6 * ReportBase::WORD_SIZE)?;
86+
let best_ask = ReportBase::read_int192(data, 7 * ReportBase::WORD_SIZE)?;
87+
let best_bid = ReportBase::read_int192(data, 8 * ReportBase::WORD_SIZE)?;
88+
let ask_volume = ReportBase::read_uint64(data, 9 * ReportBase::WORD_SIZE)?;
89+
let bid_volume = ReportBase::read_uint64(data, 10 * ReportBase::WORD_SIZE)?;
90+
let last_traded_price = ReportBase::read_int192(data, 11 * ReportBase::WORD_SIZE)?;
91+
92+
Ok(Self {
93+
feed_id,
94+
valid_from_timestamp,
95+
observations_timestamp,
96+
native_fee,
97+
link_fee,
98+
expires_at,
99+
last_update_timestamp,
100+
best_ask,
101+
best_bid,
102+
ask_volume,
103+
bid_volume,
104+
last_traded_price,
105+
})
106+
}
107+
108+
/// Encodes the `ReportDataV13` into an ABI-encoded byte array.
109+
///
110+
/// # Returns
111+
///
112+
/// The ABI-encoded report data.
113+
///
114+
/// # Errors
115+
///
116+
/// Returns a `ReportError` if the data is invalid.
117+
pub fn abi_encode(&self) -> Result<Vec<u8>, ReportError> {
118+
let mut buffer = Vec::with_capacity(13 * ReportBase::WORD_SIZE);
119+
120+
buffer.extend_from_slice(&self.feed_id.0);
121+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.valid_from_timestamp)?);
122+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.observations_timestamp)?);
123+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.native_fee)?);
124+
buffer.extend_from_slice(&ReportBase::encode_uint192(&self.link_fee)?);
125+
buffer.extend_from_slice(&ReportBase::encode_uint32(self.expires_at)?);
126+
buffer.extend_from_slice(&ReportBase::encode_uint64(self.last_update_timestamp)?);
127+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.best_ask)?);
128+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.best_bid)?);
129+
buffer.extend_from_slice(&ReportBase::encode_uint64(self.ask_volume)?);
130+
buffer.extend_from_slice(&ReportBase::encode_uint64(self.bid_volume)?);
131+
buffer.extend_from_slice(&ReportBase::encode_int192(&self.last_traded_price)?);
132+
133+
Ok(buffer)
134+
}
135+
}
136+
137+
#[cfg(test)]
138+
mod tests {
139+
use super::*;
140+
use crate::report::tests::{
141+
generate_mock_report_data_v13, MOCK_FEE, MOCK_TIMESTAMP, MOCK_BEST_ASK, MOCK_BEST_BID, MOCK_ASK_VOLUME,
142+
MOCK_BID_VOLUME, MOCK_LAST_TRADED_PRICE
143+
};
144+
145+
const V13_FEED_ID_STR: &str =
146+
"0x000d13a9b9c5e37a099f374e92c37914af5c268f3a8a9721f1725135bfb4cbb8";
147+
148+
#[test]
149+
fn test_decode_report_data_v13() {
150+
let report_data = generate_mock_report_data_v13();
151+
let encoded = report_data.abi_encode().unwrap();
152+
let decoded = ReportDataV13::decode(&encoded).unwrap();
153+
154+
const MOCK_MULTIPLIER: isize = 1000000000000000000;
155+
156+
let expected_feed_id = ID::from_hex_str(V13_FEED_ID_STR).unwrap();
157+
let expected_timestamp: u32 = MOCK_TIMESTAMP;
158+
let expected_fee = BigInt::from(MOCK_FEE);
159+
let expected_best_ask = BigInt::from(MOCK_BEST_ASK);
160+
let expected_best_bid = BigInt::from(MOCK_BEST_BID);
161+
let expected_ask_volume: u64 = MOCK_ASK_VOLUME;
162+
let expected_bid_volume: u64 = MOCK_BID_VOLUME;
163+
let expected_last_traded_price = BigInt::from(MOCK_LAST_TRADED_PRICE);
164+
165+
assert_eq!(decoded.feed_id, expected_feed_id);
166+
assert_eq!(decoded.valid_from_timestamp, expected_timestamp);
167+
assert_eq!(decoded.observations_timestamp, expected_timestamp);
168+
assert_eq!(decoded.native_fee, expected_fee);
169+
assert_eq!(decoded.link_fee, expected_fee);
170+
assert_eq!(decoded.expires_at, expected_timestamp + 100);
171+
assert_eq!(decoded.last_update_timestamp, expected_timestamp as u64);
172+
assert_eq!(decoded.best_ask, expected_best_ask);
173+
assert_eq!(decoded.best_bid, expected_best_bid);
174+
assert_eq!(decoded.ask_volume, expected_ask_volume);
175+
assert_eq!(decoded.bid_volume, expected_bid_volume);
176+
assert_eq!(decoded.last_traded_price, expected_last_traded_price);
177+
}
178+
}

typescript/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ TypeScript SDK for accessing Chainlink Data Streams with real-time streaming and
2727
- **Real-time streaming** via WebSocket connections
2828
- **High Availability mode** with multiple connections and automatic failover
2929
- **Historical data access** via REST API
30-
- **Automatic report decoding** for all supported formats (V2, V3, V4, V5, V6, V7, V8, V9, V10)
30+
- **Automatic report decoding** for all supported formats (V2, V3, V4, V5, V6, V7, V8, V9, V10, V13)
3131
- **Metrics** for monitoring and observability
3232
- **Type-safe** with full TypeScript support
3333
- **Event-driven architecture** for complete developer control
@@ -244,13 +244,14 @@ The SDK automatically detects and decodes all report versions based on Feed ID p
244244
- **V8**: Feed IDs starting with `0x0008` (Non-OTC RWA)
245245
- **V9**: Feed IDs starting with `0x0009` (NAV Fund Data)
246246
- **V10**: Feed IDs starting with `0x000a` (Tokenized Equity)
247+
- **V13**: Feed IDs starting with `0x000d` (Best Bid/Ask)
247248
248249
### Common Fields
249250
250251
All reports include standard metadata:
251252
```typescript
252253
interface BaseFields {
253-
version: "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10";
254+
version: "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10" | "V13";
254255
nativeFee: bigint;
255256
linkFee: bigint;
256257
expiresAt: number;
@@ -271,6 +272,7 @@ interface BaseFields {
271272
- **V8**: `midPrice: bigint, lastUpdateTimestamp: number, marketStatus: MarketStatus` - Non-OTC RWA data
272273
- **V9**: `navPerShare: bigint, navDate: number, aum: bigint, ripcord: number` - NAV fund data
273274
- **V10**: `price: bigint, lastUpdateTimestamp: number, marketStatus: MarketStatus, currentMultiplier: bigint, newMultiplier: bigint, activationDateTime: number, tokenizedPrice: bigint` - Tokenized equity data
275+
- **V13**: `bestAsk: bigint, bestBid: bigint, askVolume: number, bidVOlume: number, lastTradedPrice: bigint` - Best Bid/Ask
274276
275277
For complete field definitions, see the [documentation](https://docs.chain.link/data-streams/reference/report-schema-v3).
276278

typescript/examples/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ npx ts-node examples/list-feeds.ts
8787

8888
## Feed IDs
8989

90-
The SDK automatically detects and supports all report schema versions (V2, V3, V4, V5, V6, V7, V8, V9, V10).
90+
The SDK automatically detects and supports all report schema versions (V2, V3, V4, V5, V6, V7, V8, V9, V10, V13).
9191

9292
For available feed IDs, see the official [Chainlink documentation](https://docs.chain.link/data-streams/).
9393

@@ -149,4 +149,4 @@ const config = {
149149
// logLevel: LogLevel.INFO
150150
// }
151151
};
152-
```
152+
```

0 commit comments

Comments
 (0)