Skip to content

Commit 014a940

Browse files
Support unit structs as contract events (#1917)
### What The contractevent attribute macro now accepts fieldless unit structs such as `pub struct MyEvent;`, treating them as events that carry no topic or data fields and emit only the default prefix topic derived from the struct name. ### Why Issue #1571 reports that the macro only worked on structs with named fields and rejected unit structs with a hard compile error, which prevented defining simple event markers that carry no additional data. Empty braced structs (`pub struct MyEvent {}`) already worked, so the gap was specifically the unit-struct form. ### Known limitations When a contract's spec is regenerated via contractimport!, a fieldless event is rendered as an empty braced struct (`pub struct MyEvent {}`) rather than a unit struct, because the on-wasm spec records only that the event has no parameters and does not preserve the unit-versus-braced distinction. The generated type produces an identical on-chain event so that difference is of little consequence. Close #1571 --- _Generated by [Claude Code](https://claude.ai/code/session_011mKQ41rU7Ze9hxQAUEdfmk)_ --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent ac9e84a commit 014a940

14 files changed

Lines changed: 917 additions & 13 deletions

File tree

soroban-sdk-macros/src/derive_event.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,7 @@ fn derive_impls(args: &ContractEventArgs, input: &DeriveInput) -> Result<TokenSt
114114
"structs with unnamed fields are not supported as contract events",
115115
)
116116
.with_span(&struct_.fields.span()))?,
117-
Fields::Unit => Err(Error::custom(
118-
"structs with no fields are not supported as contract events",
119-
)
120-
.with_span(&struct_.fields.span()))?,
117+
Fields::Unit => Vec::new(),
121118
},
122119
Data::Enum(_) => Err(Error::custom("enums are not supported as contract events")
123120
.with_span(&input.span()))?,

soroban-sdk/src/tests/contract_event.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,36 @@ fn test_no_topics_no_data() {
213213
assert_eq!(event.to_xdr(&env, &id), expected_event);
214214
}
215215

216+
#[test]
217+
fn test_unit_struct() {
218+
let env = Env::default();
219+
220+
#[contract]
221+
pub struct Contract;
222+
let id = env.register(Contract, ());
223+
224+
#[contractevent]
225+
pub struct MyEvent;
226+
227+
let event = MyEvent;
228+
env.as_contract(&id, || {
229+
event.publish(&env);
230+
});
231+
232+
let data: Val = Map::<Symbol, Val>::new(&env).into_val(&env);
233+
let expected_event = xdr::ContractEvent {
234+
ext: xdr::ExtensionPoint::V0,
235+
type_: xdr::ContractEventType::Contract,
236+
contract_id: Some(id.contract_id()),
237+
body: xdr::ContractEventBody::V0(xdr::ContractEventV0 {
238+
topics: vec![&env, symbol_short!("my_event")].into(),
239+
data: xdr::ScVal::try_from_val(&env, &data).unwrap(),
240+
}),
241+
};
242+
assert_eq!(env.events().all(), std::vec![expected_event.clone()]);
243+
assert_eq!(event.to_xdr(&env, &id), expected_event);
244+
}
245+
216246
#[test]
217247
fn test_data_single_value() {
218248
let env = Env::default();
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
{
2+
"generators": {
3+
"address": 1,
4+
"nonce": 0,
5+
"mux_id": 0
6+
},
7+
"auth": [
8+
[],
9+
[]
10+
],
11+
"ledger": {
12+
"protocol_version": 27,
13+
"sequence_number": 0,
14+
"timestamp": 0,
15+
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
16+
"base_reserve": 0,
17+
"min_persistent_entry_ttl": 4096,
18+
"min_temp_entry_ttl": 16,
19+
"max_entry_ttl": 6312000,
20+
"ledger_entries": [
21+
{
22+
"entry": {
23+
"last_modified_ledger_seq": 0,
24+
"data": {
25+
"contract_data": {
26+
"ext": "v0",
27+
"contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
28+
"key": "ledger_key_contract_instance",
29+
"durability": "persistent",
30+
"val": {
31+
"contract_instance": {
32+
"executable": {
33+
"wasm": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
34+
},
35+
"storage": null
36+
}
37+
}
38+
}
39+
},
40+
"ext": "v0"
41+
},
42+
"live_until": 4095
43+
},
44+
{
45+
"entry": {
46+
"last_modified_ledger_seq": 0,
47+
"data": {
48+
"contract_code": {
49+
"ext": "v0",
50+
"hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
51+
"code": ""
52+
}
53+
},
54+
"ext": "v0"
55+
},
56+
"live_until": 4095
57+
}
58+
]
59+
},
60+
"events": [
61+
{
62+
"event": {
63+
"ext": "v0",
64+
"contract_id": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM",
65+
"type_": "contract",
66+
"body": {
67+
"v0": {
68+
"topics": [
69+
{
70+
"symbol": "my_event"
71+
}
72+
],
73+
"data": {
74+
"map": []
75+
}
76+
}
77+
}
78+
},
79+
"failed_call": false
80+
}
81+
]
82+
}

tests-expanded/test_spec_import_tests.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ extern crate core;
55
#[prelude_import]
66
use core::prelude::rust_2021::*;
77
use soroban_sdk::{contract, contractimpl, Address, Env, String};
8-
use test_spec_lib::{EnumA, EnumIntA, ErrorA, EventA, StructA, StructTupleA};
8+
use test_spec_lib::{EnumA, EnumIntA, ErrorA, EventA, EventD, StructA, StructTupleA};
99
pub struct Contract;
1010
///ContractArgs is a type for building arg lists for functions defined in "Contract".
1111
pub struct ContractArgs;
@@ -48,6 +48,9 @@ impl Contract {
4848
pub fn fn_event_a(env: Env, f1: Address, f2: String) {
4949
EventA { f1, f2 }.publish(&env);
5050
}
51+
pub fn fn_event_d(env: Env) {
52+
EventD.publish(&env);
53+
}
5154
}
5255
#[doc(hidden)]
5356
#[allow(non_snake_case)]
@@ -135,6 +138,20 @@ impl Contract {
135138
*b"\0\0\0\0\0\0\0\0\0\0\0\nfn_event_a\0\0\0\0\0\x02\0\0\0\0\0\0\0\x02f1\0\0\0\0\0\x13\0\0\0\0\0\0\0\x02f2\0\0\0\0\0\x10\0\0\0\0"
136139
}
137140
}
141+
#[doc(hidden)]
142+
#[allow(non_snake_case)]
143+
pub mod __Contract__fn_event_d__spec {
144+
#[doc(hidden)]
145+
#[allow(non_snake_case)]
146+
#[allow(non_upper_case_globals)]
147+
pub static __SPEC_XDR_FN_FN_EVENT_D: [u8; 32usize] = super::Contract::spec_xdr_fn_event_d();
148+
}
149+
impl Contract {
150+
#[allow(non_snake_case)]
151+
pub const fn spec_xdr_fn_event_d() -> [u8; 32usize] {
152+
*b"\0\0\0\0\0\0\0\0\0\0\0\nfn_event_d\0\0\0\0\0\0\0\0\0\0"
153+
}
154+
}
138155
impl<'a> ContractClient<'a> {
139156
pub fn fn_struct_a(&self, f1: &u32, f2: &bool) -> StructA {
140157
use core::ops::Not;
@@ -325,6 +342,30 @@ impl<'a> ContractClient<'a> {
325342
);
326343
res
327344
}
345+
pub fn fn_event_d(&self) -> () {
346+
use core::ops::Not;
347+
use soroban_sdk::{FromVal, IntoVal};
348+
let res = self.env.invoke_contract(
349+
&self.address,
350+
&{ soroban_sdk::Symbol::new(&self.env, "fn_event_d") },
351+
::soroban_sdk::Vec::new(&self.env),
352+
);
353+
res
354+
}
355+
pub fn try_fn_event_d(
356+
&self,
357+
) -> Result<
358+
Result<(), <() as soroban_sdk::TryFromVal<soroban_sdk::Env, soroban_sdk::Val>>::Error>,
359+
Result<soroban_sdk::Error, soroban_sdk::InvokeError>,
360+
> {
361+
use soroban_sdk::{FromVal, IntoVal};
362+
let res = self.env.try_invoke_contract(
363+
&self.address,
364+
&{ soroban_sdk::Symbol::new(&self.env, "fn_event_d") },
365+
::soroban_sdk::Vec::new(&self.env),
366+
);
367+
res
368+
}
328369
}
329370
impl ContractArgs {
330371
#[inline(always)]
@@ -357,6 +398,11 @@ impl ContractArgs {
357398
pub fn fn_event_a<'i>(f1: &'i Address, f2: &'i String) -> (&'i Address, &'i String) {
358399
(f1, f2)
359400
}
401+
#[inline(always)]
402+
#[allow(clippy::unused_unit)]
403+
pub fn fn_event_d<'i>() -> () {
404+
()
405+
}
360406
}
361407
#[doc(hidden)]
362408
#[allow(non_snake_case)]
@@ -527,6 +573,23 @@ pub extern "C" fn __Contract__fn_event_a__invoke_raw_extern(
527573
#[allow(deprecated)]
528574
__Contract__fn_event_a__invoke_raw(soroban_sdk::Env::default(), arg_0, arg_1)
529575
}
576+
#[doc(hidden)]
577+
#[allow(non_snake_case)]
578+
#[deprecated(note = "use `ContractClient::new(&env, &contract_id).fn_event_d` instead")]
579+
#[allow(deprecated)]
580+
pub fn __Contract__fn_event_d__invoke_raw(env: soroban_sdk::Env) -> soroban_sdk::Val {
581+
soroban_sdk::IntoValForContractFn::into_val_for_contract_fn(
582+
<Contract>::fn_event_d(env.clone()),
583+
&env,
584+
)
585+
}
586+
#[doc(hidden)]
587+
#[allow(non_snake_case)]
588+
#[deprecated(note = "use `ContractClient::new(&env, &contract_id).fn_event_d` instead")]
589+
pub extern "C" fn __Contract__fn_event_d__invoke_raw_extern() -> soroban_sdk::Val {
590+
#[allow(deprecated)]
591+
__Contract__fn_event_d__invoke_raw(soroban_sdk::Env::default())
592+
}
530593
#[rustc_main]
531594
#[coverage(off)]
532595
#[doc(hidden)]

tests-expanded/test_spec_import_wasm32v1-none.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ extern crate core;
55
#[prelude_import]
66
use core::prelude::rust_2021::*;
77
use soroban_sdk::{contract, contractimpl, Address, Env, String};
8-
use test_spec_lib::{EnumA, EnumIntA, ErrorA, EventA, StructA, StructTupleA};
8+
use test_spec_lib::{EnumA, EnumIntA, ErrorA, EventA, EventD, StructA, StructTupleA};
99
pub struct Contract;
1010
///ContractArgs is a type for building arg lists for functions defined in "Contract".
1111
pub struct ContractArgs;
@@ -48,6 +48,9 @@ impl Contract {
4848
pub fn fn_event_a(env: Env, f1: Address, f2: String) {
4949
EventA { f1, f2 }.publish(&env);
5050
}
51+
pub fn fn_event_d(env: Env) {
52+
EventD.publish(&env);
53+
}
5154
}
5255
#[doc(hidden)]
5356
#[allow(non_snake_case)]
@@ -141,6 +144,21 @@ impl Contract {
141144
*b"\0\0\0\0\0\0\0\0\0\0\0\nfn_event_a\0\0\0\0\0\x02\0\0\0\0\0\0\0\x02f1\0\0\0\0\0\x13\0\0\0\0\0\0\0\x02f2\0\0\0\0\0\x10\0\0\0\0"
142145
}
143146
}
147+
#[doc(hidden)]
148+
#[allow(non_snake_case)]
149+
pub mod __Contract__fn_event_d__spec {
150+
#[doc(hidden)]
151+
#[allow(non_snake_case)]
152+
#[allow(non_upper_case_globals)]
153+
#[link_section = "contractspecv0"]
154+
pub static __SPEC_XDR_FN_FN_EVENT_D: [u8; 32usize] = super::Contract::spec_xdr_fn_event_d();
155+
}
156+
impl Contract {
157+
#[allow(non_snake_case)]
158+
pub const fn spec_xdr_fn_event_d() -> [u8; 32usize] {
159+
*b"\0\0\0\0\0\0\0\0\0\0\0\nfn_event_d\0\0\0\0\0\0\0\0\0\0"
160+
}
161+
}
144162
impl<'a> ContractClient<'a> {
145163
pub fn fn_struct_a(&self, f1: &u32, f2: &bool) -> StructA {
146164
use core::ops::Not;
@@ -331,6 +349,30 @@ impl<'a> ContractClient<'a> {
331349
);
332350
res
333351
}
352+
pub fn fn_event_d(&self) -> () {
353+
use core::ops::Not;
354+
use soroban_sdk::{FromVal, IntoVal};
355+
let res = self.env.invoke_contract(
356+
&self.address,
357+
&{ soroban_sdk::Symbol::new(&self.env, "fn_event_d") },
358+
::soroban_sdk::Vec::new(&self.env),
359+
);
360+
res
361+
}
362+
pub fn try_fn_event_d(
363+
&self,
364+
) -> Result<
365+
Result<(), <() as soroban_sdk::TryFromVal<soroban_sdk::Env, soroban_sdk::Val>>::Error>,
366+
Result<soroban_sdk::Error, soroban_sdk::InvokeError>,
367+
> {
368+
use soroban_sdk::{FromVal, IntoVal};
369+
let res = self.env.try_invoke_contract(
370+
&self.address,
371+
&{ soroban_sdk::Symbol::new(&self.env, "fn_event_d") },
372+
::soroban_sdk::Vec::new(&self.env),
373+
);
374+
res
375+
}
334376
}
335377
impl ContractArgs {
336378
#[inline(always)]
@@ -363,6 +405,11 @@ impl ContractArgs {
363405
pub fn fn_event_a<'i>(f1: &'i Address, f2: &'i String) -> (&'i Address, &'i String) {
364406
(f1, f2)
365407
}
408+
#[inline(always)]
409+
#[allow(clippy::unused_unit)]
410+
pub fn fn_event_d<'i>() -> () {
411+
()
412+
}
366413
}
367414
#[doc(hidden)]
368415
#[allow(non_snake_case)]
@@ -539,3 +586,21 @@ pub extern "C" fn __Contract__fn_event_a__invoke_raw_extern(
539586
#[allow(deprecated)]
540587
__Contract__fn_event_a__invoke_raw(soroban_sdk::Env::default(), arg_0, arg_1)
541588
}
589+
#[doc(hidden)]
590+
#[allow(non_snake_case)]
591+
#[deprecated(note = "use `ContractClient::new(&env, &contract_id).fn_event_d` instead")]
592+
#[allow(deprecated)]
593+
pub fn __Contract__fn_event_d__invoke_raw(env: soroban_sdk::Env) -> soroban_sdk::Val {
594+
soroban_sdk::IntoValForContractFn::into_val_for_contract_fn(
595+
<Contract>::fn_event_d(env.clone()),
596+
&env,
597+
)
598+
}
599+
#[doc(hidden)]
600+
#[allow(non_snake_case)]
601+
#[deprecated(note = "use `ContractClient::new(&env, &contract_id).fn_event_d` instead")]
602+
#[export_name = "fn_event_d"]
603+
pub extern "C" fn __Contract__fn_event_d__invoke_raw_extern() -> soroban_sdk::Val {
604+
#[allow(deprecated)]
605+
__Contract__fn_event_d__invoke_raw(soroban_sdk::Env::default())
606+
}

0 commit comments

Comments
 (0)