Skip to content

Commit 7727d3c

Browse files
committed
fix(price-service-sdk): make price-service-sdk portable
The price-service-sdk package previously used `Buffers` in it's interface in many places. However, `Buffer` is a node-specific interface which does not exist in browsers. Many browsers ship [a polyfill](https://github.com/feross/buffer), however I've discovered that the polyfill is not actually fully compatible with the Node module. In particular, while the Node module [supports aliases such as `readUint8` for `readUInt8`](https://nodejs.org/api/buffer.html#bufreaduint8offset) (note the difference in capitalization), [the polyfill does not](https://github.com/feross/buffer/blob/master/index.d.ts). As a result, attempting to utilize `price-service-sdk` in the browser was either going to cause errors because the Buffer sdk isn't present, or it would cause errors because of the use of aliased function names. There were three options to fix this issue: 1. Switch everything to using unaliased function names. This would work portably, but only assuming that the Buffer polyfill is present. 2. Switch everything to using [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array), which is a more generic [typed array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) and largely supersedes the need to use the `Buffer` class at all. This would work, however it would be a breaking change. 3. Switch everything to using a generic interface which accepts anything which extends `Uint8Array`. This requires a few gross typescript hacks to type check correctly and avoid anything being a breaking change, but it makes the code the most portable without requiring any major version or any consumer code changes. This commit implements option 3.
1 parent 74e976f commit 7727d3c

File tree

2 files changed

+105
-35
lines changed

2 files changed

+105
-35
lines changed

price_service/sdk/js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@pythnetwork/price-service-sdk",
3-
"version": "1.8.0",
3+
"version": "1.9.0",
44
"description": "Pyth price service SDK",
55
"homepage": "https://pyth.network",
66
"main": "lib/index.js",

price_service/sdk/js/src/AccumulatorUpdateData.ts

Lines changed: 104 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import BN from "bn.js";
2+
import type { Buffer } from "buffer";
23

34
const ACCUMULATOR_MAGIC = "504e4155";
45
const MAJOR_VERSION = 1;
@@ -7,12 +8,12 @@ const KECCAK160_HASH_SIZE = 20;
78
const PRICE_FEED_MESSAGE_VARIANT = 0;
89
const TWAP_MESSAGE_VARIANT = 1;
910

10-
export type AccumulatorUpdateData = {
11-
vaa: Buffer;
12-
updates: { message: Buffer; proof: number[][] }[];
11+
export type AccumulatorUpdateData<T extends Uint8ArrayLike = Buffer> = {
12+
vaa: T;
13+
updates: { message: T; proof: number[][] }[];
1314
};
14-
export type PriceFeedMessage = {
15-
feedId: Buffer;
15+
export type PriceFeedMessage<T extends Uint8ArrayLike = Buffer> = {
16+
feedId: T;
1617
price: BN;
1718
confidence: BN;
1819
exponent: number;
@@ -22,8 +23,8 @@ export type PriceFeedMessage = {
2223
emaConf: BN;
2324
};
2425

25-
export type TwapMessage = {
26-
feedId: Buffer;
26+
export type TwapMessage<T extends Uint8ArrayLike = Buffer> = {
27+
feedId: T;
2728
cumulativePrice: BN;
2829
cumulativeConf: BN;
2930
numDownSlots: BN;
@@ -33,17 +34,22 @@ export type TwapMessage = {
3334
publishSlot: BN;
3435
};
3536

36-
export function isAccumulatorUpdateData(updateBytes: Buffer): boolean {
37+
export function isAccumulatorUpdateData<T extends Uint8ArrayLike>(
38+
updateBytes: T
39+
): boolean {
3740
return (
38-
updateBytes.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC &&
41+
toHex(updateBytes).slice(0, 8) === ACCUMULATOR_MAGIC &&
3942
updateBytes[4] === MAJOR_VERSION &&
4043
updateBytes[5] === MINOR_VERSION
4144
);
4245
}
4346

44-
export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
47+
export function parsePriceFeedMessage<T extends Uint8ArrayLike>(
48+
message: T
49+
): PriceFeedMessage<T> {
4550
let cursor = 0;
46-
const variant = message.readUInt8(cursor);
51+
const dataView = getDataView(message);
52+
const variant = dataView.getUint8(cursor);
4753
if (variant !== PRICE_FEED_MESSAGE_VARIANT) {
4854
throw new Error("Not a price feed message");
4955
}
@@ -54,7 +60,7 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
5460
cursor += 8;
5561
const confidence = new BN(message.subarray(cursor, cursor + 8), "be");
5662
cursor += 8;
57-
const exponent = message.readInt32BE(cursor);
63+
const exponent = dataView.getInt32(cursor);
5864
cursor += 4;
5965
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
6066
cursor += 8;
@@ -76,9 +82,12 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
7682
};
7783
}
7884

79-
export function parseTwapMessage(message: Buffer): TwapMessage {
85+
export function parseTwapMessage<T extends Uint8ArrayLike>(
86+
message: T
87+
): TwapMessage<T> {
8088
let cursor = 0;
81-
const variant = message.readUInt8(cursor);
89+
const dataView = getDataView(message);
90+
const variant = dataView.getUint8(cursor);
8291
if (variant !== TWAP_MESSAGE_VARIANT) {
8392
throw new Error("Not a twap message");
8493
}
@@ -91,7 +100,7 @@ export function parseTwapMessage(message: Buffer): TwapMessage {
91100
cursor += 16;
92101
const numDownSlots = new BN(message.subarray(cursor, cursor + 8), "be");
93102
cursor += 8;
94-
const exponent = message.readInt32BE(cursor);
103+
const exponent = dataView.getInt32(cursor);
95104
cursor += 4;
96105
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
97106
cursor += 8;
@@ -114,38 +123,39 @@ export function parseTwapMessage(message: Buffer): TwapMessage {
114123
/**
115124
* An AccumulatorUpdateData contains a VAA and a list of updates. This function returns a new serialized AccumulatorUpdateData with only the updates in the range [start, end).
116125
*/
117-
export function sliceAccumulatorUpdateData(
118-
data: Buffer,
126+
export function sliceAccumulatorUpdateData<T extends Uint8ArrayLike>(
127+
data: T,
119128
start?: number,
120129
end?: number
121-
): Buffer {
130+
): T {
122131
if (!isAccumulatorUpdateData(data)) {
123132
throw new Error("Invalid accumulator message");
124133
}
125134
let cursor = 6;
126-
const trailingPayloadSize = data.readUint8(cursor);
135+
const dataView = getDataView(data);
136+
const trailingPayloadSize = dataView.getUint8(cursor);
127137
cursor += 1 + trailingPayloadSize;
128138

129139
// const proofType = data.readUint8(cursor);
130140
cursor += 1;
131141

132-
const vaaSize = data.readUint16BE(cursor);
142+
const vaaSize = dataView.getUint16(cursor);
133143
cursor += 2;
134144
cursor += vaaSize;
135145

136146
const endOfVaa = cursor;
137147

138148
const updates = [];
139-
const numUpdates = data.readUInt8(cursor);
149+
const numUpdates = dataView.getUint8(cursor);
140150
cursor += 1;
141151

142152
for (let i = 0; i < numUpdates; i++) {
143153
const updateStart = cursor;
144-
const messageSize = data.readUint16BE(cursor);
154+
const messageSize = dataView.getUint16(cursor);
145155
cursor += 2;
146156
cursor += messageSize;
147157

148-
const numProofs = data.readUInt8(cursor);
158+
const numProofs = dataView.getUint8(cursor);
149159
cursor += 1;
150160
cursor += KECCAK160_HASH_SIZE * numProofs;
151161

@@ -157,44 +167,45 @@ export function sliceAccumulatorUpdateData(
157167
}
158168

159169
const sliceUpdates = updates.slice(start, end);
160-
return Buffer.concat([
170+
return mergeUint8ArrayLikes([
161171
data.subarray(0, endOfVaa),
162-
Buffer.from([sliceUpdates.length]),
172+
fromAsTypeOf(data, [sliceUpdates.length]),
163173
...updates.slice(start, end),
164174
]);
165175
}
166176

167-
export function parseAccumulatorUpdateData(
168-
data: Buffer
169-
): AccumulatorUpdateData {
177+
export function parseAccumulatorUpdateData<T extends Uint8ArrayLike>(
178+
data: T
179+
): AccumulatorUpdateData<T> {
170180
if (!isAccumulatorUpdateData(data)) {
171181
throw new Error("Invalid accumulator message");
172182
}
173183

174184
let cursor = 6;
175-
const trailingPayloadSize = data.readUint8(cursor);
185+
const dataView = getDataView(data);
186+
const trailingPayloadSize = dataView.getUint8(cursor);
176187
cursor += 1 + trailingPayloadSize;
177188

178-
// const proofType = data.readUint8(cursor);
189+
// const proofType = data.getUint8(cursor);
179190
cursor += 1;
180191

181-
const vaaSize = data.readUint16BE(cursor);
192+
const vaaSize = dataView.getUint16(cursor);
182193
cursor += 2;
183194

184195
const vaa = data.subarray(cursor, cursor + vaaSize);
185196
cursor += vaaSize;
186197

187-
const numUpdates = data.readUInt8(cursor);
198+
const numUpdates = dataView.getUint8(cursor);
188199
const updates = [];
189200
cursor += 1;
190201

191202
for (let i = 0; i < numUpdates; i++) {
192-
const messageSize = data.readUint16BE(cursor);
203+
const messageSize = dataView.getUint16(cursor);
193204
cursor += 2;
194205
const message = data.subarray(cursor, cursor + messageSize);
195206
cursor += messageSize;
196207

197-
const numProofs = data.readUInt8(cursor);
208+
const numProofs = dataView.getUint8(cursor);
198209
cursor += 1;
199210
const proof = [];
200211
for (let j = 0; j < numProofs; j++) {
@@ -213,3 +224,62 @@ export function parseAccumulatorUpdateData(
213224

214225
return { vaa, updates };
215226
}
227+
228+
function mergeUint8ArrayLikes<T extends Uint8ArrayLike>(
229+
inputs: [T, ...T[]]
230+
): T {
231+
const out = createAsTypeOf(
232+
inputs[0],
233+
inputs.reduce((acc, arr) => acc + arr.length, 0)
234+
);
235+
let offset = 0;
236+
for (const arr of inputs) {
237+
out.set(arr, offset);
238+
offset += arr.length;
239+
}
240+
return out;
241+
}
242+
243+
function toHex(input: Uint8ArrayLike): string {
244+
return Array.from(input)
245+
.map((value) => value.toString(16).padStart(2, "0"))
246+
.join("");
247+
}
248+
249+
// With Uint8Arrays, we could just do `new DataView(buf.buffer)`. But to
250+
// account for `Buffers`, we need to slice to the used space since `Buffers` may
251+
// be allocated to be larger than needed.
252+
function getDataView(buf: Uint8ArrayLike) {
253+
return new DataView(
254+
buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
255+
);
256+
}
257+
258+
// This is a little bit of a typescript hack -- we know that `Buffer.from` and
259+
// `Uint8Array.from` behave effectively the same and we just want a `from` which
260+
// will return either a `Uint8Array` or a `Buffer`, depending on the type of
261+
// some other variable. But typescript sucks at typechecking prototypes so
262+
// there's really no other good way I'm aware of to do this besides a bit of
263+
// typecasting through `any`.
264+
function fromAsTypeOf<T extends Uint8ArrayLike>(
265+
buf: T,
266+
...args: Parameters<typeof Uint8Array.from>
267+
) {
268+
return Object.getPrototypeOf(buf.constructor).from(...args) as T;
269+
}
270+
271+
// Similar to `fromAsTypeOf`, here we want to be able to create either a
272+
// `Buffer` or a `Uint8Array`, matching the type of a passed in value. But this
273+
// is a bit more complex, because for `Uint8Array` we should do that with the
274+
// `Uint8Array` constructor, where for `Buffer` we should do that using
275+
// `Buffer.alloc`. This is a bit of a weird hack but I don't know a better way
276+
// to make typescript handle such cases.
277+
function createAsTypeOf<T extends Uint8ArrayLike>(buf: T, size: number) {
278+
const ctor = buf.constructor;
279+
const create = ("alloc" in ctor ? ctor.alloc : ctor) as (size: number) => T;
280+
return create(size);
281+
}
282+
283+
interface Uint8ArrayLike extends Uint8Array {
284+
subarray: (from: number, to: number) => this;
285+
}

0 commit comments

Comments
 (0)