Skip to content
Closed
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
56 changes: 28 additions & 28 deletions src/services/price/protos/price.proto
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
syntax = "proto3";

service PriceFeed {
rpc GetPrice(PriceQuery) returns (PriceResponse) {}
rpc ListCurrencies(ListCurrenciesQuery) returns (ListCurrenciesResponse) {}
}

message PriceResponse {
float price = 1;
}

message PriceQuery {
string currency = 1;
}

message ListCurrenciesQuery { }

message ListCurrenciesResponse {
repeated Currency currencies = 1;
}

message Currency {
string code = 1;
string symbol = 2;
string name = 3;
string flag = 4;
int32 fractionDigits = 5;
}
syntax = "proto3";
service PriceFeed {
rpc GetPrice(PriceQuery) returns (PriceResponse) {}
rpc ListCurrencies(ListCurrenciesQuery) returns (ListCurrenciesResponse) {}
}
message PriceResponse {
double price = 1;
}
message PriceQuery {
string currency = 1;
}
message ListCurrenciesQuery { }
message ListCurrenciesResponse {
repeated Currency currencies = 1;
}
message Currency {
string code = 1;
string symbol = 2;
string name = 3;
string flag = 4;
int32 fractionDigits = 5;
}
52 changes: 26 additions & 26 deletions src/services/price/protos/price_history.proto
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
syntax = "proto3";

service PriceHistory {
rpc ListPrices(PriceHistoryQuery) returns (PriceHistoryResponse) {}
}

message PriceHistoryQuery {
string currency = 1;
enum PriceRange {
OneDay = 0;
OneWeek = 1;
OneMonth = 2;
OneYear = 3;
FiveYears = 4;
}
PriceRange range = 2;
}

message PriceHistoryResponse {
repeated Tick priceHistory = 1;
}

message Tick {
uint64 timestamp = 1;
float price = 2;
}
syntax = "proto3";
service PriceHistory {
rpc ListPrices(PriceHistoryQuery) returns (PriceHistoryResponse) {}
}
message PriceHistoryQuery {
string currency = 1;
enum PriceRange {
OneDay = 0;
OneWeek = 1;
OneMonth = 2;
OneYear = 3;
FiveYears = 4;
}
PriceRange range = 2;
}
message PriceHistoryResponse {
repeated Tick priceHistory = 1;
}
message Tick {
uint64 timestamp = 1;
double price = 2;
}
68 changes: 68 additions & 0 deletions test/flash/unit/services/price/price-proto-double.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import path from "path"

const protobuf = require("protobufjs")

// Both price streams cross the gRPC boundary as a single scalar: `PriceResponse.price`
// (src/services/price/protos/price.proto) and `Tick.price`
// (src/services/price/protos/price_history.proto). `float` in protobuf is 32-bit
// IEEE 754 — about 7 significant digits — so a rate such as 157.1234567 (JMD) or
// 392.9876543 (NGN), multiplied onward through the 8-decimal sat math in
// `getRealTimePrice`, is silently rounded inside the wire layer where no caller can
// see it. `double` keeps the value the JS number already holds.
//
// The stubs are loaded at runtime straight from these .proto files (see
// src/services/price/grpc.ts), so the schema *is* the contract: encoding and decoding
// through the loaded type is the same round trip a gRPC call performs, without needing
// a price-server on the other end.

const PROTO_DIR = path.join(__dirname, "../../../../../src/services/price/protos")

const loadRoot = (file: string) => protobuf.loadSync(path.join(PROTO_DIR, file))

const roundTrip = (root: any, typeName: string, value: number) => {
const type = root.lookupType(typeName)
const bytes = type.encode(type.create({ price: value })).finish()

return type.decode(bytes).price as number
}

describe("price protos keep full rate precision", () => {
const priceRoot = loadRoot("price.proto")
const historyRoot = loadRoot("price_history.proto")

it("declares PriceResponse.price as double", () => {
expect(priceRoot.lookupType("PriceResponse").fields.price.type).toBe("double")
})

it("declares Tick.price as double", () => {
expect(historyRoot.lookupType("Tick").fields.price.type).toBe("double")
})

it.each([
[157.123456789], // JMD-style rate carried through sat-scale precision
[392.987654321], // NGN-style rate
[0.0000000123456], // sub-cent sats-per-unit ratio
])("round-trips %p through PriceResponse bit-for-bit", (rate) => {
expect(roundTrip(priceRoot, "PriceResponse", rate)).toBe(rate)
})

it("round-trips a history tick alongside its timestamp", () => {
const type = historyRoot.lookupType("Tick")
const rate = 157.123456789
const bytes = type
.encode(type.create({ timestamp: "1700000000", price: rate }))
.finish()
const decoded = type.decode(bytes)

expect(decoded.price).toBe(rate)
expect(decoded.timestamp.toString()).toBe("1700000000")
})

it("shows what a 32-bit field would have done to the same rate", () => {
const rate = 157.123456789
const view = new DataView(new ArrayBuffer(8))
view.setFloat32(0, rate)

expect(view.getFloat32(0)).not.toBe(rate)
})
})