Skip to content

Commit 21de2de

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 21de2de

File tree

1 file changed

+103
-34
lines changed

1 file changed

+103
-34
lines changed

price_service/sdk/js/src/AccumulatorUpdateData.ts

Lines changed: 103 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ const KECCAK160_HASH_SIZE = 20;
77
const PRICE_FEED_MESSAGE_VARIANT = 0;
88
const TWAP_MESSAGE_VARIANT = 1;
99

10-
export type AccumulatorUpdateData = {
11-
vaa: Buffer;
12-
updates: { message: Buffer; proof: number[][] }[];
10+
export type AccumulatorUpdateData<T extends Uint8ArrayLike> = {
11+
vaa: T;
12+
updates: { message: T; proof: number[][] }[];
1313
};
14-
export type PriceFeedMessage = {
15-
feedId: Buffer;
14+
export type PriceFeedMessage<T extends Uint8ArrayLike> = {
15+
feedId: T;
1616
price: BN;
1717
confidence: BN;
1818
exponent: number;
@@ -22,8 +22,8 @@ export type PriceFeedMessage = {
2222
emaConf: BN;
2323
};
2424

25-
export type TwapMessage = {
26-
feedId: Buffer;
25+
export type TwapMessage<T extends Uint8ArrayLike> = {
26+
feedId: T;
2727
cumulativePrice: BN;
2828
cumulativeConf: BN;
2929
numDownSlots: BN;
@@ -33,17 +33,22 @@ export type TwapMessage = {
3333
publishSlot: BN;
3434
};
3535

36-
export function isAccumulatorUpdateData(updateBytes: Buffer): boolean {
36+
export function isAccumulatorUpdateData<T extends Uint8ArrayLike>(
37+
updateBytes: T
38+
): boolean {
3739
return (
38-
updateBytes.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC &&
40+
toHex(updateBytes).slice(0, 8) === ACCUMULATOR_MAGIC &&
3941
updateBytes[4] === MAJOR_VERSION &&
4042
updateBytes[5] === MINOR_VERSION
4143
);
4244
}
4345

44-
export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
46+
export function parsePriceFeedMessage<T extends Uint8ArrayLike>(
47+
message: T
48+
): PriceFeedMessage<T> {
4549
let cursor = 0;
46-
const variant = message.readUInt8(cursor);
50+
const dataView = getDataView(message);
51+
const variant = dataView.getUint8(cursor);
4752
if (variant !== PRICE_FEED_MESSAGE_VARIANT) {
4853
throw new Error("Not a price feed message");
4954
}
@@ -54,7 +59,7 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
5459
cursor += 8;
5560
const confidence = new BN(message.subarray(cursor, cursor + 8), "be");
5661
cursor += 8;
57-
const exponent = message.readInt32BE(cursor);
62+
const exponent = dataView.getInt32(cursor);
5863
cursor += 4;
5964
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
6065
cursor += 8;
@@ -76,9 +81,12 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
7681
};
7782
}
7883

79-
export function parseTwapMessage(message: Buffer): TwapMessage {
84+
export function parseTwapMessage<T extends Uint8ArrayLike>(
85+
message: T
86+
): TwapMessage<T> {
8087
let cursor = 0;
81-
const variant = message.readUInt8(cursor);
88+
const dataView = getDataView(message);
89+
const variant = dataView.getUint8(cursor);
8290
if (variant !== TWAP_MESSAGE_VARIANT) {
8391
throw new Error("Not a twap message");
8492
}
@@ -91,7 +99,7 @@ export function parseTwapMessage(message: Buffer): TwapMessage {
9199
cursor += 16;
92100
const numDownSlots = new BN(message.subarray(cursor, cursor + 8), "be");
93101
cursor += 8;
94-
const exponent = message.readInt32BE(cursor);
102+
const exponent = dataView.getInt32(cursor);
95103
cursor += 4;
96104
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
97105
cursor += 8;
@@ -114,38 +122,39 @@ export function parseTwapMessage(message: Buffer): TwapMessage {
114122
/**
115123
* 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).
116124
*/
117-
export function sliceAccumulatorUpdateData(
118-
data: Buffer,
125+
export function sliceAccumulatorUpdateData<T extends Uint8ArrayLike>(
126+
data: T,
119127
start?: number,
120128
end?: number
121-
): Buffer {
129+
): T {
122130
if (!isAccumulatorUpdateData(data)) {
123131
throw new Error("Invalid accumulator message");
124132
}
125133
let cursor = 6;
126-
const trailingPayloadSize = data.readUint8(cursor);
134+
const dataView = getDataView(data);
135+
const trailingPayloadSize = dataView.getUint8(cursor);
127136
cursor += 1 + trailingPayloadSize;
128137

129138
// const proofType = data.readUint8(cursor);
130139
cursor += 1;
131140

132-
const vaaSize = data.readUint16BE(cursor);
141+
const vaaSize = dataView.getUint16(cursor);
133142
cursor += 2;
134143
cursor += vaaSize;
135144

136145
const endOfVaa = cursor;
137146

138147
const updates = [];
139-
const numUpdates = data.readUInt8(cursor);
148+
const numUpdates = dataView.getUint8(cursor);
140149
cursor += 1;
141150

142151
for (let i = 0; i < numUpdates; i++) {
143152
const updateStart = cursor;
144-
const messageSize = data.readUint16BE(cursor);
153+
const messageSize = dataView.getUint16(cursor);
145154
cursor += 2;
146155
cursor += messageSize;
147156

148-
const numProofs = data.readUInt8(cursor);
157+
const numProofs = dataView.getUint8(cursor);
149158
cursor += 1;
150159
cursor += KECCAK160_HASH_SIZE * numProofs;
151160

@@ -157,44 +166,45 @@ export function sliceAccumulatorUpdateData(
157166
}
158167

159168
const sliceUpdates = updates.slice(start, end);
160-
return Buffer.concat([
169+
return mergeUint8ArrayLikes([
161170
data.subarray(0, endOfVaa),
162-
Buffer.from([sliceUpdates.length]),
171+
fromAsTypeOf(data, [sliceUpdates.length]),
163172
...updates.slice(start, end),
164173
]);
165174
}
166175

167-
export function parseAccumulatorUpdateData(
168-
data: Buffer
169-
): AccumulatorUpdateData {
176+
export function parseAccumulatorUpdateData<T extends Uint8ArrayLike>(
177+
data: T
178+
): AccumulatorUpdateData<T> {
170179
if (!isAccumulatorUpdateData(data)) {
171180
throw new Error("Invalid accumulator message");
172181
}
173182

174183
let cursor = 6;
175-
const trailingPayloadSize = data.readUint8(cursor);
184+
const dataView = getDataView(data);
185+
const trailingPayloadSize = dataView.getUint8(cursor);
176186
cursor += 1 + trailingPayloadSize;
177187

178-
// const proofType = data.readUint8(cursor);
188+
// const proofType = data.getUint8(cursor);
179189
cursor += 1;
180190

181-
const vaaSize = data.readUint16BE(cursor);
191+
const vaaSize = dataView.getUint16(cursor);
182192
cursor += 2;
183193

184194
const vaa = data.subarray(cursor, cursor + vaaSize);
185195
cursor += vaaSize;
186196

187-
const numUpdates = data.readUInt8(cursor);
197+
const numUpdates = dataView.getUint8(cursor);
188198
const updates = [];
189199
cursor += 1;
190200

191201
for (let i = 0; i < numUpdates; i++) {
192-
const messageSize = data.readUint16BE(cursor);
202+
const messageSize = dataView.getUint16(cursor);
193203
cursor += 2;
194204
const message = data.subarray(cursor, cursor + messageSize);
195205
cursor += messageSize;
196206

197-
const numProofs = data.readUInt8(cursor);
207+
const numProofs = dataView.getUint8(cursor);
198208
cursor += 1;
199209
const proof = [];
200210
for (let j = 0; j < numProofs; j++) {
@@ -213,3 +223,62 @@ export function parseAccumulatorUpdateData(
213223

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

0 commit comments

Comments
 (0)