mirror of
https://github.com/0xfnzero/solana-streamer.git
synced 2026-08-20 12:28:08 +00:00
feat: add Bonk migration events and bump to v0.1.9
- Add BonkMigrateToAmmEvent and BonkMigrateToCpswapEvent - Update parsers and event handlers - Add rustfmt.toml configuration - Update documentation and examples
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
use crate::impl_unified_event;
|
||||
use crate::streaming::event_parser::common::EventMetadata;
|
||||
use crate::streaming::event_parser::protocols::bonk::types::{
|
||||
CurveParams, MintParams, PoolStatus, TradeDirection, VestingParams,
|
||||
};
|
||||
use crate::streaming::event_parser::common::EventMetadata;
|
||||
use crate::impl_unified_event;
|
||||
use borsh::BorshDeserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// 买入事件
|
||||
/// Trade event
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BonkTradeEvent {
|
||||
#[borsh(skip)]
|
||||
@@ -53,7 +53,7 @@ pub struct BonkTradeEvent {
|
||||
pub is_bot: bool,
|
||||
}
|
||||
|
||||
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
|
||||
// Macro to generate UnifiedEvent implementation, specifying the fields to be merged
|
||||
impl_unified_event!(
|
||||
BonkTradeEvent,
|
||||
pool_state,
|
||||
@@ -73,7 +73,7 @@ impl_unified_event!(
|
||||
pool_status
|
||||
);
|
||||
|
||||
/// 创建池事件
|
||||
/// Create pool event
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BonkPoolCreateEvent {
|
||||
#[borsh(skip)]
|
||||
@@ -100,7 +100,7 @@ pub struct BonkPoolCreateEvent {
|
||||
pub platform_config: Pubkey,
|
||||
}
|
||||
|
||||
// 使用宏生成UnifiedEvent实现,指定需要合并的字段
|
||||
// Macro to generate UnifiedEvent implementation, specifying the fields to be merged
|
||||
impl_unified_event!(
|
||||
BonkPoolCreateEvent,
|
||||
pool_state,
|
||||
@@ -111,16 +111,138 @@ impl_unified_event!(
|
||||
vesting_param
|
||||
);
|
||||
|
||||
/// 事件鉴别器常量
|
||||
/// Create pool event
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BonkMigrateToAmmEvent {
|
||||
#[borsh(skip)]
|
||||
pub metadata: EventMetadata,
|
||||
pub base_lot_size: u64,
|
||||
pub quote_lot_size: u64,
|
||||
pub market_vault_signer_nonce: u8,
|
||||
#[borsh(skip)]
|
||||
pub payer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub openbook_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub market: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub request_queue: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub event_queue: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub bids: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub asks: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub market_vault_signer: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub market_base_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub market_quote_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_pool: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_open_orders: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_lp_mint: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_base_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_quote_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_target_orders: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub amm_create_fee_destination: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub authority: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_state: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub global_config: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub base_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub quote_vault: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub pool_lp_token: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub spl_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub associated_token_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub system_program: Pubkey,
|
||||
#[borsh(skip)]
|
||||
pub rent_program: Pubkey,
|
||||
}
|
||||
|
||||
// Macro to generate UnifiedEvent implementation, specifying the fields to be merged
|
||||
impl_unified_event!(
|
||||
BonkMigrateToAmmEvent,
|
||||
base_lot_size,
|
||||
quote_lot_size,
|
||||
market_vault_signer_nonce
|
||||
);
|
||||
|
||||
// Migrate to CP Swap event
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
|
||||
pub struct BonkMigrateToCpswapEvent {
|
||||
pub metadata: EventMetadata,
|
||||
pub payer: Pubkey,
|
||||
pub base_mint: Pubkey,
|
||||
pub quote_mint: Pubkey,
|
||||
pub platform_config: Pubkey,
|
||||
pub cpswap_program: Pubkey,
|
||||
pub cpswap_pool: Pubkey,
|
||||
pub cpswap_authority: Pubkey,
|
||||
pub cpswap_lp_mint: Pubkey,
|
||||
pub cpswap_base_vault: Pubkey,
|
||||
pub cpswap_quote_vault: Pubkey,
|
||||
pub cpswap_config: Pubkey,
|
||||
pub cpswap_create_pool_fee: Pubkey,
|
||||
pub cpswap_observation: Pubkey,
|
||||
pub lock_program: Pubkey,
|
||||
pub lock_authority: Pubkey,
|
||||
pub lock_lp_vault: Pubkey,
|
||||
pub authority: Pubkey,
|
||||
pub pool_state: Pubkey,
|
||||
pub global_config: Pubkey,
|
||||
pub base_vault: Pubkey,
|
||||
pub quote_vault: Pubkey,
|
||||
pub pool_lp_token: Pubkey,
|
||||
pub base_token_program: Pubkey,
|
||||
pub quote_token_program: Pubkey,
|
||||
pub associated_token_program: Pubkey,
|
||||
pub system_program: Pubkey,
|
||||
pub rent_program: Pubkey,
|
||||
pub metadata_program: Pubkey,
|
||||
pub remaining_accounts: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
// Macro to generate UnifiedEvent implementation, specifying the fields to be merged
|
||||
impl_unified_event!(BonkMigrateToCpswapEvent,);
|
||||
|
||||
/// Event discriminator constants
|
||||
pub mod discriminators {
|
||||
// 事件鉴别器
|
||||
// Event discriminators
|
||||
pub const TRADE_EVENT: &str = "0xe445a52e51cb9a1dbddb7fd34ee661ee";
|
||||
pub const POOL_CREATE_EVENT: &str = "0xe445a52e51cb9a1d97d7e20976a173ae";
|
||||
|
||||
// 指令鉴别器
|
||||
// Instruction discriminators
|
||||
pub const BUY_EXACT_IN: &[u8] = &[250, 234, 13, 123, 213, 156, 19, 236];
|
||||
pub const BUY_EXACT_OUT: &[u8] = &[24, 211, 116, 40, 105, 3, 153, 56];
|
||||
pub const SELL_EXACT_IN: &[u8] = &[149, 39, 222, 155, 211, 124, 152, 26];
|
||||
pub const SELL_EXACT_OUT: &[u8] = &[95, 200, 71, 34, 8, 9, 11, 166];
|
||||
pub const INITIALIZE: &[u8] = &[175, 175, 109, 31, 13, 152, 155, 237];
|
||||
pub const MIGRATE_TO_AMM: &[u8] = &[207, 82, 192, 145, 254, 207, 145, 223];
|
||||
pub const MIGRATE_TO_CP_SWAP: &[u8] = &[136, 92, 200, 103, 28, 218, 144, 140];
|
||||
}
|
||||
|
||||
@@ -6,16 +6,17 @@ use crate::streaming::event_parser::{
|
||||
common::{utils::*, EventMetadata, EventType, ProtocolType},
|
||||
core::traits::{EventParser, GenericEventParseConfig, GenericEventParser, UnifiedEvent},
|
||||
protocols::bonk::{
|
||||
discriminators, BonkPoolCreateEvent, BonkTradeEvent, ConstantCurve, CurveParams,
|
||||
FixedCurve, LinearCurve, MintParams, TradeDirection, VestingParams,
|
||||
discriminators, BonkMigrateToAmmEvent, BonkMigrateToCpswapEvent, BonkPoolCreateEvent,
|
||||
BonkTradeEvent, ConstantCurve, CurveParams, FixedCurve, LinearCurve, MintParams,
|
||||
TradeDirection, VestingParams,
|
||||
},
|
||||
};
|
||||
|
||||
/// Bonk程序ID
|
||||
/// Bonk Program ID
|
||||
pub const BONK_PROGRAM_ID: Pubkey =
|
||||
solana_sdk::pubkey!("LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj");
|
||||
|
||||
/// Bonk事件解析器
|
||||
/// Bonk Event Parser
|
||||
pub struct BonkEventParser {
|
||||
inner: GenericEventParser,
|
||||
}
|
||||
@@ -28,7 +29,7 @@ impl Default for BonkEventParser {
|
||||
|
||||
impl BonkEventParser {
|
||||
pub fn new() -> Self {
|
||||
// 配置所有事件类型
|
||||
// Configure all event types
|
||||
let configs = vec![
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: discriminators::TRADE_EVENT,
|
||||
@@ -65,6 +66,20 @@ impl BonkEventParser {
|
||||
inner_instruction_parser: Self::parse_pool_create_inner_instruction,
|
||||
instruction_parser: Self::parse_initialize_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: "",
|
||||
instruction_discriminator: discriminators::MIGRATE_TO_AMM,
|
||||
event_type: EventType::BonkMigrateToAmm,
|
||||
inner_instruction_parser: Self::parse_migrate_to_amm_inner_instruction,
|
||||
instruction_parser: Self::parse_migrate_to_amm_instruction,
|
||||
},
|
||||
GenericEventParseConfig {
|
||||
inner_instruction_discriminator: "",
|
||||
instruction_discriminator: discriminators::MIGRATE_TO_CP_SWAP,
|
||||
event_type: EventType::BonkMigrateToCpswap,
|
||||
inner_instruction_parser: Self::parse_migrate_to_cpswap_inner_instruction,
|
||||
instruction_parser: Self::parse_migrate_to_cpswap_instruction,
|
||||
},
|
||||
];
|
||||
|
||||
let inner = GenericEventParser::new(BONK_PROGRAM_ID, ProtocolType::Bonk, configs);
|
||||
@@ -72,7 +87,7 @@ impl BonkEventParser {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// 解析创建池事件
|
||||
/// Parse pool creation event
|
||||
fn parse_pool_create_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
@@ -80,27 +95,36 @@ impl BonkEventParser {
|
||||
if let Ok(event) = borsh::from_slice::<BonkPoolCreateEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(metadata.signature.to_string());
|
||||
Some(Box::new(BonkPoolCreateEvent {
|
||||
metadata,
|
||||
..event
|
||||
}))
|
||||
Some(Box::new(BonkPoolCreateEvent { metadata, ..event }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析交易事件
|
||||
/// Parse migrate to AMM event
|
||||
fn parse_migrate_to_amm_inner_instruction(
|
||||
_data: &[u8],
|
||||
_metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse migrate to CP Swap event
|
||||
fn parse_migrate_to_cpswap_inner_instruction(
|
||||
_data: &[u8],
|
||||
_metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse trade event
|
||||
fn parse_trade_inner_instruction(
|
||||
data: &[u8],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if let Ok(event) = borsh::from_slice::<BonkTradeEvent>(data) {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(format!(
|
||||
"{}-{}",
|
||||
metadata.signature,
|
||||
event.pool_state
|
||||
));
|
||||
metadata.set_id(format!("{}-{}", metadata.signature, event.pool_state));
|
||||
if metadata.event_type == EventType::BonkBuyExactIn
|
||||
|| metadata.event_type == EventType::BonkBuyExactOut
|
||||
{
|
||||
@@ -109,19 +133,17 @@ impl BonkEventParser {
|
||||
}
|
||||
} else if (metadata.event_type == EventType::BonkSellExactIn
|
||||
|| metadata.event_type == EventType::BonkSellExactOut)
|
||||
&& event.trade_direction != TradeDirection::Sell {
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(BonkTradeEvent {
|
||||
metadata,
|
||||
..event
|
||||
}))
|
||||
&& event.trade_direction != TradeDirection::Sell
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Box::new(BonkTradeEvent { metadata, ..event }))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析买入指令事件
|
||||
/// Parse buy instruction event
|
||||
fn parse_buy_exact_in_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
@@ -258,7 +280,7 @@ impl BonkEventParser {
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析初始化事件
|
||||
/// Parse initialize event
|
||||
fn parse_initialize_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
@@ -294,13 +316,13 @@ impl BonkEventParser {
|
||||
}))
|
||||
}
|
||||
|
||||
/// 解析 MintParams 结构
|
||||
/// Parse MintParams structure
|
||||
fn parse_mint_params(data: &[u8], offset: &mut usize) -> Option<MintParams> {
|
||||
// 读取decimals (1字节)
|
||||
// Read decimals (1 byte)
|
||||
let decimals = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
// 读取name字符串长度和内容
|
||||
// Read name string length and content
|
||||
let name_len = read_u32_le(data, *offset)? as usize;
|
||||
*offset += 4;
|
||||
if data.len() < *offset + name_len {
|
||||
@@ -309,7 +331,7 @@ impl BonkEventParser {
|
||||
let name = String::from_utf8(data[*offset..*offset + name_len].to_vec()).ok()?;
|
||||
*offset += name_len;
|
||||
|
||||
// 读取symbol字符串长度和内容
|
||||
// Read symbol string length and content
|
||||
let symbol_len = read_u32_le(data, *offset)? as usize;
|
||||
*offset += 4;
|
||||
if data.len() < *offset + symbol_len {
|
||||
@@ -318,7 +340,7 @@ impl BonkEventParser {
|
||||
let symbol = String::from_utf8(data[*offset..*offset + symbol_len].to_vec()).ok()?;
|
||||
*offset += symbol_len;
|
||||
|
||||
// 读取uri字符串长度和内容
|
||||
// Read uri string length and content
|
||||
let uri_len = read_u32_le(data, *offset)? as usize;
|
||||
*offset += 4;
|
||||
if data.len() < *offset + uri_len {
|
||||
@@ -327,17 +349,12 @@ impl BonkEventParser {
|
||||
let uri = String::from_utf8(data[*offset..*offset + uri_len].to_vec()).ok()?;
|
||||
*offset += uri_len;
|
||||
|
||||
Some(MintParams {
|
||||
decimals,
|
||||
name,
|
||||
symbol,
|
||||
uri,
|
||||
})
|
||||
Some(MintParams { decimals, name, symbol, uri })
|
||||
}
|
||||
|
||||
/// 解析 CurveParams 结构
|
||||
/// Parse CurveParams structure
|
||||
fn parse_curve_params(data: &[u8], offset: &mut usize) -> Option<CurveParams> {
|
||||
// 读取curve类型标识符 (1字节)
|
||||
// Read curve type identifier (1 byte)
|
||||
let curve_type = read_u8(data, *offset)?;
|
||||
*offset += 1;
|
||||
|
||||
@@ -372,11 +389,7 @@ impl BonkEventParser {
|
||||
*offset += 1;
|
||||
|
||||
Some(CurveParams::Fixed {
|
||||
data: FixedCurve {
|
||||
supply,
|
||||
total_quote_fund_raising,
|
||||
migrate_type,
|
||||
},
|
||||
data: FixedCurve { supply, total_quote_fund_raising, migrate_type },
|
||||
})
|
||||
}
|
||||
2 => {
|
||||
@@ -389,18 +402,14 @@ impl BonkEventParser {
|
||||
*offset += 1;
|
||||
|
||||
Some(CurveParams::Linear {
|
||||
data: LinearCurve {
|
||||
supply,
|
||||
total_quote_fund_raising,
|
||||
migrate_type,
|
||||
},
|
||||
data: LinearCurve { supply, total_quote_fund_raising, migrate_type },
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 解析 VestingParams 结构
|
||||
/// Parse VestingParams structure
|
||||
fn parse_vesting_params(data: &[u8], offset: &mut usize) -> Option<VestingParams> {
|
||||
let total_locked_amount = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
@@ -409,11 +418,109 @@ impl BonkEventParser {
|
||||
let unlock_period = read_u64_le(data, *offset)?;
|
||||
*offset += 8;
|
||||
|
||||
Some(VestingParams {
|
||||
total_locked_amount,
|
||||
cliff_period,
|
||||
unlock_period,
|
||||
})
|
||||
Some(VestingParams { total_locked_amount, cliff_period, unlock_period })
|
||||
}
|
||||
|
||||
/// Parse migrate to AMM event
|
||||
fn parse_migrate_to_amm_instruction(
|
||||
data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
if data.len() < 16 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let base_lot_size = u64::from_le_bytes(data[0..8].try_into().unwrap());
|
||||
let quote_lot_size = u64::from_le_bytes(data[8..16].try_into().unwrap());
|
||||
let market_vault_signer_nonce = data[16];
|
||||
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(metadata.signature.to_string());
|
||||
|
||||
Some(Box::new(BonkMigrateToAmmEvent {
|
||||
metadata,
|
||||
base_lot_size,
|
||||
quote_lot_size,
|
||||
market_vault_signer_nonce,
|
||||
payer: accounts[0],
|
||||
base_mint: accounts[1],
|
||||
quote_mint: accounts[2],
|
||||
openbook_program: accounts[3],
|
||||
market: accounts[4],
|
||||
request_queue: accounts[5],
|
||||
event_queue: accounts[6],
|
||||
bids: accounts[7],
|
||||
asks: accounts[8],
|
||||
market_vault_signer: accounts[9],
|
||||
market_base_vault: accounts[10],
|
||||
market_quote_vault: accounts[11],
|
||||
amm_program: accounts[12],
|
||||
amm_pool: accounts[13],
|
||||
amm_authority: accounts[14],
|
||||
amm_open_orders: accounts[15],
|
||||
amm_lp_mint: accounts[16],
|
||||
amm_base_vault: accounts[17],
|
||||
amm_quote_vault: accounts[18],
|
||||
amm_target_orders: accounts[19],
|
||||
amm_config: accounts[20],
|
||||
amm_create_fee_destination: accounts[21],
|
||||
authority: accounts[22],
|
||||
pool_state: accounts[23],
|
||||
global_config: accounts[24],
|
||||
base_vault: accounts[25],
|
||||
quote_vault: accounts[26],
|
||||
pool_lp_token: accounts[27],
|
||||
spl_token_program: accounts[28],
|
||||
associated_token_program: accounts[29],
|
||||
system_program: accounts[30],
|
||||
rent_program: accounts[31],
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
|
||||
/// Parse migrate to CP Swap event
|
||||
fn parse_migrate_to_cpswap_instruction(
|
||||
_data: &[u8],
|
||||
accounts: &[Pubkey],
|
||||
metadata: EventMetadata,
|
||||
) -> Option<Box<dyn UnifiedEvent>> {
|
||||
let mut metadata = metadata;
|
||||
metadata.set_id(metadata.signature.to_string());
|
||||
|
||||
Some(Box::new(BonkMigrateToCpswapEvent {
|
||||
metadata,
|
||||
payer: accounts[0],
|
||||
base_mint: accounts[1],
|
||||
quote_mint: accounts[2],
|
||||
platform_config: accounts[3],
|
||||
cpswap_program: accounts[4],
|
||||
cpswap_pool: accounts[5],
|
||||
cpswap_authority: accounts[6],
|
||||
cpswap_lp_mint: accounts[7],
|
||||
cpswap_base_vault: accounts[8],
|
||||
cpswap_quote_vault: accounts[9],
|
||||
cpswap_config: accounts[10],
|
||||
cpswap_create_pool_fee: accounts[11],
|
||||
cpswap_observation: accounts[12],
|
||||
lock_program: accounts[13],
|
||||
lock_authority: accounts[14],
|
||||
lock_lp_vault: accounts[15],
|
||||
authority: accounts[16],
|
||||
pool_state: accounts[17],
|
||||
global_config: accounts[18],
|
||||
base_vault: accounts[19],
|
||||
quote_vault: accounts[20],
|
||||
pool_lp_token: accounts[21],
|
||||
base_token_program: accounts[22],
|
||||
quote_token_program: accounts[23],
|
||||
associated_token_program: accounts[24],
|
||||
system_program: accounts[25],
|
||||
rent_program: accounts[26],
|
||||
metadata_program: accounts[27],
|
||||
remaining_accounts: accounts[28..].to_vec(),
|
||||
..Default::default()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user