1use 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
12pub struct Nep413;
15
16impl Nep413 {
17 #[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 #[inline]
31 pub fn prehash(payload: &Nep413Payload) -> [u8; 32] {
32 let mut hasher = IoWrapper(Sha256::new());
33
34 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 const NEP_NUMBER: u32 = 413;
55}
56
57#[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}