Skip to content

Allow deriving storage traits for foreign types #1863

@athei

Description

@athei

In to place a type into contract storage we require it implement certain storage specific traits. Those are meant to be derived
and not implemented. This is a problem if a contract wants to store foreign types as it cannot use the derive macros on them.

After looking through or examples the affected traits seem to be:

  • scale::Decode
  • scale::Encode
  • scale_info::TypeInfo
  • ink::storage::traits::StorageLayout

The scale and scale info types are a bit more common but still parity specific and meant to be derived.

Please note that we don't know how common this case is and if we are willing to put in the work to support this for what might be a niche feature. Needs a discussion.

Suggested Solution

Presently, when deriving a type we require all contained types of a struct (or enum) to implement the type we are deriving in order to forward to them. In our case where the type is foreign we can't derive. The workaround is to newtype and manually add a forwarding impl. This is clearly not a good solution especially if the foreign type contains further foreign types. The boilerplate adds up.

To automate this we need to write a derive macro that derives the trait it is deriving on struct A for all types contained in A that don't already implement that trait. A recursive derive so to say:

//! Foreign crate

/// Doesn't implement any storage traits even though some contained types might.
#[derive(Clone, Display, Debug)]
struct StoreMe {
    all: crate::HasAllStorageTraits,
    onlyScale: crate::OnlyScaleTraits,
    nothing: crate::NoStorageTraits,
}

//! Contract

/// User will write this. Macro iterates over all contained types and emits a newtype + derive.
///
///    Specifies for which types to derive and for which embedded types an additional derive needs to be emitted.
///    This is needed for our storage types which are not implemented on the inner type.
///    This requires that the trait is actually deriveable for the type mentioned in `for`.
/// Please note that this looks so complicated because the example tries to cover all cases. With sensible
/// defaults we can probably make the common case pretty concise.
#[foreign_derive(
    [
        {traits = [StorageLayout, TypeInfo], for = [foreign::NoStorageTraits, foreign::OnlyScaleTraits]},
        {traits = [Encode, Decode], for = [foreign::NoStorageTraits]},
    ],
)
#[derive(Clone, Display, Debug)]
struct StoreMe(foreign::StoreMe);


// Macro will replace this with

/// Normal derive works because we emit derives for the contained types.
#[derive(StorageLayout, TypeInfo, Encode, Decode)]
struct StoreMe(foreign::StoreMe);

impl Deref for StoreMe {
    Target = foreign::StoreMe;
    ...
}

impl DerefMut for StoreMe {
    Target = foreign::StoreMe;
    ...
}

impl From<foreign::StoreMe> for StoreMe { .... }

impl From<StoreMe> for foreign::StoreMe { .... }

/// Without arguments just generate basic Deref and Into impls.
#[foreign_derive]
#[derive(StorageLayout, TypeInfo)]
struct OnlyScaleTraits(foreign::OnlyScaleTraits)
#[foreign_derive]
#[derive(StorageLayout, TypeInfo, Encode, Decode)]
struct NoStorageTraits(foreign::NoStorageTraits)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ink_lang[ink_lang] Work itemA-ink_storage[ink_storage] Work ItemC-discussionAn issue for discussion for a given topic.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions