Skip to content

Commit

Permalink
fix(price-service-sdk): make price-service-sdk portable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
cprussin committed Jan 10, 2025
1 parent 74e976f commit d46cac4
Showing 1 changed file with 103 additions and 34 deletions.
137 changes: 103 additions & 34 deletions price_service/sdk/js/src/AccumulatorUpdateData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ const KECCAK160_HASH_SIZE = 20;
const PRICE_FEED_MESSAGE_VARIANT = 0;
const TWAP_MESSAGE_VARIANT = 1;

export type AccumulatorUpdateData = {
vaa: Buffer;
updates: { message: Buffer; proof: number[][] }[];
export type AccumulatorUpdateData<T extends Uint8ArrayLike> = {
vaa: T;
updates: { message: T; proof: number[][] }[];
};
export type PriceFeedMessage = {
feedId: Buffer;
export type PriceFeedMessage<T extends Uint8ArrayLike> = {
feedId: T;
price: BN;
confidence: BN;
exponent: number;
Expand All @@ -22,8 +22,8 @@ export type PriceFeedMessage = {
emaConf: BN;
};

export type TwapMessage = {
feedId: Buffer;
export type TwapMessage<T extends Uint8ArrayLike> = {
feedId: T;
cumulativePrice: BN;
cumulativeConf: BN;
numDownSlots: BN;
Expand All @@ -33,17 +33,22 @@ export type TwapMessage = {
publishSlot: BN;
};

export function isAccumulatorUpdateData(updateBytes: Buffer): boolean {
export function isAccumulatorUpdateData<T extends Uint8ArrayLike>(
updateBytes: T
): boolean {
return (
updateBytes.toString("hex").slice(0, 8) === ACCUMULATOR_MAGIC &&
toHex(updateBytes).slice(0, 8) === ACCUMULATOR_MAGIC &&
updateBytes[4] === MAJOR_VERSION &&
updateBytes[5] === MINOR_VERSION
);
}

export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
export function parsePriceFeedMessage<T extends Uint8ArrayLike>(
message: T
): PriceFeedMessage<T> {
let cursor = 0;
const variant = message.readUInt8(cursor);
const dataView = getDataView(message);
const variant = dataView.getUint8(cursor);
if (variant !== PRICE_FEED_MESSAGE_VARIANT) {
throw new Error("Not a price feed message");
}
Expand All @@ -54,7 +59,7 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
cursor += 8;
const confidence = new BN(message.subarray(cursor, cursor + 8), "be");
cursor += 8;
const exponent = message.readInt32BE(cursor);
const exponent = dataView.getInt32(cursor);
cursor += 4;
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
cursor += 8;
Expand All @@ -76,9 +81,12 @@ export function parsePriceFeedMessage(message: Buffer): PriceFeedMessage {
};
}

export function parseTwapMessage(message: Buffer): TwapMessage {
export function parseTwapMessage<T extends Uint8ArrayLike>(
message: T
): TwapMessage<T> {
let cursor = 0;
const variant = message.readUInt8(cursor);
const dataView = getDataView(message);
const variant = dataView.getUint8(cursor);
if (variant !== TWAP_MESSAGE_VARIANT) {
throw new Error("Not a twap message");
}
Expand All @@ -91,7 +99,7 @@ export function parseTwapMessage(message: Buffer): TwapMessage {
cursor += 16;
const numDownSlots = new BN(message.subarray(cursor, cursor + 8), "be");
cursor += 8;
const exponent = message.readInt32BE(cursor);
const exponent = dataView.getInt32(cursor);
cursor += 4;
const publishTime = new BN(message.subarray(cursor, cursor + 8), "be");
cursor += 8;
Expand All @@ -114,38 +122,39 @@ export function parseTwapMessage(message: Buffer): TwapMessage {
/**
* 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).
*/
export function sliceAccumulatorUpdateData(
data: Buffer,
export function sliceAccumulatorUpdateData<T extends Uint8ArrayLike>(
data: T,
start?: number,
end?: number
): Buffer {
): T {
if (!isAccumulatorUpdateData(data)) {
throw new Error("Invalid accumulator message");
}
let cursor = 6;
const trailingPayloadSize = data.readUint8(cursor);
const dataView = getDataView(data);
const trailingPayloadSize = dataView.getUint8(cursor);
cursor += 1 + trailingPayloadSize;

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

const vaaSize = data.readUint16BE(cursor);
const vaaSize = dataView.getUint16(cursor);
cursor += 2;
cursor += vaaSize;

const endOfVaa = cursor;

const updates = [];
const numUpdates = data.readUInt8(cursor);
const numUpdates = dataView.getUint8(cursor);
cursor += 1;

for (let i = 0; i < numUpdates; i++) {
const updateStart = cursor;
const messageSize = data.readUint16BE(cursor);
const messageSize = dataView.getUint16(cursor);
cursor += 2;
cursor += messageSize;

const numProofs = data.readUInt8(cursor);
const numProofs = dataView.getUint8(cursor);
cursor += 1;
cursor += KECCAK160_HASH_SIZE * numProofs;

Expand All @@ -157,44 +166,45 @@ export function sliceAccumulatorUpdateData(
}

const sliceUpdates = updates.slice(start, end);
return Buffer.concat([
return mergeUint8ArrayLikes([
data.subarray(0, endOfVaa),
Buffer.from([sliceUpdates.length]),
fromAsTypeOf(data, [sliceUpdates.length]),
...updates.slice(start, end),
]);
}

export function parseAccumulatorUpdateData(
data: Buffer
): AccumulatorUpdateData {
export function parseAccumulatorUpdateData<T extends Uint8ArrayLike>(
data: T
): AccumulatorUpdateData<T> {
if (!isAccumulatorUpdateData(data)) {
throw new Error("Invalid accumulator message");
}

let cursor = 6;
const trailingPayloadSize = data.readUint8(cursor);
const dataView = getDataView(data);
const trailingPayloadSize = dataView.getUint8(cursor);
cursor += 1 + trailingPayloadSize;

// const proofType = data.readUint8(cursor);
// const proofType = data.getUint8(cursor);
cursor += 1;

const vaaSize = data.readUint16BE(cursor);
const vaaSize = dataView.getUint16(cursor);
cursor += 2;

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

const numUpdates = data.readUInt8(cursor);
const numUpdates = dataView.getUint8(cursor);
const updates = [];
cursor += 1;

for (let i = 0; i < numUpdates; i++) {
const messageSize = data.readUint16BE(cursor);
const messageSize = dataView.getUint16(cursor);
cursor += 2;
const message = data.subarray(cursor, cursor + messageSize);
cursor += messageSize;

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

return { vaa, updates };
}

function mergeUint8ArrayLikes<T extends Uint8ArrayLike>(
inputs: [T, ...T[]]
): T {
const out = createAsTypeOf(
inputs[0],
inputs.reduce((acc, arr) => acc + arr.length, 0)
);
let offset = 0;
for (const arr of inputs) {
out.set(arr, offset);
offset += arr.length;
}
return out;
}

function toHex(input: Uint8ArrayLike): string {
return Array.from(input)
.map((value) => value.toString(16).padStart(2, "0"))
.join("");
}

// With Uint8Arrays, we could just do `new DataView(buf.buffer)`. But to
// account for `Buffers`, we need to slice to the used space since `Buffers` may
// be allocated to be larger than needed.
function getDataView(buf: Uint8ArrayLike) {
return new DataView(
buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)
);
}

// This is a little bit of a typescript hack -- we know that `Buffer.from` and
// `Uint8Array.from` behave effectively the same and we just want a `from` which
// will return either a `Uint8Array` or a `Buffer`, depending on the type of
// some other variable. But typescript sucks at typechecking prototypes so
// there's really no other good way I'm aware of to do this besides a bit of
// typecasting through `any`.
function fromAsTypeOf<T extends Uint8ArrayLike>(
buf: T,
...args: Parameters<typeof Uint8Array.from>
) {
return Object.getPrototypeOf(buf.constructor).from(...args) as T;
}

// Similar to `fromAsTypeOf`, here we want to be able to create either a
// `Buffer` or a `Uint8Array`, matching the type of a passed in value. But this
// is a bit more complex, because for `Uint8Array` we should do that with the
// `Uint8Array` constructor, where for `Buffer` we should do that using
// `Buffer.from`. This is a bit of a weird hack but I don't know a better way
// to make typescript handle such cases.
function createAsTypeOf<T extends Uint8ArrayLike>(buf: T, size: number) {
const ctor = buf.constructor;
const create = ("alloc" in ctor ? ctor.alloc : ctor) as (size: number) => T;
return create(size);
}

interface Uint8ArrayLike extends Uint8Array {
subarray: (from: number, to: number) => this;
}

0 comments on commit d46cac4

Please sign in to comment.