1use nom::{
2 bytes::complete::tag,
3 character::complete::line_ending,
4 combinator::{eof, map, verify},
5 error::{Error, ParseError},
6 sequence::delimited,
7 AsChar, Compare, Err, IResult, Input, Offset, ParseTo, Parser,
8};
9use solana_intents::{tag_key_value, SymbolOrMint, Version};
10
11const BRIDGE_MESSAGE_PREFIX: &str =
12 "Fogo Bridge Transfer:\nSigning this intent will bridge out the tokens as described below.\n";
13
14#[derive(Debug, PartialEq)]
15pub enum BridgeMessage {
16 Ntt(NttMessage),
17}
18
19#[derive(Debug, PartialEq)]
20pub struct NttMessage {
21 pub version: Version,
22 pub from_chain_id: String,
23 pub symbol_or_mint: SymbolOrMint,
24 pub amount: String,
25 pub to_chain_id: String,
26 pub recipient_address: String,
27 pub fee_amount: String,
28 pub fee_symbol_or_mint: SymbolOrMint,
29 pub nonce: u64,
30}
31
32#[derive(Copy, Clone, PartialEq)]
33pub enum WormholeChainId {
34 Solana,
35 Fogo,
36}
37
38impl From<WormholeChainId> for u16 {
40 fn from(chain_id: WormholeChainId) -> u16 {
41 match chain_id {
42 WormholeChainId::Solana => 1,
43 WormholeChainId::Fogo => 51,
44 }
45 }
46}
47
48impl WormholeChainId {
49 pub fn decimals_native(self) -> u32 {
50 match self {
51 WormholeChainId::Solana => 9,
52 WormholeChainId::Fogo => 9,
53 }
54 }
55
56 pub fn decimals_gas_price(self) -> u32 {
58 match self {
59 WormholeChainId::Solana => 15,
60 WormholeChainId::Fogo => 15,
61 }
62 }
63}
64
65pub fn convert_chain_id_to_wormhole(chain_id: &str) -> Option<WormholeChainId> {
66 match chain_id {
67 "solana" => Some(WormholeChainId::Solana),
68 "fogo" => Some(WormholeChainId::Fogo),
69 _ => None,
70 }
71}
72
73impl TryFrom<Vec<u8>> for BridgeMessage {
74 type Error = Err<Error<Vec<u8>>>;
75
76 fn try_from(message: Vec<u8>) -> Result<Self, Self::Error> {
77 match message_ntt.parse(message.as_slice()) {
78 Ok((_, message)) => Ok(BridgeMessage::Ntt(message)),
79 Err(e) => Err(Err::<Error<&[u8]>>::to_owned(e)),
80 }
81 }
82}
83
84fn message_ntt<I, E>(input: I) -> IResult<I, NttMessage, E>
85where
86 I: Input,
87 I: ParseTo<String>,
88 I: ParseTo<SymbolOrMint>,
89 I: ParseTo<Version>,
90 I: ParseTo<u64>,
91 I: ParseTo<u16>,
92 I: Offset,
93 I: for<'a> Compare<&'a str>,
94 <I as Input>::Item: AsChar,
95 E: ParseError<I>,
96{
97 map(
98 delimited(
99 (tag(BRIDGE_MESSAGE_PREFIX), line_ending),
100 (
101 verify(tag_key_value("version"), |version: &Version| {
102 version.major == 0 && version.minor == 2
103 }),
104 tag_key_value("from_chain_id"),
105 tag_key_value("to_chain_id"),
106 tag_key_value("token"),
107 tag_key_value("amount"),
108 tag_key_value("recipient_address"),
109 tag_key_value("fee_token"),
110 tag_key_value("fee_amount"),
111 tag_key_value("nonce"),
112 ),
113 eof,
114 ),
115 |(
116 version,
117 from_chain_id,
118 to_chain_id,
119 symbol_or_mint,
120 amount,
121 recipient_address,
122 fee_symbol_or_mint,
123 fee_amount,
124 nonce,
125 )| NttMessage {
126 version,
127 from_chain_id,
128 to_chain_id,
129 symbol_or_mint,
130 amount,
131 recipient_address,
132 fee_amount,
133 fee_symbol_or_mint,
134 nonce,
135 },
136 )
137 .parse(input)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use indoc::indoc;
144 use nom::error::ErrorKind;
145
146 #[test]
147 fn test_parse() {
148 let message = indoc! {"
149 Fogo Bridge Transfer:
150 Signing this intent will bridge out the tokens as described below.
151
152 version: 0.2
153 from_chain_id: foo
154 to_chain_id: solana
155 token: FOGO
156 amount: 42.676
157 recipient_address: 0xabc906d4A6074599D5471f04f9d6261030C8debe
158 fee_token: USDC
159 fee_amount: 0.001
160 nonce: 1
161 "};
162
163 assert_eq!(
164 TryInto::<BridgeMessage>::try_into(message.as_bytes().to_vec()).unwrap(),
165 BridgeMessage::Ntt(NttMessage {
166 version: Version { major: 0, minor: 2 },
167 from_chain_id: "foo".to_string(),
168 to_chain_id: "solana".to_string(),
169 symbol_or_mint: SymbolOrMint::Symbol("FOGO".to_string()),
170 amount: "42.676".to_string(),
171 recipient_address: "0xabc906d4A6074599D5471f04f9d6261030C8debe".to_string(),
172 fee_symbol_or_mint: SymbolOrMint::Symbol("USDC".to_string()),
173 fee_amount: "0.001".to_string(),
174 nonce: 1
175 })
176 );
177 }
178
179 #[test]
180 fn test_parse_with_unexpected_data_after_end() {
181 let message = indoc! {"
182 Fogo Bridge Transfer:
183 Signing this intent will bridge out the tokens as described below.
184
185 version: 0.2
186 from_chain_id: foo
187 to_chain_id: solana
188 token: FOGO
189 amount: 42.676
190 recipient_address: 0xabc906d4A6074599D5471f04f9d6261030C8debe
191 fee_token: USDC
192 fee_amount: 0.001
193 nonce: 1
194 this data should not be here"};
195
196 let result = TryInto::<BridgeMessage>::try_into(message.as_bytes().to_vec());
197 assert_eq!(
198 result,
199 Err(Err::Error(Error {
200 code: ErrorKind::Eof,
201 input: "this data should not be here".as_bytes().to_vec()
202 }))
203 );
204 }
205}