Skip to main content

defuse_nep413/
lib.rs

1//! [NEP-413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md)
2//! Offchain Signing Standard
3
4use core::fmt::Display;
5
6use borsh::{BorshDeserialize, BorshSerialize};
7use defuse_crypto::{Curve, ed25519::Ed25519};
8use defuse_digest::{Digest, sha2::Sha256};
9use defuse_nep461::{OffchainMessage, SignedMessageNep};
10use digest_io::IoWrapper;
11
12/// [NEP-413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md)
13/// Offchain Signing Standard
14pub struct Nep413;
15
16impl Nep413 {
17    /// Verify signature over given payload for given public key according to
18    /// [NEP-413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md).
19    #[must_use = "check if verification passed"]
20    #[inline]
21    pub fn verify(
22        public_key: &<Ed25519 as Curve>::PublicKey,
23        payload: &Nep413Payload,
24        signature: &<Ed25519 as Curve>::Signature,
25    ) -> bool {
26        Ed25519::verify(public_key, &Self::prehash(payload), signature)
27    }
28
29    /// Derive prehash for signing.
30    #[inline]
31    pub fn prehash(payload: &Nep413Payload) -> [u8; 32] {
32        let mut hasher = IoWrapper(Sha256::new());
33
34        // serialize directly to hasher
35        borsh::to_writer(&mut hasher, &(Self::OFFCHAIN_PREFIX_TAG, payload))
36            .unwrap_or_else(|_| unreachable!());
37
38        hasher.0.finalize().into()
39    }
40}
41
42impl SignedMessageNep for Nep413 {
43    /// NEP number used to derive offchain prefix tag according to
44    /// [NEP-461](https://github.com/near/NEPs/pull/461).
45    ///
46    /// # Examples
47    ///
48    /// ```rust
49    /// use defuse_nep413::Nep413;
50    /// use defuse_nep461::OffchainMessage;
51    ///
52    /// assert_eq!(Nep413::OFFCHAIN_PREFIX_TAG, 2147484061);
53    /// ```
54    const NEP_NUMBER: u32 = 413;
55}
56
57/// [NEP-413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md) payload
58#[cfg_attr(
59    feature = "serde",
60    ::cfg_eval::cfg_eval,
61    ::serde_with::serde_as,
62    derive(::serde::Serialize, ::serde::Deserialize),
63    cfg_attr(feature = "schemars-v0_8", derive(::schemars::JsonSchema)),
64    serde(rename_all = "camelCase")
65)]
66#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
67#[cfg_attr(feature = "borsh-schema", derive(::borsh::BorshSchema))]
68#[derive(Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
69pub struct Nep413Payload {
70    pub message: String,
71
72    #[cfg_attr(feature = "serde", serde_as(as = "::serde_with::base64::Base64"))]
73    pub nonce: [u8; 32],
74
75    pub recipient: String,
76
77    #[cfg_attr(
78        feature = "serde",
79        serde(default, skip_serializing_if = "Option::is_none")
80    )]
81    pub callback_url: Option<String>,
82}
83
84impl Nep413Payload {
85    #[must_use]
86    #[inline]
87    pub fn new(message: impl Into<String>) -> Self {
88        Self {
89            message: message.into(),
90            nonce: [0u8; 32],
91            recipient: String::new(),
92            callback_url: None,
93        }
94    }
95
96    #[must_use]
97    #[inline]
98    pub fn nonce(mut self, nonce: impl Into<[u8; 32]>) -> Self {
99        self.nonce = nonce.into();
100        self
101    }
102
103    #[must_use]
104    #[inline]
105    pub fn recipient(mut self, recipient: impl Display) -> Self {
106        self.recipient = recipient.to_string();
107        self
108    }
109
110    #[must_use]
111    #[inline]
112    pub fn callback_url(mut self, callback_url: impl Into<String>) -> Self {
113        self.callback_url = Some(callback_url.into());
114        self
115    }
116}
117
118#[cfg(feature = "near-kit")]
119const _: () = {
120    impl From<Nep413Payload> for near_kit::nep413::SignMessageParams {
121        #[inline]
122        fn from(payload: Nep413Payload) -> Self {
123            Self {
124                message: payload.message,
125                nonce: payload.nonce,
126                recipient: payload.recipient,
127                callback_url: payload.callback_url,
128                state: None,
129            }
130        }
131    }
132};
133
134#[cfg(test)]
135mod tests {
136    use defuse_crypto::ed25519::{Ed25519PublicKey, Ed25519Signature};
137    use hex_literal::hex;
138    use rstest::rstest;
139
140    use super::*;
141
142    #[rstest]
143    #[case(
144        hex!("e2e9cb7ac57cb46d4da1ce1d1cc2c33bdfe17407c517916b522724a8ea2c6c50"),
145        Nep413Payload {
146            message: "Hello, world!".to_string(),
147            nonce: [0u8; 32],
148            recipient: "intents.near".to_string(),
149            callback_url: None,
150        },
151        hex!("e2ff6254871a3fec1853c167b42f0f14248c4cf7fef5452dc24d8dbdc5c4bf183ab707322b4d782d5f5a05571bae476c5f7ee41c473f3002e600865e46b75d0f"),
152    )]
153    fn verify_ok(
154        #[case] public_key: impl Into<Ed25519PublicKey>,
155        #[case] payload: Nep413Payload,
156        #[case] signature: impl Into<Ed25519Signature>,
157    ) {
158        assert!(Nep413::verify(
159            &public_key.into().try_into().unwrap(),
160            &payload,
161            &signature.into().into()
162        ));
163    }
164}