Is there any way to store the bytes of a struct in storage?

I have this struct in sway:

pub struct TestStructMetadataBytes {
    val1: b256,
    val2: b256,
    // metadata_keys: Vec<(String, String)>,
    metadata_keys: Vec<(raw_slice, raw_slice)>,
}

impl TestStructMetadataBytes {
    pub fn new(val1: b256, val2: b256) -> Self {
        Self {
            val1,
            val2,
            metadata_keys: Vec::new(),
        }
    }

    pub fn from_bytes(bytes: Bytes) -> Self {
        let mut bytes_alloc = Bytes::with_capacity(size_of::<TestStructMetadataBytes>());
        bytes.buf.ptr.copy_bytes_to(bytes_alloc.buf.ptr, bytes.len);
        return bytes_alloc.buf.ptr.read::<TestStructMetadataBytes>();
    }

    pub fn to_bytes(self) -> Bytes {
        let foo_len = size_of_val(self);
        let foo_ptr = __addr_of(self);
        let buf_len = foo_len / size_of::<u8>();
        let foo_buf = raw_slice::from_parts::<u8>(foo_ptr, buf_len);
        return Bytes::from(foo_buf);
    }

    pub fn has_metadata_key(self, key: String) -> bool {
        let mut count: u64 = 0;
        while count < self.metadata_keys.len() {
            let (metadata_key, _) = self.metadata_keys.get(count).unwrap();

            if String::from(metadata_key) == key {
                return true;
            }

            count = count + 1;
        }
        return false;
    }

    pub fn add_metadata(ref mut self, metadata: MetadataValue) {
        self.metadata_keys.push((metadata.key.as_raw_slice(), metadata.value.as_raw_slice()));
    }

    pub fn get_metadata(self, key: String) -> Option<MetadataValue> {
        let mut count: u64 = 0;
        while count < self.metadata_keys.len() {
            let (metadata_key, metadata_value) = self.metadata_keys.get(count).unwrap();
            
            if String::from(metadata_key) == key {
                return Option::Some(MetadataValue {
                    key: String::from(metadata_key),
                    value: String::from(metadata_value),
                });
            }

            count = count + 1;
        }
        return Option::None;
    }
}

When I convert the struct to bytes and store it in storage, I can access the values of val1 and val2, but the metadata_keys comes with 1 item in the array and the Strings are empty.

abi MetadataStorage {
    #[storage(read, write)]
    fn save(name: String, metadata: MetadataValue);

    #[storage(read)]
    fn get(name: String, key_metadata: String) -> Option<MetadataValue>;

    #[storage(read, write)]
    fn save_handle(name: String);
}

storage {
    handles: StorageMap<b256, StorageBytes> = StorageMap {},
}

impl MetadataStorage for Contract {
    #[storage(read, write)]
    fn save_handle(name: String) {
        let key = sha256(name);
        let mut domain = TestStructMetadataBytes::new(key, key);
        storage.handles.insert(key, StorageBytes {});
        storage.handles.get(key).write_slice(domain.to_bytes());
    }

    #[storage(read, write)]
    fn save(name: String, metadata: MetadataValue) {
        let key = sha256(name);
        let domain_bytes = storage.handles.get(key).read_slice().unwrap();
        let mut domain = TestStructMetadataBytes::from_bytes(domain_bytes);
        domain.add_metadata(metadata);
        storage.handles.get(key).write_slice(domain.to_bytes());
    }

    #[storage(read)]
    fn get(name: String, key_metadata: String) -> Option<MetadataValue> {
        let key = sha256(name);
        let domain_bytes = storage.handles.get(key).read_slice().unwrap();
        let mut domain = TestStructMetadataBytes::from_bytes(domain_bytes);

        return domain.get_metadata(key_metadata);
    }
}

In tests on sway works normally but executing the method get(name, key_metadata) in fuels-ts don’t return the value because metadata_keys has 1 item in Vec with empty Strings.

I tried change to Vec<Bytes, Bytes> and Vec<String, String) but not work.

Here the test in sway:

#[test]
fn test_contract() {
    let metadata_storage = abi(MetadataStorage, CONTRACT_ID);

    let name = String::from_ascii_str("github");
    let github_metadata = MetadataValue {
        key: String::from_ascii_str("github"),
        value: String::from_ascii_str("name"),
    };
    let linkedin_metadata = MetadataValue {
        key: String::from_ascii_str("linkedin"),
        value: String::from_ascii_str("name"),
    };

    metadata_storage.save_handle(name);
    metadata_storage.save(name, github_metadata);
    metadata_storage.save(name, linkedin_metadata);

    let github_metadata1 = metadata_storage.get(name, github_metadata.key);
    let linkedin_metadata1 = metadata_storage.get(name, linkedin_metadata.key);
    assert(github_metadata1.unwrap().value == github_metadata.value);
    assert(linkedin_metadata1.unwrap().value == linkedin_metadata.value);
}

The same test In ts:

const provider = await Provider.create('http://localhost:4000/graphql');
const walletUnlocked = Wallet.fromPrivateKey('PRIVATE_KEY', provider);

const metadataStorageAbi = MetadataStorageAbi__factory.connect(metadataStorage, walletUnlocked);

const name = 'github';
const txParams = {gasPrice: 1, gasLimit: 1_000_000};
const github_metadata = {
    key: 'github',
    value: 'name',
};

await metadataStorageAbi.functions.save_handle(name).txParams(txParams).call();
await metadataStorageAbi.functions.save(name, github_metadata).txParams(txParams).call();
const result = await metadataStorageAbi.functions.get(name, github_metadata.key).txParams(txParams).call();

console.log(result.value) // undefined

Is there any way to work around this issue or any other solution?

It’s important to note that Vec<T> is a heap type. This means that it contains a pointer to the elements in the vector.

Then each raw_slice of Vec<(raw_slice, raw_slice)> contains its own pointer to its elements as well. The problem here is not that you have bytes in a struct, but that these are not stored sequentially in memory.

Storage will soon be reworked to make this a lot easier, but for now your to_bytes() should look something like this:

    pub fn to_bytes(self) -> Bytes {
        // Get the length of all elements, note we need to account for the length of each raw_slice
        let val1_len = size_of_val(self.val1);
        let val2_len = size_of_val(self.val2);

        let mut metadata_keys_len = 0;
        let mut i = 0;
        for i < self. metadata_keys.len() {
             let slice_tuple = self.metadata_key.get(i);
             metadata_keys_len +=  slice_tuple.0.number_of_bytes();
             metadata_keys_len +=  slice_tuple.1.number_of_bytes();
             i += 1;
        }

        // Now create the ptr which will contain all of Self's elements
        let ptr = alloc_bytes(val1_len + val2_len + metadata_keys_len);

        // Copy val1 to the new pointer
        ptr.write(self.val1);

        // Copy val2 to the new pointer, offset by 1 b256
        ptr.add<b256>(1).write(self.val2);

        // Copy each slice to the new pointer, starting with an offset of 2 b256
        let slice_start_ptr = ptr.add<b256>(2);
        let mut i = 0;
        let mut offset = 0;
        for i < self.metadata_keys.len() {
             let slice_tuple = self.metadata_key.get(i);

             // Copy slice 1 data
             let slice_0_num_bytes = slice_tuple.0.number_of_bytes();
             slice_tuple.0.ptr()
                   .copy_bytes_to(slice_start_ptr.add_uint_offset(offset), slice_0_num_bytes);
             offset += slice_0_num_bytes;

             // Copy slice 2 data
             let slice_1_num_bytes = slice_tuple.1.number_of_bytes();
             slice_tuple.1.ptr()
                   .copy_bytes_to(slice_start_ptr.add_uint_offset(offset), slice_1_num_bytes);
             offset += slice_1_num_bytes;

             // Increment the offset by 1 so we start at a fresh index on the next loop
             offset += 1;
             i += 1;
        }

        // Create the Bytes from a raw_slice
        let slice = raw_slice::from_parts::<u8>(ptr,  val1_len + val2_len + metadata_keys_len);
        return Bytes::from(slice);
    }
2 Likes

Perfect!
So, there wouldn’t be a problem if my type of metadata_keys was Vec<(String, String)>?