intent_transfer/bridge/
message.rs

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
38/// Mapping from https://wormhole.com/docs/products/reference/chain-ids/
39impl 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    /// The decimals of the gas price specification (e.g. microlamports)
57    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}