Skip to main content

near_primitives/action/
delegate.rs

1//! DelegateAction is a type of action to support meta transactions.
2//!
3//! NEP: <https://github.com/near/NEPs/pull/366>
4//! This is the module containing the types introduced for delegate actions.
5
6use super::Action;
7use crate::signable_message::{SignableMessage, SignableMessageType};
8use borsh::{BorshDeserialize, BorshSerialize};
9use near_crypto::{PublicKey, Signature, Signer};
10use near_primitives_core::hash::{CryptoHash, hash};
11use near_primitives_core::types::BlockHeight;
12use near_primitives_core::types::{AccountId, Nonce};
13use near_schema_checker_lib::ProtocolSchema;
14use serde::{Deserialize, Serialize};
15use std::io::{Error, ErrorKind, Read};
16
17/// This is an index number of Action::Delegate in Action enumeration
18const ACTION_DELEGATE_NUMBER: u8 = 8;
19/// This action allows to execute the inner actions behalf of the defined sender.
20#[derive(
21    BorshSerialize,
22    BorshDeserialize,
23    Serialize,
24    Deserialize,
25    PartialEq,
26    Eq,
27    Clone,
28    Debug,
29    ProtocolSchema,
30)]
31#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
32pub struct DelegateAction {
33    /// Signer of the delegated actions
34    pub sender_id: AccountId,
35    /// Receiver of the delegated actions.
36    pub receiver_id: AccountId,
37    /// List of actions to be executed.
38    ///
39    /// With the meta transactions MVP defined in NEP-366, nested
40    /// DelegateActions are not allowed. A separate type is used to enforce it.
41    pub actions: Vec<NonDelegateAction>,
42    /// Nonce to ensure that the same delegate action is not sent twice by a
43    /// relayer and should match for given account's `public_key`.
44    /// After this action is processed it will increment.
45    pub nonce: Nonce,
46    /// The maximal height of the block in the blockchain below which the given DelegateAction is valid.
47    pub max_block_height: BlockHeight,
48    /// Public key used to sign this delegated action.
49    pub public_key: PublicKey,
50}
51
52#[derive(
53    BorshSerialize,
54    BorshDeserialize,
55    Serialize,
56    Deserialize,
57    PartialEq,
58    Eq,
59    Clone,
60    Debug,
61    ProtocolSchema,
62)]
63#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
64pub struct SignedDelegateAction {
65    pub delegate_action: DelegateAction,
66    pub signature: Signature,
67}
68
69impl SignedDelegateAction {
70    pub fn verify(&self) -> bool {
71        let delegate_action = &self.delegate_action;
72        let hash = delegate_action.get_nep461_hash();
73        let public_key = &delegate_action.public_key;
74
75        self.signature.verify(hash.as_ref(), public_key)
76    }
77
78    pub fn sign(singer: &Signer, delegate_action: DelegateAction) -> Self {
79        let signature = singer.sign(delegate_action.get_nep461_hash().as_bytes());
80        Self { delegate_action, signature }
81    }
82}
83
84impl From<SignedDelegateAction> for Action {
85    fn from(delegate_action: SignedDelegateAction) -> Self {
86        Self::Delegate(Box::new(delegate_action))
87    }
88}
89
90impl DelegateAction {
91    pub fn get_actions(&self) -> Vec<Action> {
92        self.actions.iter().map(|a| a.clone().into()).collect()
93    }
94
95    /// Delegate action hash used for NEP-461 signature scheme which tags
96    /// different messages before hashing
97    ///
98    /// For more details, see: [NEP-461](https://github.com/near/NEPs/pull/461)
99    pub fn get_nep461_hash(&self) -> CryptoHash {
100        let signable = SignableMessage::new(&self, SignableMessageType::DelegateAction);
101        let bytes = borsh::to_vec(&signable).expect("Failed to deserialize");
102        hash(&bytes)
103    }
104}
105
106/// This is Action which mustn't contain DelegateAction.
107///
108/// This struct is needed to avoid the recursion when Action/DelegateAction is deserialized.
109///
110/// Important: Don't make the inner Action public, this must only be constructed
111/// through the correct interface that ensures the inner Action is actually not
112/// a delegate action. That would break an assumption of this type, which we use
113/// in several places. For example, borsh de-/serialization relies on it. If the
114/// invariant is broken, we may end up with a `Transaction` or `Receipt` that we
115/// can serialize but deserializing it back causes a parsing error.
116#[derive(Serialize, BorshSerialize, Deserialize, PartialEq, Eq, Clone, Debug, ProtocolSchema)]
117pub struct NonDelegateAction(Action);
118
119#[cfg(feature = "schemars")]
120impl schemars::JsonSchema for NonDelegateAction {
121    fn schema_name() -> std::borrow::Cow<'static, str> {
122        "NonDelegateAction".into()
123    }
124
125    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
126        // Get the actual Action schema by calling its json_schema method directly
127        // This gives us the full schema with the oneOf array, not just a reference
128        let mut action_schema = Action::json_schema(generator);
129
130        // Find and filter the oneOf array directly on the Schema object
131        if let Some(one_of) = action_schema.get_mut("oneOf") {
132            if let Some(arr) = one_of.as_array_mut() {
133                // Remove the Delegate variant
134                arr.retain(|variant| {
135                    !variant
136                        .get("properties")
137                        .and_then(|p| p.as_object())
138                        .map(|p| p.contains_key("Delegate"))
139                        .unwrap_or(false)
140                });
141            }
142        }
143
144        // Update description to be more client-friendly
145        action_schema.insert("description".to_string(), serde_json::json!(
146            "An Action that can be included in a transaction or receipt, excluding delegate actions. \
147            This type represents all possible action types except DelegateAction to prevent \
148            infinite recursion in meta-transactions."
149        ));
150
151        action_schema
152    }
153}
154
155/// A small private module to protect the private fields inside `NonDelegateAction`.
156mod private_non_delegate_action {
157    use super::*;
158
159    impl From<NonDelegateAction> for Action {
160        fn from(action: NonDelegateAction) -> Self {
161            action.0
162        }
163    }
164
165    #[derive(Debug, thiserror::Error)]
166    #[error("attempted to construct NonDelegateAction from Action::Delegate")]
167    pub struct IsDelegateAction;
168
169    impl TryFrom<Action> for NonDelegateAction {
170        type Error = IsDelegateAction;
171
172        fn try_from(action: Action) -> Result<Self, IsDelegateAction> {
173            if matches!(action, Action::Delegate(_)) {
174                Err(IsDelegateAction)
175            } else {
176                Ok(Self(action))
177            }
178        }
179    }
180
181    impl borsh::de::BorshDeserialize for NonDelegateAction {
182        fn deserialize_reader<R: Read>(rd: &mut R) -> ::core::result::Result<Self, Error> {
183            match u8::deserialize_reader(rd)? {
184                ACTION_DELEGATE_NUMBER => Err(Error::new(
185                    ErrorKind::InvalidInput,
186                    "DelegateAction mustn't contain a nested one",
187                )),
188                n => borsh::de::EnumExt::deserialize_variant(rd, n).map(Self),
189            }
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use crate::action::CreateAccountAction;
198    use near_crypto::KeyType;
199
200    /// A serialized `Action::Delegate(SignedDelegateAction)` for testing.
201    ///
202    /// We want this to be parsable and accepted by protocol versions with meta
203    /// transactions enabled. But it should fail either in parsing or in
204    /// validation when this is included in a receipt for a block of an earlier
205    /// version. For now, it just fails to parse, as a test below checks.
206    const DELEGATE_ACTION_HEX: &str = concat!(
207        "0803000000616161030000006262620100000000010000000000000002000000000000",
208        "0000000000000000000000000000000000000000000000000000000000000000000000",
209        "0000000000000000000000000000000000000000000000000000000000000000000000",
210        "0000000000000000000000000000000000000000000000000000000000"
211    );
212
213    fn create_delegate_action(actions: Vec<Action>) -> Action {
214        Action::Delegate(Box::new(SignedDelegateAction {
215            delegate_action: DelegateAction {
216                sender_id: "aaa".parse().unwrap(),
217                receiver_id: "bbb".parse().unwrap(),
218                actions: actions
219                    .iter()
220                    .map(|a| NonDelegateAction::try_from(a.clone()).unwrap())
221                    .collect(),
222                nonce: 1,
223                max_block_height: 2,
224                public_key: PublicKey::empty(KeyType::ED25519),
225            },
226            signature: Signature::empty(KeyType::ED25519),
227        }))
228    }
229
230    #[test]
231    fn test_delegate_action_deserialization() {
232        // Expected an error. Buffer is empty
233        assert_eq!(
234            NonDelegateAction::try_from_slice(Vec::new().as_ref()).map_err(|e| e.kind()),
235            Err(ErrorKind::InvalidData)
236        );
237
238        let delegate_action = create_delegate_action(Vec::<Action>::new());
239        let serialized_non_delegate_action = borsh::to_vec(&delegate_action).expect("Expect ok");
240
241        // Expected Action::Delegate has not been moved in enum Action
242        assert_eq!(serialized_non_delegate_action[0], ACTION_DELEGATE_NUMBER);
243
244        // Expected a nested DelegateAction error
245        assert_eq!(
246            NonDelegateAction::try_from_slice(&serialized_non_delegate_action)
247                .map_err(|e| e.kind()),
248            Err(ErrorKind::InvalidInput)
249        );
250
251        let delegate_action =
252            create_delegate_action(vec![Action::CreateAccount(CreateAccountAction {})]);
253        let serialized_delegate_action = borsh::to_vec(&delegate_action).expect("Expect ok");
254
255        // Valid action
256        assert_eq!(
257            Action::try_from_slice(&serialized_delegate_action).expect("Expect ok"),
258            delegate_action
259        );
260    }
261
262    /// Check that the hard-coded delegate action is valid.
263    #[test]
264    fn test_delegate_action_deserialization_hard_coded() {
265        let serialized_delegate_action = hex::decode(DELEGATE_ACTION_HEX).expect("invalid hex");
266        // The hex data is the same as the one we create below.
267        let delegate_action =
268            create_delegate_action(vec![Action::CreateAccount(CreateAccountAction {})]);
269
270        // Valid action
271        assert_eq!(
272            Action::try_from_slice(&serialized_delegate_action).expect("Expect ok"),
273            delegate_action
274        );
275    }
276
277    #[cfg(feature = "schemars")]
278    #[test]
279    fn test_non_delegate_action_json_schema_excludes_delegate() {
280        use schemars::{JsonSchema, SchemaGenerator};
281
282        // Create a schema generator
283        let mut generator = SchemaGenerator::default();
284
285        // Generate the NonDelegateAction schema
286        let non_delegate_schema = NonDelegateAction::json_schema(&mut generator);
287
288        // Convert to JSON for inspection
289        let schema_json = serde_json::to_value(&non_delegate_schema).unwrap();
290
291        // Get the oneOf array
292        let one_of = schema_json
293            .get("oneOf")
294            .expect("NonDelegateAction schema must have oneOf")
295            .as_array()
296            .expect("NonDelegateAction oneOf must be an array");
297
298        // Verify that none of the variants have a Delegate property
299        for variant in one_of {
300            if let Some(properties) = variant.get("properties") {
301                if let Some(props_obj) = properties.as_object() {
302                    assert!(
303                        !props_obj.contains_key("Delegate"),
304                        "NonDelegateAction schema should not contain Delegate variant"
305                    );
306                }
307            }
308        }
309
310        // Verify that the Action schema (for comparison) does include Delegate
311        let action_schema = Action::json_schema(&mut generator);
312        let action_json = serde_json::to_value(&action_schema).unwrap();
313
314        // Action MUST have a oneOf array
315        let action_one_of = action_json
316            .get("oneOf")
317            .expect("Action schema must have oneOf")
318            .as_array()
319            .expect("Action oneOf must be an array");
320
321        // Count how many variants have Delegate
322        let delegate_count = action_one_of
323            .iter()
324            .filter(|variant| {
325                variant
326                    .get("properties")
327                    .and_then(|p| p.as_object())
328                    .map(|p| p.contains_key("Delegate"))
329                    .unwrap_or(false)
330            })
331            .count();
332
333        assert_eq!(delegate_count, 1, "Action schema should contain exactly one Delegate variant");
334
335        // NonDelegateAction should have one less variant than Action
336        assert_eq!(
337            one_of.len(),
338            action_one_of.len() - 1,
339            "NonDelegateAction should have one less variant than Action (excluding Delegate)"
340        );
341    }
342}