coral-xyz / anchor

⚓ Solana Sealevel Framework
https://anchor-lang.com
Apache License 2.0
3.36k stars 1.25k forks source link

React Native Error: b.readUIntLE is not a function due to usage of `subarray` function in versions >= v0.29.0 #3041

Open Michaelsulistio opened 1 week ago

Michaelsulistio commented 1 week ago

Describe the bug

In React Native applications, when trying to use @coral-xyz/anchor >= v0.29.0, users will run into this error when using the library to fetch and decode certain accounts (i.e in program.account.accountName.fetch()):

TypeError: b.readUIntLE is not a function (it is undefined)

Other expected Buffer methods like readUInt32LE will fail too (have not tested myself).

Specifically, in my case, the stack trace is throwing an error in buffer-layout's UInt layout class decode method.

Root issue

The root of the issue stems from this subarray function call in decodeUnchecked.

public decodeUnchecked<T = any>(accountName: A, acc: Buffer): T {
  // In RN, `subarray` returns `data` as a Uint8Array, rather than a Buffer.
  const data = acc.subarray(DISCRIMINATOR_SIZE); 
  const layout = this.accountLayouts.get(accountName);
  if (!layout) {
    throw new Error(`Unknown account: ${accountName}`);
  }
  return layout.decode(data);
}

The returned data is a Uint8Array when it should be a Buffer. This means it is missing the function readUIntLE and this causes the error later in buffer-layout's decode method.

Explanation

acc is aBuffer that comes from @solana/web3,js which uses the buffer npm package.

In React Native runtime environment (Hermes) Buffer.subarray behaves differently than on Browser/Node environment. This is a known issue(1, 2). As a result, the subarray call incorrectly returns an instance of a Uint8Array rather than a Buffer.

For a full understanding, read this issue.

Solution

1. Locally patch the Buffer.subarray function

We can manually fix the subarray function by patching the Buffer.subarray prototype to explicitly return an object that has the expected Buffer methods (like readUIntLE).

In polyfills.ts:

import { Buffer } from "buffer";
global.Buffer = Buffer;

Buffer.prototype.subarray = function subarray(
  begin: number | undefined,
  end: number | undefined
) {
  const result = Uint8Array.prototype.subarray.apply(this, [begin, end]);
  Object.setPrototypeOf(result, Buffer.prototype); // Explicitly add the `Buffer` prototype (adds `readUIntLE`!)
  return result;
};

2. Patch @coral-xyz/anchor to use slice instead of subarray

Not an ideal because subarray should be more performant than slice.

acheroncrypto commented 1 week ago

Thank you for this comprehensive issue report!

In React Native applications, when trying to use @coral-xyz/anchor >= v0.29.0

Why is it specifically >= 0.29.0? The library has been using Buffer pretty much since the beginning, so this issue should exist on all versions.

  1. Locally patch the Buffer.subarray function

Not pretty but at least it's a short fix. Is there a published polyfill that does this?

2. Patch @coral-xyz/anchor to use slice instead of subarray

Not an ideal because subarray should be more performant than slice.

Not only that but also slice method is deprecated afaik.

Buffer compatibility has not only been an issue for React Native, but also browsers too. The best solution long term would be to completely get rid of it, but unfortunately, we still depend on packages that use it internally.

Michaelsulistio commented 1 week ago

Why is it specifically >= 0.29.0? The library has been using Buffer pretty much since the beginning, so this issue should exist on all versions.

The issue lies with the usage of subarray in decodeUnchecked, not just with Buffer. This is the commit that introduced subarray into decodeUnchecked.

So whichever version of the package includes that commit, is when it started becoming problematic for React Native. On second glance, seems like that commit began inclusion in some version of v0.28.X?

Let me know what is the starting version that includes that commit, and I can update the issue title for more accurate documentation.

Not pretty but at least it's a short fix. Is there a published polyfill that does this?

Not that I know of, most discussion online just encourages the project to manually polyfill. The @craftzdog/react-native-buffer package actually includes this polyfill into their implementation of Buffer but it wouldn't help in this case, because acc: Buffer is produced by @solana/web3.js which itself uses the problematic buffer package.

The best solution long term would be to completely get rid of it, but unfortunately, we still depend on packages that use it internally.

Yup, like mentioned above, this issue comes all the way from the Connection class from @solana/web3.js which itself depends on buffer.

For an interim solution, this issue/behavior should be properly documented and the fix I mentioned should be discoverable. Is there anywhere on the Anchor documentation we can add some warning/disclaimer for React Native behavior?