1use 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
31pub const CORE_WRITER: Address = Address::from_bytes([0x33; 20]);
33
34pub 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
40pub const SEND_RAW_ACTION: [u8; 4] = [0x17, 0x93, 0x8e, 0x13];
42
43pub const ENCODING_VERSION: u8 = 1;
45
46pub const GAP: u32 = 14;
48
49pub const ORDER_DECIMALS: u32 = 8;
51pub const USD_DECIMALS: u32 = 6;
53pub const HYPE_DECIMALS: u32 = 8;
55pub const DECIBPS_DECIMALS: u32 = 3;
57pub const SPOT_DEX: u32 = u32::MAX;
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct Spec {
63 pub id: u32,
64 pub name: &'static str,
65 pub fields: &'static [&'static str],
67 pub types: &'static str,
69}
70
71pub 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#[must_use]
123pub fn spec(id: u32) -> Option<&'static Spec> {
124 SPECS.iter().find(|spec| spec.id == id)
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum Tif {
130 Alo,
131 Gtc,
132 Ioc,
133 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
161pub enum FinalizeVariant {
162 Create,
164 FirstStorageSlot,
166 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum BorrowLendOp {
198 Supply,
199 Withdraw,
200 Borrow,
202 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#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
300pub enum Action {
301 LimitOrder { asset: u32, is_buy: bool, limit_px: u64, sz: u64, reduce_only: bool, tif: Tif, cloid: u128 },
303 VaultTransfer { vault: Address, is_deposit: bool, usd: u64 },
305 TokenDelegate { validator: Address, wei: u64, is_undelegate: bool },
307 StakingDeposit { wei: u64 },
309 StakingWithdraw { wei: u64 },
311 SpotSend { destination: Address, token: u64, wei: u64 },
313 UsdClassTransfer { ntl: u64, to_perp: bool },
315 FinalizeEvmContract { token: u64, variant: FinalizeVariant, create_nonce: u64 },
317 AddApiWallet { wallet: Address, name: String },
319 CancelByOid { asset: u32, oid: u64 },
321 CancelByCloid { asset: u32, cloid: u128 },
323 ApproveBuilderFee { max_fee_rate: u64, builder: Address },
325 SendAsset {
328 destination: Address,
329 sub_account: Address,
330 source_dex: u32,
331 destination_dex: u32,
332 token: u64,
333 wei: u64,
334 },
335 BorrowLend { operation: BorrowLendOp, token: u64, wei: u64 },
337 SetAbstraction { user: Address, abstraction: Abstraction },
339 Outcome { operation: OutcomeOp, question: u32, outcome: u32, wei: u64 },
341}
342
343#[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
360enum 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
378fn 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 #[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 #[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 #[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 #[must_use]
489 pub fn call(&self) -> Call {
490 Call { to: Some(CORE_WRITER), value: U256::ZERO, data: self.calldata() }
491 }
492
493 #[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 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 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 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 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 pub fn staking_deposit(hype: Dec) -> Result<Action, BadUnits> {
596 Ok(Action::StakingDeposit { wei: exact_units(hype, HYPE_DECIMALS)? })
597 }
598
599 pub fn staking_withdraw(hype: Dec) -> Result<Action, BadUnits> {
601 Ok(Action::StakingWithdraw { wei: exact_units(hype, HYPE_DECIMALS)? })
602 }
603
604 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
610struct 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#[derive(Debug, Clone, PartialEq, Eq)]
651pub struct RawAction {
652 pub user: Address,
654 pub version: u8,
655 pub id: u32,
656 pub payload: Vec<u8>,
658 pub action: Result<Action, BadAction>,
660}
661
662impl RawAction {
663 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#[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;