1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
use {
18
    ethabi::{encode, Token},
19
    hex,
20
    parity_scale_codec::Encode,
21
    snowbridge_core::TokenIdOf,
22
    snowbridge_inbound_queue_primitives::v1::{
23
        Command, Destination, MessageV1, VersionedXcmMessage,
24
    },
25
    sp_core::H160,
26
    xcm::latest::prelude::*,
27
    xcm::latest::Location,
28
    xcm::prelude::InteriorLocation,
29
    xcm::v5::NetworkId,
30
    xcm_executor::traits::ConvertLocation,
31
};
32

            
33
pub const DANCELIGHT_GENESIS_HASH: [u8; 32] =
34
    hex_literal::hex!["983a1a72503d6cc3636776747ec627172b51272bf45e50a355348facb67a820a"];
35

            
36
#[derive(Debug, clap::ValueEnum, Clone)]
37
pub enum DestinationType {
38
    Relay,
39
    Container,
40
}
41

            
42
#[derive(Debug, clap::ValueEnum, Clone)]
43
pub enum TokenType {
44
    Native,
45
    Erc20,
46
}
47

            
48
#[derive(Debug, clap::Parser)]
49
pub struct PayloadGeneratorCmd {
50
    /// token_location: "Here" or "Parachain:<id>,PalletInstance:<id>"
51
    #[arg(long)]
52
    pub token_location: Option<String>,
53

            
54
    /// ParaId
55
    #[arg(long)]
56
    pub para_id: u32,
57

            
58
    /// Beneficiary address (AccountId20 or AccountId32 in hex)
59
    #[arg(long)]
60
    pub beneficiary: String,
61

            
62
    /// Container fee
63
    #[arg(long)]
64
    pub container_fee: u128,
65

            
66
    /// Amount
67
    #[arg(long)]
68
    pub amount: u128,
69

            
70
    /// Full fee
71
    #[arg(long)]
72
    pub fee: u128,
73

            
74
    /// Nonce (default = 1)
75
    #[arg(long, default_value_t = 1)]
76
    pub nonce: u64,
77

            
78
    #[arg(long)]
79
    pub genesis_hash: Option<String>,
80

            
81
    /// Destination type: relay or container
82
    #[arg(long, value_enum)]
83
    pub destination: DestinationType,
84

            
85
    /// Token type: native or erc20
86
    #[arg(long, value_enum)]
87
    pub token: TokenType,
88

            
89
    /// Only required if token = erc20
90
    #[arg(long)]
91
    pub token_address: Option<String>,
92
}
93

            
94
#[derive(Debug, Clone, PartialEq)]
95
pub struct PayloadResult {
96
    pub payload_bytes: Vec<u8>,
97
    pub encoded_hex: String,
98
}
99

            
100
impl PayloadGeneratorCmd {
101
4
    pub fn run(&self) -> PayloadResult {
102
4
        let beneficiary_bytes = self.decode_beneficiary();
103

            
104
4
        let payload = match (&self.destination, &self.token) {
105
            (DestinationType::Relay, TokenType::Native) => {
106
1
                self.build_native_relay(&beneficiary_bytes)
107
            }
108
            (DestinationType::Container, TokenType::Native) => {
109
1
                self.build_native_container(&beneficiary_bytes)
110
            }
111
            (DestinationType::Relay, TokenType::Erc20) => {
112
1
                self.build_erc20_relay(&beneficiary_bytes)
113
            }
114
            (DestinationType::Container, TokenType::Erc20) => {
115
1
                self.build_erc20_container(&beneficiary_bytes)
116
            }
117
        };
118

            
119
4
        let payload_bytes = payload.encode();
120
4
        println!("\nPayload (bytes): {:?}", payload_bytes);
121
4
        let encoded = encode(&[
122
4
            Token::Uint(self.nonce.into()),
123
4
            Token::Bytes(payload_bytes.clone()),
124
4
        ]);
125
4
        let encoded_hex = hex::encode(encoded);
126
4
        println!(
127
4
            "\nSnowbridgeVerificationPrimitivesLog.data (hex): 0x{}",
128
            encoded_hex
129
        );
130

            
131
4
        PayloadResult {
132
4
            payload_bytes,
133
4
            encoded_hex,
134
4
        }
135
4
    }
136

            
137
1
    fn build_native_relay(&self, beneficiary: &[u8]) -> VersionedXcmMessage {
138
1
        let token_location = self.parse_token_location();
139
1
        let token_location_reanchored = self.reanchor_token(token_location);
140
1
        let token_id = TokenIdOf::convert_location(&token_location_reanchored)
141
1
            .expect("unable to convert token location to token_id");
142

            
143
1
        let destination = self.build_account32_destination(beneficiary);
144

            
145
1
        VersionedXcmMessage::V1(MessageV1 {
146
1
            chain_id: 1,
147
1
            command: Command::SendNativeToken {
148
1
                token_id,
149
1
                destination,
150
1
                amount: self.amount,
151
1
                fee: self.fee,
152
1
            },
153
1
        })
154
1
    }
155

            
156
1
    fn build_native_container(&self, beneficiary: &[u8]) -> VersionedXcmMessage {
157
1
        let token_location = self.parse_token_location();
158
1
        let token_location_reanchored = self.reanchor_token(token_location);
159
1
        let token_id = TokenIdOf::convert_location(&token_location_reanchored)
160
1
            .expect("unable to convert token location to token_id");
161

            
162
1
        let destination = self.build_foreign_destination(beneficiary);
163

            
164
1
        VersionedXcmMessage::V1(MessageV1 {
165
1
            chain_id: 1,
166
1
            command: Command::SendNativeToken {
167
1
                token_id,
168
1
                destination,
169
1
                amount: self.amount,
170
1
                fee: self.fee,
171
1
            },
172
1
        })
173
1
    }
174

            
175
1
    fn build_erc20_relay(&self, beneficiary: &[u8]) -> VersionedXcmMessage {
176
1
        let token_address = self.parse_token_address();
177
1
        let token = H160::from_slice(&token_address[12..]);
178
1
        let destination = self.build_account32_destination(beneficiary);
179

            
180
1
        VersionedXcmMessage::V1(MessageV1 {
181
1
            chain_id: 1,
182
1
            command: Command::SendToken {
183
1
                token,
184
1
                destination,
185
1
                amount: self.amount,
186
1
                fee: self.fee,
187
1
            },
188
1
        })
189
1
    }
190

            
191
1
    fn build_erc20_container(&self, beneficiary: &[u8]) -> VersionedXcmMessage {
192
1
        let token_address = self.parse_token_address();
193
1
        let token = H160::from_slice(&token_address[12..]);
194
1
        let destination = self.build_foreign_destination(beneficiary);
195

            
196
1
        VersionedXcmMessage::V1(MessageV1 {
197
1
            chain_id: 1,
198
1
            command: Command::SendToken {
199
1
                token,
200
1
                destination,
201
1
                amount: self.amount,
202
1
                fee: self.fee,
203
1
            },
204
1
        })
205
1
    }
206

            
207
4
    fn decode_beneficiary(&self) -> Vec<u8> {
208
4
        let hex_trimmed = self
209
4
            .beneficiary
210
4
            .strip_prefix("0x")
211
4
            .unwrap_or(&self.beneficiary);
212
4
        hex::decode(hex_trimmed).expect("invalid hex in beneficiary")
213
4
    }
214

            
215
2
    fn parse_token_address(&self) -> [u8; 32] {
216
2
        let addr = self
217
2
            .token_address
218
2
            .as_ref()
219
2
            .expect("token_address is required for ERC20");
220
2
        let hex_trimmed = addr.strip_prefix("0x").unwrap_or(addr);
221
2
        let bytes = hex::decode(hex_trimmed).expect("invalid hex for token_address");
222
2
        let mut arr = [0u8; 32];
223
2
        if bytes.len() == 20 {
224
2
            arr[12..].copy_from_slice(&bytes);
225
2
        } else if bytes.len() == 32 {
226
            arr.copy_from_slice(&bytes);
227
        } else {
228
            panic!("token_address must be 20 or 32 bytes");
229
        }
230
2
        arr
231
2
    }
232

            
233
2
    fn parse_genesis_hash(&self) -> NetworkId {
234
2
        if let Some(ref h) = self.genesis_hash {
235
            let hex_trimmed = h.strip_prefix("0x").unwrap_or(h);
236
            let bytes = hex::decode(hex_trimmed).expect("invalid hex for genesis_hash");
237
            let mut genesis_array = [0u8; 32];
238
            genesis_array.copy_from_slice(&bytes);
239
            NetworkId::ByGenesis(genesis_array)
240
        } else {
241
2
            NetworkId::ByGenesis(DANCELIGHT_GENESIS_HASH)
242
        }
243
2
    }
244

            
245
2
    fn parse_token_location(&self) -> Location {
246
2
        let token_location = self
247
2
            .token_location
248
2
            .as_ref()
249
2
            .expect("--token-location is required for native");
250

            
251
2
        serde_json::from_str::<Location>(token_location).expect("invalid JSON for token_location")
252
2
    }
253

            
254
2
    fn reanchor_token(&self, token_location: Location) -> Location {
255
2
        let this_network = self.parse_genesis_hash();
256
2
        let ethereum_network = NetworkId::Ethereum { chain_id: 11155111 };
257
2
        let eth_location: Location = Location::new(1, ethereum_network);
258
2
        let universal_location: InteriorLocation = this_network.into();
259

            
260
2
        token_location
261
2
            .reanchored(&eth_location, &universal_location)
262
2
            .expect("unable to reanchor token")
263
2
    }
264

            
265
2
    fn build_foreign_destination(&self, bytes: &[u8]) -> Destination {
266
2
        match bytes.len() {
267
            20 => {
268
                let mut id20 = [0u8; 20];
269
                id20.copy_from_slice(bytes);
270
                Destination::ForeignAccountId20 {
271
                    para_id: self.para_id,
272
                    id: id20,
273
                    fee: self.container_fee,
274
                }
275
            }
276
            32 => {
277
2
                let mut id32 = [0u8; 32];
278
2
                id32.copy_from_slice(bytes);
279
2
                Destination::ForeignAccountId32 {
280
2
                    para_id: self.para_id,
281
2
                    id: id32,
282
2
                    fee: self.container_fee,
283
2
                }
284
            }
285
            n => panic!("beneficiary must be 20 or 32 bytes, got {}", n),
286
        }
287
2
    }
288

            
289
2
    fn build_account32_destination(&self, bytes: &[u8]) -> Destination {
290
2
        if bytes.len() != 32 {
291
            panic!("relay destination requires 32-byte AccountId32");
292
2
        }
293
2
        let mut id32 = [0u8; 32];
294
2
        id32.copy_from_slice(bytes);
295
2
        Destination::AccountId32 { id: id32 }
296
2
    }
297
}
298

            
299
#[cfg(test)]
300
mod tests {
301
    use super::*;
302
    use serde_json::json;
303

            
304
    #[test]
305
1
    fn e2e_native_container() {
306
1
        let cmd = PayloadGeneratorCmd {
307
1
            token_location: Some(
308
1
                json!({"parents":0,"interior":{"X2": [{"Parachain": 2002, }, {"PalletInstance": 10}]}})
309
1
                    .to_string(),
310
1
            ),
311
1
            para_id: 2002,
312
1
            beneficiary: "0x0505050505050505050505050505050505050505050505050505050505050505"
313
1
                .into(),
314
1
            container_fee: 500000000000000,
315
1
            amount: 100000000,
316
1
            fee: 1500000000000000,
317
1
            destination: DestinationType::Container,
318
1
            token: TokenType::Native,
319
1
            genesis_hash: None,
320
1
            token_address: None,
321
1
            nonce: 1,
322
1
        };
323
1
        let result = cmd.run();
324

            
325
1
        assert_eq!(result.encoded_hex, "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000007f00010000000000000002c97f6a848a8e7895b55dc9b894e4f552ea33203bfbdb478d506f05b62d9d5fd101d2070000050505050505050505050505050505050505050505050505050505050505050500406352bfc60100000000000000000000e1f50500000000000000000000000000c029f73d540500000000000000000000");
326
1
    }
327

            
328
    #[test]
329
1
    fn e2e_relay_native_to_relay() {
330
1
        let cmd = PayloadGeneratorCmd {
331
1
            token_location: Some(json!({"parents":0,"interior":"Here"}).to_string()),
332
1
            para_id: 2002,
333
1
            beneficiary: "0x0505050505050505050505050505050505050505050505050505050505050505"
334
1
                .into(),
335
1
            container_fee: 500000000000000,
336
1
            amount: 100000000,
337
1
            fee: 1500000000000000,
338
1
            destination: DestinationType::Relay,
339
1
            token: TokenType::Native,
340
1
            genesis_hash: None,
341
1
            token_address: None,
342
1
            nonce: 1,
343
1
        };
344
1
        let result = cmd.run();
345

            
346
1
        assert_eq!(result.encoded_hex, "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000006b00010000000000000002e95142d5aca3299068a3d9b4a659f9589559382d0a130a1d7cedc67d6c3d401d00050505050505050505050505050505050505050505050505050505050505050500e1f50500000000000000000000000000c029f73d5405000000000000000000000000000000000000000000000000000000000000");
347
1
    }
348

            
349
    #[test]
350
1
    fn e2e_erc20_to_relay() {
351
1
        let cmd = PayloadGeneratorCmd {
352
1
            token_location: Some(json!({"parents":0,"interior":"Here"}).to_string()),
353
1
            para_id: 2002,
354
1
            beneficiary: "0x0505050505050505050505050505050505050505050505050505050505050505"
355
1
                .into(),
356
1
            container_fee: 500000000000000,
357
1
            amount: 100000000,
358
1
            fee: 1500000000000000,
359
1
            destination: DestinationType::Relay,
360
1
            token: TokenType::Erc20,
361
1
            genesis_hash: None,
362
1
            nonce: 1,
363
1
            token_address: Some("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".into()),
364
1
        };
365
1
        let result = cmd.run();
366

            
367
1
        assert_eq!(result.encoded_hex, "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005f00010000000000000001deadbeefdeadbeefdeadbeefdeadbeefdeadbeef00050505050505050505050505050505050505050505050505050505050505050500e1f50500000000000000000000000000c029f73d540500000000000000000000");
368
1
    }
369

            
370
    #[test]
371
1
    fn e2e_to_container() {
372
1
        let cmd = PayloadGeneratorCmd {
373
1
            token_location: Some(json!({"parents":0,"interior":"Here"}).to_string()),
374
1
            para_id: 2002,
375
1
            beneficiary: "0x0505050505050505050505050505050505050505050505050505050505050505"
376
1
                .into(),
377
1
            container_fee: 500000000000000,
378
1
            amount: 100000000,
379
1
            fee: 1500000000000000,
380
1
            destination: DestinationType::Container,
381
1
            token: TokenType::Erc20,
382
1
            genesis_hash: None,
383
1
            nonce: 1,
384
1
            token_address: Some("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".into()),
385
1
        };
386
1
        let result = cmd.run();
387

            
388
1
        assert_eq!(result.encoded_hex, "00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000007300010000000000000001deadbeefdeadbeefdeadbeefdeadbeefdeadbeef01d2070000050505050505050505050505050505050505050505050505050505050505050500406352bfc60100000000000000000000e1f50500000000000000000000000000c029f73d540500000000000000000000000000000000000000000000");
389
1
    }
390
}