Skip to main content

df_hyperliquid_core/client/evm/
corewriter.rs

1//! CoreWriter, the write path from HyperEVM to HyperCore: a system contract
2//! at `0x3333...3333` (`CoreWriter.sol` in the docs):
3//!
4//! ```text
5//! event RawAction(address indexed user, bytes data);
6//! function sendRawAction(bytes calldata data) external;
7//! ```
8//!
9//! `data` is the encoding version (one byte, 1), the action id (a big-endian
10//! `uint24`), then the action's fields ABI-encoded with no selector.
11//!
12//! - The action acts for `msg.sender`: an agent key sending it acts for the
13//!   agent's own account, never for the account it trades for.
14//! - The transaction's value must be 0: value voids the action silently, with
15//!   no log.
16//! - Orders and vault transfers are delayed on chain by a few seconds.
17//!
18//! Units: `limitPx` and `sz` are 1e8 whatever the market; `usd` and `ntl`
19//! are perp USDC at 1e6; staking and delegation `wei` are HYPE at 1e8; any
20//! other `wei` is in the token's `weiDecimals`; `maxFeeRate` is in tenths of a
21//! basis point (10 is 0.01%).
22
23use crate::client::Network;
24use crate::client::evm::abi::{self, AbiError, Token};
25use crate::client::evm::records::Log;
26use crate::client::evm::rpc::{BlockTag, LogFilter};
27use crate::client::evm::tx::{Call, Eip1559, Fees};
28use crate::client::evm::u256::{B256, U256};
29use crate::utils::{Address, BadUnits, Dec, exact_units};
30
31/// The CoreWriter system contract.
32pub const CORE_WRITER: Address = Address::from_bytes([0x33; 20]);
33
34/// `keccak256("RawAction(address,bytes)")`, the topic every action is logged under.
35pub const RAW_ACTION_TOPIC: B256 = B256([
36    0x8c, 0x7f, 0x58, 0x5f, 0xb2, 0x95, 0xf7, 0xeb, 0x1e, 0x6a, 0xeb, 0x8f, 0xba, 0x61, 0xb2, 0x3a, 0x4f, 0xe6, 0x0b,
37    0xed, 0xa4, 0x05, 0xf0, 0x04, 0x50, 0x73, 0xb1, 0x85, 0xc7, 0x44, 0x12, 0xe3,
38]);
39
40/// `sendRawAction(bytes)`.
41pub const SEND_RAW_ACTION: [u8; 4] = [0x17, 0x93, 0x8e, 0x13];
42
43/// The only encoding version the docs give.
44pub const ENCODING_VERSION: u8 = 1;
45
46/// The one id the docs' table skips.
47pub const GAP: u32 = 14;
48
49/// `limitPx` and `sz`: 1e8 times what a person reads, whatever the market.
50pub const ORDER_DECIMALS: u32 = 8;
51/// `usd` and `ntl`: perp USDC.
52pub const USD_DECIMALS: u32 = 6;
53/// Staking and delegation `wei`: HYPE.
54pub const HYPE_DECIMALS: u32 = 8;
55/// `maxFeeRate` in tenths of a basis point, to a percent.
56pub const DECIBPS_DECIMALS: u32 = 3;
57/// The dex id `sendAsset` gives spot.
58pub const SPOT_DEX: u32 = u32::MAX;
59
60/// One row of the docs' action table.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct Spec {
63    pub id: u32,
64    pub name: &'static str,
65    /// The field names, in order, as the docs give them.
66    pub fields: &'static [&'static str],
67    /// The Solidity types, as the docs give them.
68    pub types: &'static str,
69}
70
71/// The docs' table, every id it has.
72pub const SPECS: [Spec; 16] = [
73    Spec {
74        id: 1,
75        name: "Limit order",
76        fields: &["asset", "isBuy", "limitPx", "sz", "reduceOnly", "encodedTif", "cloid"],
77        types: "uint32, bool, uint64, uint64, bool, uint8, uint128",
78    },
79    Spec { id: 2, name: "Vault transfer", fields: &["vault", "isDeposit", "usd"], types: "address, bool, uint64" },
80    Spec {
81        id: 3,
82        name: "Token delegate",
83        fields: &["validator", "wei", "isUndelegate"],
84        types: "address, uint64, bool",
85    },
86    Spec { id: 4, name: "Staking deposit", fields: &["wei"], types: "uint64" },
87    Spec { id: 5, name: "Staking withdraw", fields: &["wei"], types: "uint64" },
88    Spec { id: 6, name: "Spot send", fields: &["destination", "token", "wei"], types: "address, uint64, uint64" },
89    Spec { id: 7, name: "USD class transfer", fields: &["ntl", "toPerp"], types: "uint64, bool" },
90    Spec {
91        id: 8,
92        name: "Finalize EVM contract",
93        fields: &["token", "encodedFinalizeEvmContractVariant", "createNonce"],
94        types: "uint64, uint8, uint64",
95    },
96    Spec { id: 9, name: "Add API wallet", fields: &["wallet", "name"], types: "address, string" },
97    Spec { id: 10, name: "Cancel order by oid", fields: &["asset", "oid"], types: "uint32, uint64" },
98    Spec { id: 11, name: "Cancel order by cloid", fields: &["asset", "cloid"], types: "uint32, uint128" },
99    Spec { id: 12, name: "Approve builder fee", fields: &["maxFeeRate", "builder"], types: "uint64, address" },
100    Spec {
101        id: 13,
102        name: "Send asset",
103        fields: &["destination", "subAccount", "source_dex", "destination_dex", "token", "wei"],
104        types: "address, address, uint32, uint32, uint64, uint64",
105    },
106    Spec {
107        id: 15,
108        name: "Borrow lend operation",
109        fields: &["encodedOperation", "token", "wei"],
110        types: "uint8, uint64, uint64",
111    },
112    Spec { id: 16, name: "Set abstraction", fields: &["user", "abstraction"], types: "address, uint8" },
113    Spec {
114        id: 17,
115        name: "Outcome operation",
116        fields: &["encodedOperation", "question", "outcome", "wei"],
117        types: "uint8, uint32, uint32, uint64",
118    },
119];
120
121/// The table's row for `id`.
122#[must_use]
123pub fn spec(id: u32) -> Option<&'static Spec> {
124    SPECS.iter().find(|spec| spec.id == id)
125}
126
127/// A limit order's time in force.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum Tif {
130    Alo,
131    Gtc,
132    Ioc,
133    /// A value the docs do not name, kept as it came.
134    Other(u8),
135}
136
137impl Tif {
138    #[must_use]
139    pub const fn code(self) -> u8 {
140        match self {
141            Tif::Alo => 1,
142            Tif::Gtc => 2,
143            Tif::Ioc => 3,
144            Tif::Other(code) => code,
145        }
146    }
147
148    #[must_use]
149    pub const fn from_code(code: u8) -> Tif {
150        match code {
151            1 => Tif::Alo,
152            2 => Tif::Gtc,
153            3 => Tif::Ioc,
154            other => Tif::Other(other),
155        }
156    }
157}
158
159/// How the deployer of an ERC-20 proves it to finalize the link (action 8).
160#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum FinalizeVariant {
162    /// The deploying address, with the nonce it deployed at.
163    Create,
164    /// The address in the contract's first storage slot.
165    FirstStorageSlot,
166    /// The address in the slot at `keccak256("HyperCore deployer")`.
167    CustomStorageSlot,
168    Other(u8),
169}
170
171impl FinalizeVariant {
172    #[must_use]
173    pub const fn code(self) -> u8 {
174        match self {
175            FinalizeVariant::Create => 1,
176            FinalizeVariant::FirstStorageSlot => 2,
177            FinalizeVariant::CustomStorageSlot => 3,
178            FinalizeVariant::Other(code) => code,
179        }
180    }
181
182    #[must_use]
183    pub const fn from_code(code: u8) -> FinalizeVariant {
184        match code {
185            1 => FinalizeVariant::Create,
186            2 => FinalizeVariant::FirstStorageSlot,
187            3 => FinalizeVariant::CustomStorageSlot,
188            other => FinalizeVariant::Other(other),
189        }
190    }
191}
192
193/// A lending operation (action 15). The docs give supply and withdraw; on
194/// chain (observed 2026-08-21), 2 routes as borrow and 3 as repay, and 4, 5
195/// and 255 are discarded.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum BorrowLendOp {
198    Supply,
199    Withdraw,
200    /// Undocumented.
201    Borrow,
202    /// Undocumented.
203    Repay,
204    Other(u8),
205}
206
207impl BorrowLendOp {
208    #[must_use]
209    pub const fn code(self) -> u8 {
210        match self {
211            BorrowLendOp::Supply => 0,
212            BorrowLendOp::Withdraw => 1,
213            BorrowLendOp::Borrow => 2,
214            BorrowLendOp::Repay => 3,
215            BorrowLendOp::Other(code) => code,
216        }
217    }
218
219    #[must_use]
220    pub const fn from_code(code: u8) -> BorrowLendOp {
221        match code {
222            0 => BorrowLendOp::Supply,
223            1 => BorrowLendOp::Withdraw,
224            2 => BorrowLendOp::Borrow,
225            3 => BorrowLendOp::Repay,
226            other => BorrowLendOp::Other(other),
227        }
228    }
229}
230
231/// An account mode (action 16), for the master account or a subaccount.
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum Abstraction {
234    Disabled,
235    UnifiedAccount,
236    PortfolioMargin,
237    Other(u8),
238}
239
240impl Abstraction {
241    #[must_use]
242    pub const fn code(self) -> u8 {
243        match self {
244            Abstraction::Disabled => 1,
245            Abstraction::UnifiedAccount => 2,
246            Abstraction::PortfolioMargin => 3,
247            Abstraction::Other(code) => code,
248        }
249    }
250
251    #[must_use]
252    pub const fn from_code(code: u8) -> Abstraction {
253        match code {
254            1 => Abstraction::Disabled,
255            2 => Abstraction::UnifiedAccount,
256            3 => Abstraction::PortfolioMargin,
257            other => Abstraction::Other(other),
258        }
259    }
260}
261
262/// A HIP-4 operation (action 17). Unused fields must be zero, or HyperCore
263/// drops the payload without a word: split and merge ignore `question`,
264/// merge question ignores `outcome`, negate uses both.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum OutcomeOp {
267    SplitOutcome,
268    MergeOutcome,
269    MergeQuestion,
270    NegateOutcome,
271    Other(u8),
272}
273
274impl OutcomeOp {
275    #[must_use]
276    pub const fn code(self) -> u8 {
277        match self {
278            OutcomeOp::SplitOutcome => 0,
279            OutcomeOp::MergeOutcome => 1,
280            OutcomeOp::MergeQuestion => 2,
281            OutcomeOp::NegateOutcome => 3,
282            OutcomeOp::Other(code) => code,
283        }
284    }
285
286    #[must_use]
287    pub const fn from_code(code: u8) -> OutcomeOp {
288        match code {
289            0 => OutcomeOp::SplitOutcome,
290            1 => OutcomeOp::MergeOutcome,
291            2 => OutcomeOp::MergeQuestion,
292            3 => OutcomeOp::NegateOutcome,
293            other => OutcomeOp::Other(other),
294        }
295    }
296}
297
298/// Every action in the docs' table, its fields in their units.
299#[derive(Debug, Clone, PartialEq, Eq)]
300pub enum Action {
301    /// Action 1. `cloid` 0 is none.
302    LimitOrder { asset: u32, is_buy: bool, limit_px: u64, sz: u64, reduce_only: bool, tif: Tif, cloid: u128 },
303    /// Action 2.
304    VaultTransfer { vault: Address, is_deposit: bool, usd: u64 },
305    /// Action 3.
306    TokenDelegate { validator: Address, wei: u64, is_undelegate: bool },
307    /// Action 4.
308    StakingDeposit { wei: u64 },
309    /// Action 5.
310    StakingWithdraw { wei: u64 },
311    /// Action 6.
312    SpotSend { destination: Address, token: u64, wei: u64 },
313    /// Action 7.
314    UsdClassTransfer { ntl: u64, to_perp: bool },
315    /// Action 8. `create_nonce` is read only by [`FinalizeVariant::Create`].
316    FinalizeEvmContract { token: u64, variant: FinalizeVariant, create_nonce: u64 },
317    /// Action 9. An empty name makes the wallet the main, unnamed agent.
318    AddApiWallet { wallet: Address, name: String },
319    /// Action 10.
320    CancelByOid { asset: u32, oid: u64 },
321    /// Action 11.
322    CancelByCloid { asset: u32, cloid: u128 },
323    /// Action 12.
324    ApproveBuilderFee { max_fee_rate: u64, builder: Address },
325    /// Action 13. A non-zero `sub_account` sends from that subaccount; a dex of
326    /// [`SPOT_DEX`] is spot.
327    SendAsset {
328        destination: Address,
329        sub_account: Address,
330        source_dex: u32,
331        destination_dex: u32,
332        token: u64,
333        wei: u64,
334    },
335    /// Action 15. `wei` 0 applies the operation to the most there is.
336    BorrowLend { operation: BorrowLendOp, token: u64, wei: u64 },
337    /// Action 16.
338    SetAbstraction { user: Address, abstraction: Abstraction },
339    /// Action 17. `wei` must be a multiple of the size; 0 merges the most there is.
340    Outcome { operation: OutcomeOp, question: u32, outcome: u32, wei: u64 },
341}
342
343/// Why bytes are not a CoreWriter action this reads.
344#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
345pub enum BadAction {
346    #[error("not a RawAction log of CoreWriter's")]
347    NotRawAction,
348    #[error("{0} bytes is shorter than the four-byte header")]
349    Short(usize),
350    #[error("encoding version {0}; the docs give only 1")]
351    Version(u8),
352    #[error("action 14, the gap in the docs' table; it has no published meaning")]
353    Gap,
354    #[error("action {0} is not in the docs' table")]
355    UnknownId(u32),
356    #[error(transparent)]
357    Abi(#[from] AbiError),
358}
359
360/// One field's encoding: a word, or text behind an offset.
361enum Field {
362    Word([u8; 32]),
363    Text(Vec<u8>),
364}
365
366fn word(value: impl Into<U256>) -> Field {
367    Field::Word(value.into().to_be_bytes())
368}
369
370fn address(address: Address) -> Field {
371    Field::Word(B256::from_address(address).0)
372}
373
374fn flag(value: bool) -> Field {
375    word(u8::from(value))
376}
377
378/// `abi.encode` of the fields: heads, then the tail of the one string.
379fn encode(fields: &[Field]) -> Vec<u8> {
380    let heads = fields.len() * 32;
381    let mut head = Vec::with_capacity(heads);
382    let mut tail = Vec::new();
383    for field in fields {
384        match field {
385            Field::Word(word) => head.extend_from_slice(word),
386            Field::Text(bytes) => {
387                head.extend_from_slice(&U256::from_u64((heads + tail.len()) as u64).to_be_bytes());
388                tail.extend_from_slice(&U256::from_u64(bytes.len() as u64).to_be_bytes());
389                tail.extend_from_slice(bytes);
390                tail.resize(tail.len().div_ceil(32) * 32, 0);
391            }
392        }
393    }
394    head.extend(tail);
395    head
396}
397
398impl Action {
399    #[must_use]
400    pub const fn id(&self) -> u32 {
401        match self {
402            Action::LimitOrder { .. } => 1,
403            Action::VaultTransfer { .. } => 2,
404            Action::TokenDelegate { .. } => 3,
405            Action::StakingDeposit { .. } => 4,
406            Action::StakingWithdraw { .. } => 5,
407            Action::SpotSend { .. } => 6,
408            Action::UsdClassTransfer { .. } => 7,
409            Action::FinalizeEvmContract { .. } => 8,
410            Action::AddApiWallet { .. } => 9,
411            Action::CancelByOid { .. } => 10,
412            Action::CancelByCloid { .. } => 11,
413            Action::ApproveBuilderFee { .. } => 12,
414            Action::SendAsset { .. } => 13,
415            Action::BorrowLend { .. } => 15,
416            Action::SetAbstraction { .. } => 16,
417            Action::Outcome { .. } => 17,
418        }
419    }
420
421    /// The docs' row for this action.
422    #[must_use]
423    pub fn spec(&self) -> &'static Spec {
424        let id = self.id();
425        SPECS.iter().find(|spec| spec.id == id).unwrap_or(&SPECS[0])
426    }
427
428    fn fields(&self) -> Vec<Field> {
429        match self {
430            Action::LimitOrder { asset, is_buy, limit_px, sz, reduce_only, tif, cloid } => vec![
431                word(*asset),
432                flag(*is_buy),
433                word(*limit_px),
434                word(*sz),
435                flag(*reduce_only),
436                word(tif.code()),
437                word(*cloid),
438            ],
439            Action::VaultTransfer { vault, is_deposit, usd } => vec![address(*vault), flag(*is_deposit), word(*usd)],
440            Action::TokenDelegate { validator, wei, is_undelegate } => {
441                vec![address(*validator), word(*wei), flag(*is_undelegate)]
442            }
443            Action::StakingDeposit { wei } | Action::StakingWithdraw { wei } => vec![word(*wei)],
444            Action::SpotSend { destination, token, wei } => vec![address(*destination), word(*token), word(*wei)],
445            Action::UsdClassTransfer { ntl, to_perp } => vec![word(*ntl), flag(*to_perp)],
446            Action::FinalizeEvmContract { token, variant, create_nonce } => {
447                vec![word(*token), word(variant.code()), word(*create_nonce)]
448            }
449            Action::AddApiWallet { wallet, name } => vec![address(*wallet), Field::Text(name.as_bytes().to_vec())],
450            Action::CancelByOid { asset, oid } => vec![word(*asset), word(*oid)],
451            Action::CancelByCloid { asset, cloid } => vec![word(*asset), word(*cloid)],
452            Action::ApproveBuilderFee { max_fee_rate, builder } => vec![word(*max_fee_rate), address(*builder)],
453            Action::SendAsset { destination, sub_account, source_dex, destination_dex, token, wei } => vec![
454                address(*destination),
455                address(*sub_account),
456                word(*source_dex),
457                word(*destination_dex),
458                word(*token),
459                word(*wei),
460            ],
461            Action::BorrowLend { operation, token, wei } => vec![word(operation.code()), word(*token), word(*wei)],
462            Action::SetAbstraction { user, abstraction } => vec![address(*user), word(abstraction.code())],
463            Action::Outcome { operation, question, outcome, wei } => {
464                vec![word(operation.code()), word(*question), word(*outcome), word(*wei)]
465            }
466        }
467    }
468
469    /// The `data` of `sendRawAction`: the version, the id in three bytes, then
470    /// the fields.
471    #[must_use]
472    pub fn payload(&self) -> Vec<u8> {
473        let mut out = vec![ENCODING_VERSION];
474        out.extend_from_slice(&self.id().to_be_bytes()[1..]);
475        out.extend(encode(&self.fields()));
476        out
477    }
478
479    /// The whole calldata of the `sendRawAction(bytes)` call.
480    #[must_use]
481    pub fn calldata(&self) -> Vec<u8> {
482        let mut out = SEND_RAW_ACTION.to_vec();
483        out.extend(encode(&[Field::Text(self.payload())]));
484        out
485    }
486
487    /// The call to CoreWriter, with no value: value voids the action.
488    #[must_use]
489    pub fn call(&self) -> Call {
490        Call { to: Some(CORE_WRITER), value: U256::ZERO, data: self.calldata() }
491    }
492
493    /// The HyperEVM transaction that sends it on `network`, from the account
494    /// it acts for.
495    #[must_use]
496    pub fn transaction(&self, network: Network, nonce: u64, gas: u64, fees: Fees) -> Eip1559 {
497        self.call().transaction(network.evm_chain_id(), nonce, gas, fees)
498    }
499
500    /// An action back out of a payload: the header checked, the fields read
501    /// against the docs' types.
502    pub fn decode(payload: &[u8]) -> Result<Action, BadAction> {
503        let Some((header, body)) = payload.split_first_chunk::<4>() else {
504            return Err(BadAction::Short(payload.len()));
505        };
506        if header[0] != ENCODING_VERSION {
507            return Err(BadAction::Version(header[0]));
508        }
509        let id = u32::from_be_bytes([0, header[1], header[2], header[3]]);
510        let spec = match (spec(id), id) {
511            (Some(spec), _) => spec,
512            (None, GAP) => return Err(BadAction::Gap),
513            (None, _) => return Err(BadAction::UnknownId(id)),
514        };
515        let tokens = abi::decode(&abi::params(spec.types)?, body)?;
516        let mut fields = Reader { items: tokens.iter() };
517        let r = &mut fields;
518        Ok(match id {
519            1 => Action::LimitOrder {
520                asset: r.u32()?,
521                is_buy: r.bool()?,
522                limit_px: r.u64()?,
523                sz: r.u64()?,
524                reduce_only: r.bool()?,
525                tif: Tif::from_code(r.u8()?),
526                cloid: r.u128()?,
527            },
528            2 => Action::VaultTransfer { vault: r.address()?, is_deposit: r.bool()?, usd: r.u64()? },
529            3 => Action::TokenDelegate { validator: r.address()?, wei: r.u64()?, is_undelegate: r.bool()? },
530            4 => Action::StakingDeposit { wei: r.u64()? },
531            5 => Action::StakingWithdraw { wei: r.u64()? },
532            6 => Action::SpotSend { destination: r.address()?, token: r.u64()?, wei: r.u64()? },
533            7 => Action::UsdClassTransfer { ntl: r.u64()?, to_perp: r.bool()? },
534            8 => Action::FinalizeEvmContract {
535                token: r.u64()?,
536                variant: FinalizeVariant::from_code(r.u8()?),
537                create_nonce: r.u64()?,
538            },
539            9 => Action::AddApiWallet { wallet: r.address()?, name: r.string()? },
540            10 => Action::CancelByOid { asset: r.u32()?, oid: r.u64()? },
541            11 => Action::CancelByCloid { asset: r.u32()?, cloid: r.u128()? },
542            12 => Action::ApproveBuilderFee { max_fee_rate: r.u64()?, builder: r.address()? },
543            13 => Action::SendAsset {
544                destination: r.address()?,
545                sub_account: r.address()?,
546                source_dex: r.u32()?,
547                destination_dex: r.u32()?,
548                token: r.u64()?,
549                wei: r.u64()?,
550            },
551            15 => Action::BorrowLend { operation: BorrowLendOp::from_code(r.u8()?), token: r.u64()?, wei: r.u64()? },
552            16 => Action::SetAbstraction { user: r.address()?, abstraction: Abstraction::from_code(r.u8()?) },
553            _ => Action::Outcome {
554                operation: OutcomeOp::from_code(r.u8()?),
555                question: r.u32()?,
556                outcome: r.u32()?,
557                wei: r.u64()?,
558            },
559        })
560    }
561
562    /// A limit order, its price and size as a person reads them, scaled to
563    /// 1e8. More than eight places is refused, never rounded.
564    pub fn limit_order(
565        asset: u32,
566        is_buy: bool,
567        price: Dec,
568        size: Dec,
569        reduce_only: bool,
570        tif: Tif,
571        cloid: u128,
572    ) -> Result<Action, BadUnits> {
573        Ok(Action::LimitOrder {
574            asset,
575            is_buy,
576            limit_px: exact_units(price, ORDER_DECIMALS)?,
577            sz: exact_units(size, ORDER_DECIMALS)?,
578            reduce_only,
579            tif,
580            cloid,
581        })
582    }
583
584    /// USDC into or out of a vault, scaled to 1e6.
585    pub fn vault_transfer(vault: Address, is_deposit: bool, usd: Dec) -> Result<Action, BadUnits> {
586        Ok(Action::VaultTransfer { vault, is_deposit, usd: exact_units(usd, USD_DECIMALS)? })
587    }
588
589    /// USDC between spot and perps, scaled to 1e6.
590    pub fn usd_class_transfer(ntl: Dec, to_perp: bool) -> Result<Action, BadUnits> {
591        Ok(Action::UsdClassTransfer { ntl: exact_units(ntl, USD_DECIMALS)?, to_perp })
592    }
593
594    /// HYPE into staking, scaled to 1e8.
595    pub fn staking_deposit(hype: Dec) -> Result<Action, BadUnits> {
596        Ok(Action::StakingDeposit { wei: exact_units(hype, HYPE_DECIMALS)? })
597    }
598
599    /// HYPE out of staking, scaled to 1e8.
600    pub fn staking_withdraw(hype: Dec) -> Result<Action, BadUnits> {
601        Ok(Action::StakingWithdraw { wei: exact_units(hype, HYPE_DECIMALS)? })
602    }
603
604    /// Staked HYPE to or from a validator, scaled to 1e8.
605    pub fn token_delegate(validator: Address, hype: Dec, is_undelegate: bool) -> Result<Action, BadUnits> {
606        Ok(Action::TokenDelegate { validator, wei: exact_units(hype, HYPE_DECIMALS)?, is_undelegate })
607    }
608}
609
610/// The decoded fields of an action, read in order.
611struct Reader<'a> {
612    items: std::slice::Iter<'a, Token>,
613}
614
615impl<'a> Reader<'a> {
616    fn take<T>(&mut self, read: impl FnOnce(&'a Token) -> Option<T>) -> Result<T, BadAction> {
617        self.items.next().and_then(read).ok_or(BadAction::Abi(AbiError::Type("a field of another type".to_owned())))
618    }
619
620    fn u128(&mut self) -> Result<u128, BadAction> {
621        self.take(Token::as_u128)
622    }
623
624    fn u64(&mut self) -> Result<u64, BadAction> {
625        self.take(Token::as_u64)
626    }
627
628    fn u32(&mut self) -> Result<u32, BadAction> {
629        self.take(Token::as_u32)
630    }
631
632    fn u8(&mut self) -> Result<u8, BadAction> {
633        self.take(Token::as_u8)
634    }
635
636    fn bool(&mut self) -> Result<bool, BadAction> {
637        self.take(Token::as_bool)
638    }
639
640    fn address(&mut self) -> Result<Address, BadAction> {
641        self.take(Token::as_address)
642    }
643
644    fn string(&mut self) -> Result<String, BadAction> {
645        self.take(|token| token.as_str().map(ToOwned::to_owned))
646    }
647}
648
649/// A `RawAction` log: who sent it, and what it carried.
650#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct RawAction {
652    /// `msg.sender` of `sendRawAction`, the account HyperCore acts for.
653    pub user: Address,
654    pub version: u8,
655    pub id: u32,
656    /// The `data` argument as it was logged.
657    pub payload: Vec<u8>,
658    /// The payload read; an id outside the table keeps the log readable.
659    pub action: Result<Action, BadAction>,
660}
661
662impl RawAction {
663    /// A log of CoreWriter's, read. Any other log is [`BadAction::NotRawAction`].
664    pub fn from_log(log: &Log) -> Result<RawAction, BadAction> {
665        let user = match log.topics.as_slice() {
666            [topic, user, ..] if log.address == CORE_WRITER && *topic == RAW_ACTION_TOPIC => user.to_address(),
667            _ => None,
668        }
669        .ok_or(BadAction::NotRawAction)?;
670        let payload = match abi::decode(&abi::params("bytes")?, &log.data)?.as_slice() {
671            [Token::Bytes(bytes)] => bytes.clone(),
672            _ => return Err(BadAction::NotRawAction),
673        };
674        let (version, id) = match payload.split_first_chunk::<4>() {
675            Some((header, _)) => (header[0], u32::from_be_bytes([0, header[1], header[2], header[3]])),
676            None => (0, 0),
677        };
678        Ok(RawAction { user, version, id, action: Action::decode(&payload), payload })
679    }
680}
681
682/// The `eth_getLogs` filter for every action sent through CoreWriter between
683/// two blocks. HyperEVM serves at most 50 blocks a query.
684#[must_use]
685pub fn raw_action_logs(from: BlockTag, to: BlockTag) -> LogFilter {
686    LogFilter {
687        from_block: Some(from),
688        to_block: Some(to),
689        address: vec![CORE_WRITER],
690        topics: vec![Some(vec![RAW_ACTION_TOPIC])],
691        ..LogFilter::default()
692    }
693}
694
695#[cfg(test)]
696#[path = "tests/corewriter.rs"]
697mod tests;