The new pvm-contract-sdk ABI traits/macros do not currently support Rust
Option<T> as a contract method input or return type.
T itself can implement SolEncode / SolDecode, including dynamically
encoded structs when the contract uses an allocator, but Option<T> does not
appear to have corresponding ABI support. The result is that method dispatch
cannot encode returned Option<T> values, and imported callers cannot decode
them.
Observed on branch sm/cdm at 059bb9ee.
Current behavior
There are no SolEncode, SolDecode, StaticEncodedLen, or StaticDecode
implementations for Option<T> in pvm-contract-types.
That means this kind of contract surface does not compile:
#![cfg_attr(not(feature = "abi-gen"), no_main, no_std)]
extern crate alloc;
use alloc::string::String;
#[derive(Default, pvm_contract_sdk::SolType)]
pub struct Profile {
pub id: u64,
pub metadata_uri: String,
}
#[pvm_contract_sdk::contract(allocator = "bump")]
mod example {
use super::Profile;
use alloc::string::String;
pub struct Example;
impl Example {
#[pvm_contract_sdk::constructor]
pub fn new(&mut self) {}
#[pvm_contract_sdk::method]
pub fn maybe_id(&self, exists: bool) -> Option<u64> {
if exists { Some(1) } else { None }
}
#[pvm_contract_sdk::method]
pub fn maybe_profile(&self, exists: bool) -> Option<Profile> {
if exists {
Some(Profile {
id: 1,
metadata_uri: String::from("ipfs://example"),
})
} else {
None
}
}
}
}
The expected failure mode is a trait bound error along the lines of
Option<u64>: SolEncode / Option<Profile>: SolEncode not being satisfied.
The same issue applies on the caller side for imported ABIs, where return
types need SolDecode.
Why this matters
Several CDM contracts expose lookup APIs where absence is part of the contract:
pub fn get_profile_info(context_id: ContextId, profile_id: EntityId) -> Option<Profile>
pub fn get_profile_at(context_id: ContextId, owner: Address, index: u32) -> Option<EntityId>
pub fn get_post(context_id: ContextId, post_id: EntityId) -> Option<Post>
pub fn get_parent_at(context_id: ContextId, parent: EntityId, index: u32) -> Option<EntityId>
Some of these are static values (Option<EntityId>), and some are dynamic
values (Option<Profile>, Option<Post>) where the inner type includes
String or Vec<T>. Dynamic returns themselves are supported with an
allocator; the missing piece is the optional wrapper.
The CDM registry-style API also naturally wants this shape:
pub fn get_address(contract_name: String) -> Option<Address>
Returning a sentinel value such as zero address or zero id is less precise and
pushes absence semantics into every downstream caller.
Requested behavior
Please add an official ABI convention for Option<T> and implement it across:
SolEncode for Option<T>
SolDecode for Option<T>
StaticEncodedLen / StaticDecode where T is statically encoded
- contract method dispatch return encoding
- method argument decoding
abi_import! generated caller bindings
- ABI JSON generation, if applicable
A reasonable convention would be to encode Option<T> as a tuple:
Under this convention:
Some(value) encodes as (true, value).
None encodes as (false, default_or_zero_value_for_T).
- Decoding may ignore
value when is_some == false, while still requiring
well-formed ABI bytes.
Option<T> is static when T is static, with encoded size
32 + T::ENCODED_SIZE.
Option<T> is dynamic when T is dynamic.
If the SDK should not choose a default/zero value for None, another viable
design would be to require T: Default for SolEncode<Option<T>>, or to add a
dedicated trait for ABI-default values. The important part is that the SDK
should define one convention so contract authors and generated callers agree.
Expected result
Contracts should be able to write:
#[pvm_contract_sdk::method]
pub fn get_profile(&self, id: u64) -> Option<Profile> {
// ...
}
and imported callers should be able to call the generated binding as:
let maybe_profile: Option<Profile> = profiles
.get_profile(id)
.call(self)?;
without each contract having to define custom MaybeProfile,
MaybeAddress, MaybeEntityId, etc. wrapper structs by hand.
Workaround today
Contracts can manually define wrapper structs:
#[derive(Default, pvm_contract_sdk::SolType)]
pub struct MaybeProfile {
pub exists: bool,
pub value: Profile,
}
This compiles if Profile itself is ABI-compatible, but it loses the natural
Rust API shape, creates one wrapper per optional type, and forces every caller
to remember the same ad hoc convention.
The new
pvm-contract-sdkABI traits/macros do not currently support RustOption<T>as a contract method input or return type.Titself can implementSolEncode/SolDecode, including dynamicallyencoded structs when the contract uses an allocator, but
Option<T>does notappear to have corresponding ABI support. The result is that method dispatch
cannot encode returned
Option<T>values, and imported callers cannot decodethem.
Observed on branch
sm/cdmat059bb9ee.Current behavior
There are no
SolEncode,SolDecode,StaticEncodedLen, orStaticDecodeimplementations for
Option<T>inpvm-contract-types.That means this kind of contract surface does not compile:
The expected failure mode is a trait bound error along the lines of
Option<u64>: SolEncode/Option<Profile>: SolEncodenot being satisfied.The same issue applies on the caller side for imported ABIs, where return
types need
SolDecode.Why this matters
Several CDM contracts expose lookup APIs where absence is part of the contract:
Some of these are static values (
Option<EntityId>), and some are dynamicvalues (
Option<Profile>,Option<Post>) where the inner type includesStringorVec<T>. Dynamic returns themselves are supported with anallocator; the missing piece is the optional wrapper.
The CDM registry-style API also naturally wants this shape:
Returning a sentinel value such as zero address or zero id is less precise and
pushes absence semantics into every downstream caller.
Requested behavior
Please add an official ABI convention for
Option<T>and implement it across:SolEncodeforOption<T>SolDecodeforOption<T>StaticEncodedLen/StaticDecodewhereTis statically encodedabi_import!generated caller bindingsA reasonable convention would be to encode
Option<T>as a tuple:Under this convention:
Some(value)encodes as(true, value).Noneencodes as(false, default_or_zero_value_for_T).valuewhenis_some == false, while still requiringwell-formed ABI bytes.
Option<T>is static whenTis static, with encoded size32 + T::ENCODED_SIZE.Option<T>is dynamic whenTis dynamic.If the SDK should not choose a default/zero value for
None, another viabledesign would be to require
T: DefaultforSolEncode<Option<T>>, or to add adedicated trait for ABI-default values. The important part is that the SDK
should define one convention so contract authors and generated callers agree.
Expected result
Contracts should be able to write:
and imported callers should be able to call the generated binding as:
without each contract having to define custom
MaybeProfile,MaybeAddress,MaybeEntityId, etc. wrapper structs by hand.Workaround today
Contracts can manually define wrapper structs:
This compiles if
Profileitself is ABI-compatible, but it loses the naturalRust API shape, creates one wrapper per optional type, and forces every caller
to remember the same ad hoc convention.