Skip to main content

hypercall_sdk_types/directives/
eip712.rs

1use alloy::{
2    primitives::{Address, Signature as PrimitiveSignature, B256, U256},
3    sol,
4    sol_types::{Eip712Domain, SolStruct},
5};
6
7pub const HYPERCALL_TESTNET_CHAIN_ID: u64 = 998;
8pub const HYPERCALL_MAINNET_CHAIN_ID: u64 = 999;
9
10pub const HYPERCALL_API_DOMAIN_NAME: &str = "HypercallApiSign";
11pub const HYPERCALL_MANAGER_DOMAIN_NAME: &str = "HypercallManagerSign";
12pub const HYPERCALL_RSM_DOMAIN_NAME: &str = "HypercallRsmSign";
13
14pub const HYPERCALL_DOMAIN_VERSION: &str = "1";
15
16pub const HL_ACTION_VERSION: u8 = 1;
17pub const HC_ACTION_VERSION: u8 = 0;
18pub const SYSTEM_ACTION_VERSION: u8 = 0xFF;
19
20pub const HL_ACTION_ID_LIMIT_ORDER: u32 = 1;
21pub const HL_ACTION_ID_CANCEL_BY_OID: u32 = 10;
22pub const HL_ACTION_ID_CANCEL_BY_CLOID: u32 = 11;
23pub const HL_ACTION_ID_SEND_ASSET: u32 = 13;
24
25pub const HC_ACTION_ID_TRANSFER_OPTION: u32 = 1;
26pub const HC_ACTION_ID_UPDATE_API_WALLET: u32 = 2;
27pub const SYSTEM_ACTION_ID_CREDIT_TOKEN: u32 = 1;
28pub const SYSTEM_ACTION_ID_CREDIT_OPTION: u32 = 2;
29pub const SYSTEM_ACTION_ID_START_LIQUIDATION: u32 = 3;
30pub const SYSTEM_ACTION_ID_STOP_LIQUIDATION: u32 = 4;
31pub const SYSTEM_ACTION_ID_WITHDRAW_TOKEN: u32 = 5;
32
33pub const HL_TIF_ALO: u8 = 1;
34pub const HL_TIF_GTC: u8 = 2;
35pub const HL_TIF_IOC: u8 = 3;
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub enum DirectiveError {
39    UnsupportedChainId(u64),
40    InvalidSignatureLength(usize),
41    InvalidSignatureV(u8),
42    SignatureRecovery(String),
43}
44
45impl std::fmt::Display for DirectiveError {
46    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47        match self {
48            Self::UnsupportedChainId(chain_id) => {
49                write!(f, "Unsupported directive chain id: {}", chain_id)
50            }
51            Self::InvalidSignatureLength(len) => {
52                write!(
53                    f,
54                    "Invalid signature length: expected 65 bytes, got {}",
55                    len
56                )
57            }
58            Self::InvalidSignatureV(v) => {
59                write!(f, "Invalid signature v value: {} (expected 0,1,27,28)", v)
60            }
61            Self::SignatureRecovery(msg) => write!(f, "Signature recovery failed: {}", msg),
62        }
63    }
64}
65
66impl std::error::Error for DirectiveError {}
67
68sol! {
69    #[derive(Debug, PartialEq, Eq)]
70    struct LimitOrder {
71        uint32 asset;
72        bool isBuy;
73        uint64 limitPx;
74        uint64 sz;
75        bool reduceOnly;
76        uint8 encodedTif;
77        uint128 cloid;
78    }
79
80    #[derive(Debug, PartialEq, Eq)]
81    struct CancelOrderByOid {
82        uint32 asset;
83        uint64 oid;
84    }
85
86    #[derive(Debug, PartialEq, Eq)]
87    struct CancelOrderByCloid {
88        uint32 asset;
89        uint128 cloid;
90    }
91
92    #[derive(Debug, PartialEq, Eq)]
93    struct SendAsset {
94        address destination;
95        address subAccount;
96        uint32 srcDex;
97        uint32 dstDex;
98        uint64 token;
99        uint64 amountWei;
100    }
101
102    #[derive(Debug, PartialEq, Eq)]
103    struct UpdateApiWallet {
104        bytes32 name;
105        address addr;
106    }
107
108    #[derive(Debug, PartialEq, Eq)]
109    struct TransferOption {
110        address recipient;
111        address option;
112        uint256 amountWei;
113    }
114
115    #[derive(Debug, PartialEq, Eq)]
116    struct CreditToken {
117        uint32 srcDex;
118        uint32 dstDex;
119        uint64 token;
120        uint64 amountWei;
121    }
122
123    #[derive(Debug, PartialEq, Eq)]
124    struct CreditOption {
125        bytes32 underlying;
126        uint256 expiry;
127        uint256 strike;
128        bool isCall;
129        uint256 amountWei;
130    }
131
132    #[derive(Debug, PartialEq, Eq)]
133    struct HLOrder {
134        address account;
135        uint64 nonce;
136        LimitOrder action;
137    }
138
139    #[derive(Debug, PartialEq, Eq)]
140    struct HLCancel {
141        address account;
142        uint64 nonce;
143        CancelOrderByOid action;
144    }
145
146    #[derive(Debug, PartialEq, Eq)]
147    struct HLCancelByCloid {
148        address account;
149        uint64 nonce;
150        CancelOrderByCloid action;
151    }
152
153    #[derive(Debug, PartialEq, Eq)]
154    struct HLSendAsset {
155        address account;
156        uint64 nonce;
157        SendAsset action;
158    }
159
160    #[derive(Debug, PartialEq, Eq)]
161    struct HCUpdateApiWallet {
162        address account;
163        uint64 nonce;
164        UpdateApiWallet action;
165    }
166
167    #[derive(Debug, PartialEq, Eq)]
168    struct HCTransferOption {
169        address account;
170        uint64 nonce;
171        TransferOption action;
172    }
173
174    #[derive(Debug, PartialEq, Eq)]
175    struct StartLiquidation {
176        int256 equity;
177        uint256 marginNeeded;
178    }
179
180    #[derive(Debug, PartialEq, Eq)]
181    struct StopLiquidation {
182        uint256 startTime;
183    }
184
185    #[derive(Debug, PartialEq, Eq)]
186    struct WithdrawToken {
187        address destination;
188        address subAccount;
189        uint32 srcDex;
190        uint32 dstDex;
191        uint64 token;
192        uint64 amountWei;
193    }
194
195    #[derive(Debug, PartialEq, Eq)]
196    struct SystemStartLiquidation {
197        address account;
198        uint64 nonce;
199        StartLiquidation action;
200    }
201
202    #[derive(Debug, PartialEq, Eq)]
203    struct SystemCreditToken {
204        address account;
205        uint64 nonce;
206        CreditToken action;
207    }
208
209    #[derive(Debug, PartialEq, Eq)]
210    struct SystemCreditOption {
211        address account;
212        uint64 nonce;
213        CreditOption action;
214    }
215
216    #[derive(Debug, PartialEq, Eq)]
217    struct SystemStopLiquidation {
218        address account;
219        uint64 nonce;
220        StopLiquidation action;
221    }
222
223    #[derive(Debug, PartialEq, Eq)]
224    struct SystemWithdrawToken {
225        address account;
226        uint64 nonce;
227        WithdrawToken action;
228    }
229}
230
231fn validate_chain_id(chain_id: u64) -> Result<(), DirectiveError> {
232    if chain_id == HYPERCALL_TESTNET_CHAIN_ID || chain_id == HYPERCALL_MAINNET_CHAIN_ID {
233        Ok(())
234    } else {
235        Err(DirectiveError::UnsupportedChainId(chain_id))
236    }
237}
238
239fn directive_domain(name: &'static str, chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
240    validate_chain_id(chain_id)?;
241    Ok(Eip712Domain {
242        name: Some(name.into()),
243        version: Some(HYPERCALL_DOMAIN_VERSION.into()),
244        chain_id: Some(U256::from(chain_id)),
245        verifying_contract: Some(Address::ZERO),
246        salt: None,
247    })
248}
249
250pub fn hypercall_api_domain(chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
251    directive_domain(HYPERCALL_API_DOMAIN_NAME, chain_id)
252}
253
254pub fn hypercall_manager_domain(chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
255    directive_domain(HYPERCALL_MANAGER_DOMAIN_NAME, chain_id)
256}
257
258pub fn hypercall_rsm_domain(chain_id: u64) -> Result<Eip712Domain, DirectiveError> {
259    directive_domain(HYPERCALL_RSM_DOMAIN_NAME, chain_id)
260}
261
262pub fn recover_address_from_signature_bytes(
263    signature: &[u8],
264    signing_hash: &[u8; 32],
265) -> Result<Address, DirectiveError> {
266    if signature.len() != 65 {
267        return Err(DirectiveError::InvalidSignatureLength(signature.len()));
268    }
269
270    let parity = match signature[64] {
271        0 | 27 => false,
272        1 | 28 => true,
273        v => return Err(DirectiveError::InvalidSignatureV(v)),
274    };
275
276    let signature = PrimitiveSignature::from_bytes_and_parity(&signature[..64], parity);
277    signature
278        .recover_address_from_prehash(&B256::from(*signing_hash))
279        .map_err(|e| DirectiveError::SignatureRecovery(e.to_string()))
280}
281
282pub fn recover_hl_order_signer(
283    account: Address,
284    nonce: u64,
285    action: LimitOrder,
286    signature: &[u8],
287    chain_id: u64,
288) -> Result<Address, DirectiveError> {
289    let message = HLOrder {
290        account,
291        nonce,
292        action,
293    };
294    let domain = hypercall_api_domain(chain_id)?;
295    let signing_hash = message.eip712_signing_hash(&domain);
296    recover_address_from_signature_bytes(signature, &signing_hash)
297}
298
299pub fn recover_hl_cancel_by_oid_signer(
300    account: Address,
301    nonce: u64,
302    action: CancelOrderByOid,
303    signature: &[u8],
304    chain_id: u64,
305) -> Result<Address, DirectiveError> {
306    let message = HLCancel {
307        account,
308        nonce,
309        action,
310    };
311    let domain = hypercall_api_domain(chain_id)?;
312    let signing_hash = message.eip712_signing_hash(&domain);
313    recover_address_from_signature_bytes(signature, &signing_hash)
314}
315
316pub fn recover_hl_cancel_by_cloid_signer(
317    account: Address,
318    nonce: u64,
319    action: CancelOrderByCloid,
320    signature: &[u8],
321    chain_id: u64,
322) -> Result<Address, DirectiveError> {
323    let message = HLCancelByCloid {
324        account,
325        nonce,
326        action,
327    };
328    let domain = hypercall_api_domain(chain_id)?;
329    let signing_hash = message.eip712_signing_hash(&domain);
330    recover_address_from_signature_bytes(signature, &signing_hash)
331}
332
333pub fn recover_hc_update_api_wallet_signer(
334    account: Address,
335    nonce: u64,
336    action: UpdateApiWallet,
337    signature: &[u8],
338    chain_id: u64,
339) -> Result<Address, DirectiveError> {
340    let message = HCUpdateApiWallet {
341        account,
342        nonce,
343        action,
344    };
345    let domain = hypercall_manager_domain(chain_id)?;
346    let signing_hash = message.eip712_signing_hash(&domain);
347    recover_address_from_signature_bytes(signature, &signing_hash)
348}
349
350pub fn recover_rsm_hl_order_signer(
351    account: Address,
352    nonce: u64,
353    action: LimitOrder,
354    signature: &[u8],
355    chain_id: u64,
356) -> Result<Address, DirectiveError> {
357    let message = HLOrder {
358        account,
359        nonce,
360        action,
361    };
362    let domain = hypercall_rsm_domain(chain_id)?;
363    let signing_hash = message.eip712_signing_hash(&domain);
364    recover_address_from_signature_bytes(signature, &signing_hash)
365}
366
367pub fn recover_rsm_hl_cancel_by_oid_signer(
368    account: Address,
369    nonce: u64,
370    action: CancelOrderByOid,
371    signature: &[u8],
372    chain_id: u64,
373) -> Result<Address, DirectiveError> {
374    let message = HLCancel {
375        account,
376        nonce,
377        action,
378    };
379    let domain = hypercall_rsm_domain(chain_id)?;
380    let signing_hash = message.eip712_signing_hash(&domain);
381    recover_address_from_signature_bytes(signature, &signing_hash)
382}
383
384pub fn recover_rsm_hl_cancel_by_cloid_signer(
385    account: Address,
386    nonce: u64,
387    action: CancelOrderByCloid,
388    signature: &[u8],
389    chain_id: u64,
390) -> Result<Address, DirectiveError> {
391    let message = HLCancelByCloid {
392        account,
393        nonce,
394        action,
395    };
396    let domain = hypercall_rsm_domain(chain_id)?;
397    let signing_hash = message.eip712_signing_hash(&domain);
398    recover_address_from_signature_bytes(signature, &signing_hash)
399}
400
401pub fn recover_rsm_hl_send_asset_signer(
402    account: Address,
403    nonce: u64,
404    action: SendAsset,
405    signature: &[u8],
406    chain_id: u64,
407) -> Result<Address, DirectiveError> {
408    let message = HLSendAsset {
409        account,
410        nonce,
411        action,
412    };
413    let domain = hypercall_rsm_domain(chain_id)?;
414    let signing_hash = message.eip712_signing_hash(&domain);
415    recover_address_from_signature_bytes(signature, &signing_hash)
416}
417
418pub fn recover_system_credit_token_signer(
419    account: Address,
420    nonce: u64,
421    action: CreditToken,
422    signature: &[u8],
423    chain_id: u64,
424) -> Result<Address, DirectiveError> {
425    let message = SystemCreditToken {
426        account,
427        nonce,
428        action,
429    };
430    let domain = hypercall_rsm_domain(chain_id)?;
431    let signing_hash = message.eip712_signing_hash(&domain);
432    recover_address_from_signature_bytes(signature, &signing_hash)
433}
434
435pub fn recover_system_credit_option_signer(
436    account: Address,
437    nonce: u64,
438    action: CreditOption,
439    signature: &[u8],
440    chain_id: u64,
441) -> Result<Address, DirectiveError> {
442    let message = SystemCreditOption {
443        account,
444        nonce,
445        action,
446    };
447    let domain = hypercall_rsm_domain(chain_id)?;
448    let signing_hash = message.eip712_signing_hash(&domain);
449    recover_address_from_signature_bytes(signature, &signing_hash)
450}
451
452pub fn recover_system_start_liquidation_signer(
453    account: Address,
454    nonce: u64,
455    action: StartLiquidation,
456    signature: &[u8],
457    chain_id: u64,
458) -> Result<Address, DirectiveError> {
459    let message = SystemStartLiquidation {
460        account,
461        nonce,
462        action,
463    };
464    let domain = hypercall_rsm_domain(chain_id)?;
465    let signing_hash = message.eip712_signing_hash(&domain);
466    recover_address_from_signature_bytes(signature, &signing_hash)
467}
468
469pub fn recover_system_stop_liquidation_signer(
470    account: Address,
471    nonce: u64,
472    action: StopLiquidation,
473    signature: &[u8],
474    chain_id: u64,
475) -> Result<Address, DirectiveError> {
476    let message = SystemStopLiquidation {
477        account,
478        nonce,
479        action,
480    };
481    let domain = hypercall_rsm_domain(chain_id)?;
482    let signing_hash = message.eip712_signing_hash(&domain);
483    recover_address_from_signature_bytes(signature, &signing_hash)
484}
485
486pub fn recover_system_withdraw_token_signer(
487    account: Address,
488    nonce: u64,
489    action: WithdrawToken,
490    signature: &[u8],
491    chain_id: u64,
492) -> Result<Address, DirectiveError> {
493    let message = SystemWithdrawToken {
494        account,
495        nonce,
496        action,
497    };
498    let domain = hypercall_rsm_domain(chain_id)?;
499    let signing_hash = message.eip712_signing_hash(&domain);
500    recover_address_from_signature_bytes(signature, &signing_hash)
501}
502
503#[cfg(test)]
504mod tests {
505    use super::*;
506    use alloy::primitives::{address, keccak256, FixedBytes};
507    use alloy::sol_types::SolStruct;
508
509    #[test]
510    fn rsm_domain_separator_constants_match_contracts() {
511        let testnet = hypercall_rsm_domain(HYPERCALL_TESTNET_CHAIN_ID).unwrap();
512        let mainnet = hypercall_rsm_domain(HYPERCALL_MAINNET_CHAIN_ID).unwrap();
513
514        assert_eq!(
515            format!("{:#x}", testnet.hash_struct()),
516            "0x650b282053fb61d3fd477bdc28f6434311fe905e27cc4ca643e87e802c45938c"
517        );
518        assert_eq!(
519            format!("{:#x}", mainnet.hash_struct()),
520            "0x3d0cae2af623c614099dbadd67a1e1457fabde576aa270a70a57e39bf338a7be"
521        );
522    }
523
524    #[test]
525    fn rsm_type_hashes_match_contract_vectors() {
526        let account = address!("0x1111111111111111111111111111111111111111");
527
528        let credit_token = CreditToken {
529            srcDex: 1,
530            dstDex: 2,
531            token: 3,
532            amountWei: 4,
533        };
534        let credit_option = CreditOption {
535            underlying: FixedBytes::<32>::ZERO,
536            expiry: U256::from(1),
537            strike: U256::from(2),
538            isCall: true,
539            amountWei: U256::from(3),
540        };
541        let start = StartLiquidation {
542            equity: alloy::primitives::I256::try_from(-1_i128).unwrap(),
543            marginNeeded: U256::from(2),
544        };
545        let stop = StopLiquidation {
546            startTime: U256::from(1),
547        };
548        let order = LimitOrder {
549            asset: 1,
550            isBuy: true,
551            limitPx: 2,
552            sz: 3,
553            reduceOnly: false,
554            encodedTif: 2,
555            cloid: 4,
556        };
557        let cancel_oid = CancelOrderByOid { asset: 1, oid: 2 };
558        let cancel_cloid = CancelOrderByCloid { asset: 1, cloid: 2 };
559        let send_asset = SendAsset {
560            destination: account,
561            subAccount: Address::ZERO,
562            srcDex: 1,
563            dstDex: 2,
564            token: 3,
565            amountWei: 4,
566        };
567
568        let cases = [
569            (
570                keccak256(CreditToken::eip712_encode_type().as_bytes()),
571                "0xb3df0c3aecbb057482a169cb5a282d13cc163a21c12ee21da37e6b2b646312ba",
572            ),
573            (
574                SystemCreditToken {
575                    account,
576                    nonce: 1,
577                    action: credit_token,
578                }
579                .eip712_type_hash(),
580                "0xe845f2049264182802994dae939dc8ac42d3ae543a7eeb530dd021428743f052",
581            ),
582            (
583                keccak256(CreditOption::eip712_encode_type().as_bytes()),
584                "0x60662ff4c34f920ea34056c513435d0aeafe81ad26d5f76bb26e1bf1608b910b",
585            ),
586            (
587                SystemCreditOption {
588                    account,
589                    nonce: 1,
590                    action: credit_option,
591                }
592                .eip712_type_hash(),
593                "0x65aedd5af06882e1e62d752b43b72ec62a85cacf2ca59e8ec2affc57eed9d8ae",
594            ),
595            (
596                keccak256(StartLiquidation::eip712_encode_type().as_bytes()),
597                "0x322d6003c0c25354ccfe01c4c703c29f2d51892c2995c0ae32aea305bbd85086",
598            ),
599            (
600                SystemStartLiquidation {
601                    account,
602                    nonce: 1,
603                    action: start,
604                }
605                .eip712_type_hash(),
606                "0x92b6a135a656e489c24fb5511684842b0ab3895c22b51e4910dee90a7d23511e",
607            ),
608            (
609                keccak256(StopLiquidation::eip712_encode_type().as_bytes()),
610                "0xc22a843839eef1d6531ff7299ff6e1cd92160be18e3b9d6f99dec5f14cdfd7b2",
611            ),
612            (
613                SystemStopLiquidation {
614                    account,
615                    nonce: 1,
616                    action: stop,
617                }
618                .eip712_type_hash(),
619                "0xe1af98778e9cfd1d1a679bb0b64b6dc8aa3fb152ff6469d0a81a1c7eb87a4bd3",
620            ),
621            (
622                keccak256(LimitOrder::eip712_encode_type().as_bytes()),
623                "0xcc39512ca4619253e6a3650a478ceb1caa927f3429dd70c23e343970ca89d904",
624            ),
625            (
626                HLOrder {
627                    account,
628                    nonce: 1,
629                    action: order,
630                }
631                .eip712_type_hash(),
632                "0xe6facb816c5f9c321054ef43c7d8fb1ec750f65d630ec39c5c18119a6f4707dd",
633            ),
634            (
635                keccak256(CancelOrderByOid::eip712_encode_type().as_bytes()),
636                "0xa623d44292da4a773c6bda0a721c370128f7bdfa31315a7799840c9c2b54b896",
637            ),
638            (
639                HLCancel {
640                    account,
641                    nonce: 1,
642                    action: cancel_oid,
643                }
644                .eip712_type_hash(),
645                "0x23c5529f34a875b6c0d11a6dfafdd945996dbdc5fbd1bdace2c2e0cf6a253a85",
646            ),
647            (
648                keccak256(CancelOrderByCloid::eip712_encode_type().as_bytes()),
649                "0x66c29d69ed49e1f99d3a57d9a4ff80248962ccbf1553fc864a3dd38436ded220",
650            ),
651            (
652                HLCancelByCloid {
653                    account,
654                    nonce: 1,
655                    action: cancel_cloid,
656                }
657                .eip712_type_hash(),
658                "0x10cbcfa56297fc737e4defee7b7937a52978ebbe5920e584317f7f25416836f5",
659            ),
660            (
661                keccak256(SendAsset::eip712_encode_type().as_bytes()),
662                "0x9900fb5faaa28fcaadbe37dc0bc284a05049ae8b10b5b83daef5e701c10e1569",
663            ),
664            (
665                HLSendAsset {
666                    account,
667                    nonce: 1,
668                    action: send_asset,
669                }
670                .eip712_type_hash(),
671                "0x7feffdf7e7b191d5d27c2a209ca71c789289da73de203e99546abf9632860595",
672            ),
673        ];
674
675        for (actual, expected) in cases {
676            assert_eq!(format!("{:#x}", actual), expected);
677        }
678    }
679}