3

We have a struct like:

#[account(zero_copy)]
#[repr(C)]
pub struct SomeStruct {
    pub authority: Pubkey,
    pub some_value: u128,
    pub another_value: u64,
// etc...
}

This struct has alignment 16 because of the use of u128, which causes issues with some platforms, such as the Rust-based anchor client. We have lots of problems with alignment 16 generally, while 8 is universal. We can fix this by using:

#[account(zero_copy)]
#[repr(C)]
pub struct SomeStruct {
    pub authority: Pubkey,
    pub some_value: [u8; 16],
    pub another_value: u64,
// etc...
}
impl SomeStruct {
    pub fn some_value_u128(&self) -> u128 {
        bytemuck::cast_slice(&self.some_value)[0]
    }
}

Now it aligns 8, which is compatible with everything, and we can still update the value with some_struct.some_value = new_value.to_ne_bytes(); Great. The only problem is that the idl now generates an array like:

  "type": {
    "array": {
      "len": 16,
      "type": "u8"
    }
  }

i.e. a number[], instead of a BN. We can easily make it back into a BN with: new anchor.BN(someStruct.someValue, undefined, "le"),, so it is easily convertible, but this is annoying for anyone integrating on top of our idl.

How can we force the IDL to treat the [u8; 16] slice as a BN for deserialization purposes, so it can accessed as a BN like let a: BN = someStruct.someValue on the TS side without the additional conversion? Ultimately it is a BN, so this should be possible.

Manually editing the IDL to make the type a BN just returns the raw bytes.

2
  • This sounds like a general Rust question most appropriately asked in a Rust language help forum. The Rust Discord is very helpful for questions like these. Commented Sep 27, 2023 at 21:11
  • The Rust side of this is fine, using the u8 slice allows you to keep alignment 8. What we need is a way to have this slice to serialize and deserialize as a simple u128 BN when Anchor creates the idl.
    – Whiteseal
    Commented Sep 28, 2023 at 18:01

1 Answer 1

1

If [u8; 16] works for you, then you can trick Anchor into treating it as a u128 by creating a custom type with the same name, "u128". To not pollute the outer scope, you declare it in a module. Like so:

mod custom_scope {

    use anchor_lang::prelude::*;

    #[derive(Copy, Clone, bytemuck::Zeroable, bytemuck::Pod, Debug)]
    #[repr(C)]
    pub struct u128(pub [u8; 16]);

    impl u128 {
        pub fn as_u128(&self) -> core::primitive::u128 {
            core::primitive::u128::from_le_bytes(self.0)
        }
    }

    #[account(zero_copy)]
    #[repr(C)]
    pub struct SomeStruct {
        pub authority: Pubkey,
        pub some_value: u128, // anchor will think this `u128` is a primitive and not a custom type
        pub data: u64,
    }
}

It is key to also declare SomeStruct in the same scope so that we can do pub some_value: u128 because this is what tricks Anchor into treating it as a regular u128 and not a custom type.

And now, when your client sends data as u128 you can simply transform it into the type the account expects, with custom_scope::u128(some_value.to_le_bytes()).

Or if you want your my_account.some_value to act as a u128, you can simply do my_account.some_value.as_u128().

Here's the demo: https://beta.solpg.io/65184223fb53fa325bfd0bc9

Check the test file and notice it treats the someValue field in the account as BN without additional conversion. This works because in the account, the byte layout of custom_scope::u128 and u128 is the same so the client can treat it as a simple u128.

And in the IDL that the above link generates during build, it will show

{
    "name": "someValue",
    "type": "u128"
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.