signatory_client_lib/
solana.rs

1use serde::{Deserialize, Serialize};
2
3use crate::common::LiquidityUnavailableReason;
4use crate::endorsement::Endorsement;
5use crate::payment_in_lieu::PaymentInLieuToken;
6
7#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Deserialize, Serialize)]
8pub enum FeePayer {
9    #[serde(rename = "retailTrader")]
10    RetailTrader,
11    #[serde(rename = "marketMaker")]
12    MarketMaker,
13}
14
15#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct IndicativeQuoteRequestParams {
18    /// Base58-encoded SPL mint sent by the retail trader.
19    pub send_mint: String,
20    /// Base58-encoded SPL mint received by the retail trader.
21    pub receive_mint: String,
22    /// Quantity to send
23    pub send_qty: String,
24    /// DFlow network public key identifying the order flow source.
25    pub order_flow_source: String,
26    /// The order flow source's endorsement of this quote request.
27    pub endorsement: Endorsement,
28    /// (Optional) The fee payer for the transaction. If unspecified and the order flow source has
29    /// multiple auctions with different fee payer modes that match the request, the signatory server
30    /// will determine which fee payer mode to use.
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub fee_payer: Option<FeePayer>,
33}
34
35#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
36#[serde(tag = "type", content = "data")]
37pub enum IndicativeQuoteResponse {
38    Ok(IndicativeQuoteOkResponse),
39    Unavailable(IndicativeQuoteUnavailableResponse),
40}
41
42#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
43#[serde(rename_all = "camelCase")]
44pub struct IndicativeQuoteOkResponse {
45    /// Token units sent to retail trader per token received from retail trader
46    pub fill_price: String,
47    pub effective_platform_fee_bps: u16,
48    pub auction_id: u64,
49    pub auction_epoch: u64,
50}
51
52#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct IndicativeQuoteUnavailableResponse {
55    pub reason: LiquidityUnavailableReason,
56}
57
58#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct FirmQuoteRequestParams {
61    /// Base58-encoded SPL mint sent by the retail trader.
62    pub send_mint: String,
63    /// Base58-encoded SPL mint received by the retail trader.
64    pub receive_mint: String,
65    /// Quantity to send
66    pub send_qty: String,
67    /// Use native SOL instead of wrapped SOL when buying or selling SOL. Default is true.
68    #[serde(rename = "useNativeSOL", skip_serializing_if = "Option::is_none")]
69    pub use_native_sol: Option<bool>,
70    /// DFlow network public key identifying the order flow source.
71    pub order_flow_source: String,
72    /// The order flow source's endorsement of this quote request.
73    pub endorsement: Endorsement,
74    /// (Optional) The fee payer for the transaction. If unspecified and the order flow source has
75    /// multiple auctions with different fee payer modes that match the request, the signatory server
76    /// will determine which fee payer mode to use. */
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub fee_payer: Option<FeePayer>,
79}
80
81#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
82#[serde(tag = "type", content = "data")]
83pub enum FirmQuoteResponse {
84    Ok(Box<FirmQuoteOkResponse>),
85    PaymentInLieu(Box<FirmQuotePaymentInLieuResponse>),
86    Unavailable(Box<FirmQuoteUnavailableResponse>),
87}
88
89#[derive(Clone, PartialEq, Debug, Deserialize, Serialize)]
90#[serde(rename_all = "camelCase")]
91pub struct FirmQuoteOkResponse {
92    /** The Base64-encoded transaction. The client must not override the blockhash included in this
93     * transaction. */
94    pub tx: String,
95    /** Minimum lamports balance required in the retail trader's wallet for the transaction to be
96     * processed. */
97    pub tx_required_lamports: u64,
98    /** Solana transaction fee paid by the retail trader. */
99    pub solana_transaction_fee: u64,
100    /** Refundable token account deposit paid by the retail trader to create an associated token
101     * account for the token that the retail trader is receiving. This is required by Solana and is
102     * returned to the retail trader if the retail trader closes the token account. */
103    pub token_account_rent_deposit: u64,
104    /** The last valid block height to use when confirming the transaction. */
105    pub last_valid_block_height: u64,
106    /** The last block height at which the transaction can be sent via sendTransaction. */
107    pub last_allowed_block_height: u64,
108    /** The commitment level that the signatory server will use when querying the block height to
109     * determine whether the lastAllowedBlockHeight has passed. Clients should use this commitment
110     * level when querying and comparing the current block height to the lastAllowedBlockHeight. */
111    pub block_height_query_commitment: String,
112    /** Send quantity specified as a scaled integer */
113    pub send_qty: String,
114    /** Receive quantity specified as a scaled integer */
115    pub receive_qty: String,
116    /** Minimum allowed fill quantity before platform fee. Specified as a scaled integer. */
117    pub min_fill_qty: String,
118    /** Platform fee info. Only included if a platform fee was applied to the transaction. */
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub platform_fee: Option<PlatformFee>,
121    /** Time at which the DBBO was calculated */
122    pub dbbo_time: u64,
123    /** (Unstable) Response body returned by the DBBO query that was used to determine the
124     * `minFillQty` */
125    pub dbbo_data: serde_json::Value,
126    /** Notional value in USD of the send quantity */
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub send_notional: Option<f64>,
129    /** Notional value in USD of the receive quantity */
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub receive_notional: Option<f64>,
132    /** Notional value in USD of the fill quantity before platform fee */
133    #[serde(skip_serializing_if = "Option::is_none")]
134    pub fill_notional: Option<f64>,
135    /** (Unstable) Signatory server request identifier */
136    pub request_id: SignatoryRequestId,
137}
138
139#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
140#[serde(rename_all = "camelCase")]
141pub struct SignatoryRequestId {
142    /** Base64-encoded Ed25519 signature of
143     *  `"{id},{retailTrader},{notional},{auctionId},{auctionEpoch},{marketMakerURL},{quoteId},
144     *    ,{endorsement},{lastValidBlockHeight},{lastAllowedBlockHeight},{tx}"` */
145    pub signature: String,
146    /** Unique identifier for the request. */
147    pub id: String,
148    pub retail_trader: String,
149    pub notional: u64,
150    pub auction_id: u64,
151    pub auction_epoch: u64,
152    #[serde(rename = "marketMakerURL")]
153    pub market_maker_url: String,
154    pub quote_id: String,
155    pub endorsement: Endorsement,
156    pub last_valid_block_height: u64,
157    pub last_allowed_block_height: u64,
158    pub tx: String,
159}
160
161#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
162#[serde(rename_all = "camelCase")]
163pub struct PlatformFee {
164    pub receiver: String,
165    /** Platform fee quantity specified as a scaled integer. Note that this is
166     * already factored into the `receiveQty`. */
167    pub qty: String,
168    pub bps: u16,
169    /** Base58-encoded SPL mint of the token in which the platform fee will be paid */
170    pub mint: String,
171}
172
173#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
174#[serde(rename_all = "camelCase")]
175pub struct FirmQuotePaymentInLieuResponse {
176    pub reason: LiquidityUnavailableReason,
177    pub payment_in_lieu_token: PaymentInLieuToken,
178}
179
180#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
181#[serde(rename_all = "camelCase")]
182pub struct FirmQuoteUnavailableResponse {
183    pub reason: LiquidityUnavailableReason,
184}
185
186#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
187#[serde(rename_all = "camelCase")]
188pub struct SendTransactionRequestParams {
189    /// The Base64-encoded transaction, signed by the retail trader.
190    pub tx: String,
191    /// The requestId from the firm quote response
192    pub request_id: SignatoryRequestId,
193}
194
195#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
196#[serde(tag = "type", content = "data")]
197pub enum SendTransactionResponse {
198    Sent(SendTransactionSentResponse),
199    NotSent,
200}
201
202#[derive(Clone, Eq, PartialEq, Hash, Debug, Deserialize, Serialize)]
203#[serde(rename_all = "camelCase")]
204pub struct SendTransactionSentResponse {
205    pub signature: String,
206}
207
208#[cfg(test)]
209mod tests {
210    use assert_json_diff::assert_json_eq;
211    use serde_json::{json, Map, Value};
212
213    use super::*;
214
215    #[test]
216    fn test_serde_indicative_quote_ok_response() {
217        let obj = IndicativeQuoteResponse::Ok(IndicativeQuoteOkResponse {
218            fill_price: "100000".into(),
219            effective_platform_fee_bps: 50,
220            auction_id: 1,
221            auction_epoch: 2,
222        });
223        let expected = json!({
224            "type": "Ok",
225            "data": {
226                "fillPrice": "100000",
227                "effectivePlatformFeeBps": 50,
228                "auctionId": 1,
229                "auctionEpoch": 2,
230            },
231        });
232        let serialized = serde_json::to_value(&obj).unwrap();
233        assert_json_eq!(serialized, expected);
234
235        let deserialized: IndicativeQuoteResponse = serde_json::from_value(expected).unwrap();
236        assert_eq!(deserialized, obj);
237    }
238
239    #[test]
240    fn test_serde_indicative_quote_unavailable_response() {
241        let obj = IndicativeQuoteResponse::Unavailable(IndicativeQuoteUnavailableResponse {
242            reason: LiquidityUnavailableReason {
243                code: 1,
244                msg: "msg".into(),
245            },
246        });
247        let expected = json!({
248            "type": "Unavailable",
249            "data": {
250                "reason": {
251                    "code": 1,
252                    "msg": "msg",
253                },
254            },
255        });
256        let serialized = serde_json::to_value(&obj).unwrap();
257        assert_json_eq!(serialized, expected);
258
259        let deserialized: IndicativeQuoteResponse = serde_json::from_value(expected).unwrap();
260        assert_eq!(deserialized, obj);
261    }
262
263    #[test]
264    fn test_serde_firm_quote_ok_response() {
265        let obj = FirmQuoteResponse::Ok(Box::new(FirmQuoteOkResponse {
266            tx: "tx".into(),
267            tx_required_lamports: 1,
268            solana_transaction_fee: 1,
269            token_account_rent_deposit: 1,
270            last_valid_block_height: 1,
271            last_allowed_block_height: 1,
272            block_height_query_commitment: "block_height_query_commitment".into(),
273            send_qty: "5000000".into(),
274            receive_qty: "2000000".into(),
275            min_fill_qty: "2000000".into(),
276            platform_fee: Some(PlatformFee {
277                receiver: "receiver".into(),
278                qty: "17000".into(),
279                bps: 85,
280                mint: "mint".into(),
281            }),
282            dbbo_time: 1,
283            dbbo_data: json!({}),
284            send_notional: Some(0.52),
285            receive_notional: Some(0.52),
286            fill_notional: Some(0.52),
287            request_id: SignatoryRequestId {
288                signature: "signature".into(),
289                id: "id".into(),
290                retail_trader: "retail_trader".into(),
291                notional: 1,
292                auction_id: 1,
293                auction_epoch: 1,
294                market_maker_url: "market_maker_url".into(),
295                quote_id: "quote_id".into(),
296                endorsement: Endorsement {
297                    endorser: "endorser".into(),
298                    signature: "signature".into(),
299                    id: "id".into(),
300                    expiration_time_utc: 1,
301                    data: "data".into(),
302                },
303                last_valid_block_height: 1,
304                last_allowed_block_height: 1,
305                tx: "tx".into(),
306            },
307        }));
308        let expected = json!({
309            "type": "Ok",
310            "data": {
311                "tx": "tx",
312                "txRequiredLamports": 1,
313                "solanaTransactionFee": 1,
314                "tokenAccountRentDeposit": 1,
315                "lastValidBlockHeight": 1,
316                "lastAllowedBlockHeight": 1,
317                "blockHeightQueryCommitment": "block_height_query_commitment",
318                "sendQty": "5000000",
319                "receiveQty": "2000000",
320                "minFillQty": "2000000",
321                "platformFee": {
322                    "receiver": "receiver",
323                    "qty": "17000",
324                    "bps": 85,
325                    "mint": "mint",
326                },
327                "dbboTime": 1,
328                "dbboData": Value::Object(Map::new()),
329                "sendNotional": 0.52,
330                "receiveNotional": 0.52,
331                "fillNotional": 0.52,
332                "requestId": {
333                    "signature": "signature",
334                    "id": "id",
335                    "retailTrader": "retail_trader",
336                    "notional": 1,
337                    "auctionId": 1,
338                    "auctionEpoch": 1,
339                    "marketMakerURL": "market_maker_url",
340                    "quoteId": "quote_id",
341                    "endorsement": {
342                        "endorser": "endorser",
343                        "signature": "signature",
344                        "id": "id",
345                        "expirationTimeUTC": 1,
346                        "data": "data",
347                    },
348                    "lastValidBlockHeight": 1,
349                    "lastAllowedBlockHeight": 1,
350                    "tx": "tx",
351                },
352            },
353        });
354        let serialized = serde_json::to_value(&obj).unwrap();
355        assert_json_eq!(serialized, expected);
356        let deserialized: FirmQuoteResponse = serde_json::from_value(expected).unwrap();
357        assert_eq!(deserialized, obj);
358    }
359
360    #[test]
361    fn test_serde_firm_quote_ok_response_omit_none() {
362        let obj = FirmQuoteResponse::Ok(Box::new(FirmQuoteOkResponse {
363            tx: "tx".into(),
364            tx_required_lamports: 1,
365            solana_transaction_fee: 1,
366            token_account_rent_deposit: 1,
367            last_valid_block_height: 1,
368            last_allowed_block_height: 1,
369            block_height_query_commitment: "block_height_query_commitment".into(),
370            send_qty: "5000000".into(),
371            receive_qty: "2000000".into(),
372            min_fill_qty: "2000000".into(),
373            platform_fee: None,
374            dbbo_time: 1,
375            dbbo_data: json!({}),
376            send_notional: None,
377            receive_notional: None,
378            fill_notional: None,
379            request_id: SignatoryRequestId {
380                signature: "signature".into(),
381                id: "id".into(),
382                retail_trader: "retail_trader".into(),
383                notional: 1,
384                auction_id: 1,
385                auction_epoch: 1,
386                market_maker_url: "market_maker_url".into(),
387                quote_id: "quote_id".into(),
388                endorsement: Endorsement {
389                    endorser: "endorser".into(),
390                    signature: "signature".into(),
391                    id: "id".into(),
392                    expiration_time_utc: 1,
393                    data: "data".into(),
394                },
395                last_valid_block_height: 1,
396                last_allowed_block_height: 1,
397                tx: "tx".into(),
398            },
399        }));
400        let expected = json!({
401            "type": "Ok",
402            "data": {
403                "tx": "tx",
404                "txRequiredLamports": 1,
405                "solanaTransactionFee": 1,
406                "tokenAccountRentDeposit": 1,
407                "lastValidBlockHeight": 1,
408                "lastAllowedBlockHeight": 1,
409                "blockHeightQueryCommitment": "block_height_query_commitment",
410                "sendQty": "5000000",
411                "receiveQty": "2000000",
412                "minFillQty": "2000000",
413                "dbboTime": 1,
414                "dbboData": Value::Object(Map::new()),
415                "requestId": {
416                    "signature": "signature",
417                    "id": "id",
418                    "retailTrader": "retail_trader",
419                    "notional": 1,
420                    "auctionId": 1,
421                    "auctionEpoch": 1,
422                    "marketMakerURL": "market_maker_url",
423                    "quoteId": "quote_id",
424                    "endorsement": {
425                        "endorser": "endorser",
426                        "signature": "signature",
427                        "id": "id",
428                        "expirationTimeUTC": 1,
429                        "data": "data",
430                    },
431                    "lastValidBlockHeight": 1,
432                    "lastAllowedBlockHeight": 1,
433                    "tx": "tx",
434                },
435            },
436        });
437        let serialized = serde_json::to_value(&obj).unwrap();
438        assert_json_eq!(serialized, expected);
439        let deserialized: FirmQuoteResponse = serde_json::from_value(expected).unwrap();
440        assert_eq!(deserialized, obj);
441    }
442
443    #[test]
444    fn test_serde_firm_quote_payment_in_lieu_response() {
445        let obj = FirmQuoteResponse::PaymentInLieu(Box::new(FirmQuotePaymentInLieuResponse {
446            reason: LiquidityUnavailableReason {
447                code: 1,
448                msg: "msg".into(),
449            },
450            payment_in_lieu_token: PaymentInLieuToken {
451                issuer: "issuer".into(),
452                signature: "signature".into(),
453                id: "id".into(),
454                notional: 1,
455                auction_id: 2,
456                auction_epoch: 3,
457                market_maker: "market_maker".into(),
458                endorsement: Endorsement {
459                    endorser: "endorser".into(),
460                    signature: "signature".into(),
461                    id: "id".into(),
462                    expiration_time_utc: 1,
463                    data: "data".into(),
464                },
465            },
466        }));
467        let expected = json!({
468            "type": "PaymentInLieu",
469            "data": {
470                "reason": {
471                    "code": 1,
472                    "msg": "msg",
473                },
474                "paymentInLieuToken": {
475                    "issuer": "issuer",
476                    "signature": "signature",
477                    "id": "id",
478                    "notional": 1,
479                    "auctionId": 2,
480                    "auctionEpoch": 3,
481                    "marketMaker": "market_maker",
482                    "endorsement": {
483                        "endorser": "endorser",
484                        "signature": "signature",
485                        "id": "id",
486                        "expirationTimeUTC": 1,
487                        "data": "data",
488                    },
489                },
490            },
491        });
492        let serialized = serde_json::to_value(&obj).unwrap();
493        assert_json_eq!(serialized, expected);
494        let deserialized: FirmQuoteResponse = serde_json::from_value(expected).unwrap();
495        assert_eq!(deserialized, obj);
496    }
497
498    #[test]
499    fn test_serde_firm_quote_unavailable_response() {
500        let obj = FirmQuoteResponse::Unavailable(Box::new(FirmQuoteUnavailableResponse {
501            reason: LiquidityUnavailableReason {
502                code: 1,
503                msg: "msg".into(),
504            },
505        }));
506        let expected = json!({
507            "type": "Unavailable",
508            "data": {
509                "reason": {
510                    "code": 1,
511                    "msg": "msg",
512                },
513            },
514        });
515        let serialized = serde_json::to_value(&obj).unwrap();
516        assert_json_eq!(serialized, expected);
517        let deserialized: FirmQuoteResponse = serde_json::from_value(expected).unwrap();
518        assert_eq!(deserialized, obj);
519    }
520
521    #[test]
522    fn test_serde_send_transaction_sent_response() {
523        let obj = SendTransactionResponse::Sent(SendTransactionSentResponse {
524            signature: "signature".into(),
525        });
526        let expected = json!({
527            "type": "Sent",
528            "data": {
529                "signature": "signature",
530            },
531        });
532        let serialized = serde_json::to_value(&obj).unwrap();
533        assert_json_eq!(serialized, expected);
534        let deserialized: SendTransactionResponse = serde_json::from_value(expected).unwrap();
535        assert_eq!(deserialized, obj);
536    }
537
538    #[test]
539    fn test_serde_send_transaction_not_sent_response() {
540        let obj = SendTransactionResponse::NotSent;
541        let expected = json!({
542            "type": "NotSent",
543        });
544        let serialized = serde_json::to_value(&obj).unwrap();
545        assert_json_eq!(serialized, expected);
546        let deserialized: SendTransactionResponse = serde_json::from_value(expected).unwrap();
547        assert_eq!(deserialized, obj);
548    }
549
550    #[test]
551    fn test_serde_indicative_quote_request() {
552        let obj = IndicativeQuoteRequestParams {
553            send_mint: "send_mint".into(),
554            receive_mint: "receive_mint".into(),
555            send_qty: "5000000".into(),
556            order_flow_source: "order_flow_source".into(),
557            endorsement: Endorsement {
558                endorser: "endorser".into(),
559                signature: "signature".into(),
560                id: "id".into(),
561                expiration_time_utc: 1,
562                data: "data".into(),
563            },
564            fee_payer: Some(FeePayer::RetailTrader),
565        };
566        let expected = json!({
567            "sendMint": "send_mint",
568            "receiveMint": "receive_mint",
569            "sendQty": "5000000",
570            "orderFlowSource": "order_flow_source",
571            "endorsement": {
572                "endorser": "endorser",
573                "signature": "signature",
574                "id": "id",
575                "expirationTimeUTC": 1,
576                "data": "data",
577            },
578            "feePayer": "retailTrader",
579        });
580        let serialized = serde_json::to_value(&obj).unwrap();
581        assert_json_eq!(serialized, expected);
582        let deserialized: IndicativeQuoteRequestParams = serde_json::from_value(expected).unwrap();
583        assert_eq!(deserialized, obj);
584
585        let obj = IndicativeQuoteRequestParams {
586            fee_payer: Some(FeePayer::MarketMaker),
587            ..obj
588        };
589        let expected = json!({
590            "sendMint": "send_mint",
591            "receiveMint": "receive_mint",
592            "sendQty": "5000000",
593            "orderFlowSource": "order_flow_source",
594            "endorsement": {
595                "endorser": "endorser",
596                "signature": "signature",
597                "id": "id",
598                "expirationTimeUTC": 1,
599                "data": "data",
600            },
601            "feePayer": "marketMaker",
602        });
603        let serialized = serde_json::to_value(&obj).unwrap();
604        assert_json_eq!(serialized, expected);
605        let deserialized: IndicativeQuoteRequestParams = serde_json::from_value(expected).unwrap();
606        assert_eq!(deserialized, obj);
607    }
608
609    #[test]
610    fn test_serde_indicative_quote_request_omit_none() {
611        let obj = IndicativeQuoteRequestParams {
612            send_mint: "send_mint".into(),
613            receive_mint: "receive_mint".into(),
614            send_qty: "5000000".into(),
615            order_flow_source: "order_flow_source".into(),
616            endorsement: Endorsement {
617                endorser: "endorser".into(),
618                signature: "signature".into(),
619                id: "id".into(),
620                expiration_time_utc: 1,
621                data: "data".into(),
622            },
623            fee_payer: None,
624        };
625        let expected = json!({
626            "sendMint": "send_mint",
627            "receiveMint": "receive_mint",
628            "sendQty": "5000000",
629            "orderFlowSource": "order_flow_source",
630            "endorsement": {
631                "endorser": "endorser",
632                "signature": "signature",
633                "id": "id",
634                "expirationTimeUTC": 1,
635                "data": "data",
636            },
637        });
638        let serialized = serde_json::to_value(&obj).unwrap();
639        assert_json_eq!(serialized, expected);
640        let deserialized: IndicativeQuoteRequestParams = serde_json::from_value(expected).unwrap();
641        assert_eq!(deserialized, obj);
642    }
643
644    #[test]
645    fn test_serde_firm_quote_request() {
646        let obj = FirmQuoteRequestParams {
647            send_mint: "send_mint".into(),
648            receive_mint: "receive_mint".into(),
649            send_qty: "5000000".into(),
650            use_native_sol: Some(false),
651            order_flow_source: "order_flow_source".into(),
652            endorsement: Endorsement {
653                endorser: "endorser".into(),
654                signature: "signature".into(),
655                id: "id".into(),
656                expiration_time_utc: 1,
657                data: "data".into(),
658            },
659            fee_payer: Some(FeePayer::RetailTrader),
660        };
661        let expected = json!({
662            "sendMint": "send_mint",
663            "receiveMint": "receive_mint",
664            "sendQty": "5000000",
665            "useNativeSOL": false,
666            "orderFlowSource": "order_flow_source",
667            "endorsement": {
668                "endorser": "endorser",
669                "signature": "signature",
670                "id": "id",
671                "expirationTimeUTC": 1,
672                "data": "data",
673            },
674            "feePayer": "retailTrader",
675        });
676        let serialized = serde_json::to_value(&obj).unwrap();
677        assert_json_eq!(serialized, expected);
678        let deserialized: FirmQuoteRequestParams = serde_json::from_value(expected).unwrap();
679        assert_eq!(deserialized, obj);
680
681        let obj = FirmQuoteRequestParams {
682            fee_payer: Some(FeePayer::MarketMaker),
683            ..obj
684        };
685        let expected = json!({
686            "sendMint": "send_mint",
687            "receiveMint": "receive_mint",
688            "sendQty": "5000000",
689            "useNativeSOL": false,
690            "orderFlowSource": "order_flow_source",
691            "endorsement": {
692                "endorser": "endorser",
693                "signature": "signature",
694                "id": "id",
695                "expirationTimeUTC": 1,
696                "data": "data",
697            },
698            "feePayer": "marketMaker",
699        });
700        let serialized = serde_json::to_value(&obj).unwrap();
701        assert_json_eq!(serialized, expected);
702        let deserialized: FirmQuoteRequestParams = serde_json::from_value(expected).unwrap();
703        assert_eq!(deserialized, obj);
704    }
705
706    #[test]
707    fn test_serde_firm_quote_request_omit_none() {
708        let obj = FirmQuoteRequestParams {
709            send_mint: "send_mint".into(),
710            receive_mint: "receive_mint".into(),
711            send_qty: "5000000".into(),
712            use_native_sol: None,
713            order_flow_source: "order_flow_source".into(),
714            endorsement: Endorsement {
715                endorser: "endorser".into(),
716                signature: "signature".into(),
717                id: "id".into(),
718                expiration_time_utc: 1,
719                data: "data".into(),
720            },
721            fee_payer: None,
722        };
723        let expected = json!({
724            "sendMint": "send_mint",
725            "receiveMint": "receive_mint",
726            "sendQty": "5000000",
727            "orderFlowSource": "order_flow_source",
728            "endorsement": {
729                "endorser": "endorser",
730                "signature": "signature",
731                "id": "id",
732                "expirationTimeUTC": 1,
733                "data": "data",
734            },
735        });
736        let serialized = serde_json::to_value(&obj).unwrap();
737        assert_json_eq!(serialized, expected);
738        let deserialized: FirmQuoteRequestParams = serde_json::from_value(expected).unwrap();
739        assert_eq!(deserialized, obj);
740    }
741}