1use std::ops::Mul;
2
3#[cfg(not(feature = "library"))]
4use cosmwasm_std::entry_point;
5use cosmwasm_std::{
6 coin, from_binary, to_binary, Attribute, BankMsg, Binary, CosmosMsg, Decimal, Deps, DepsMut,
7 Env, MessageInfo, Order, Response, StdResult, Uint128, WasmMsg,
8};
9use cw2::set_contract_version;
10use cw20::{Cw20ExecuteMsg, Cw20ReceiveMsg};
11use cw_storage_plus::Bound;
12use komple_framework_types::modules::fee::{Fees, FixedPayment, PercentagePayment};
13use komple_framework_types::modules::Modules;
14use komple_framework_types::shared::query::ResponseWrapper;
15use komple_framework_types::shared::RegisterMsg;
16use komple_framework_utils::check_admin_privileges;
17use komple_framework_utils::funds::{check_single_amount, FundsError};
18use komple_framework_utils::response::{EventHelper, ResponseHelper};
19use komple_framework_utils::shared::{execute_lock_execute, execute_update_operators};
20
21use crate::error::ContractError;
22use crate::msg::{
23 CustomPaymentAddress, ExecuteMsg, FixedFeeResponse, PercentageFeeResponse, QueryMsg, ReceiveMsg,
24};
25use crate::state::{
26 Config, CONFIG, EXECUTE_LOCK, FIXED_FEES, HUB_ADDR, OPERATORS, PERCENTAGE_FEES,
27};
28
29const CONTRACT_NAME: &str = "crates.io:komple-framework-fee-module";
31const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
32
33#[cfg_attr(not(feature = "library"), entry_point)]
34pub fn instantiate(
35 deps: DepsMut,
36 _env: Env,
37 info: MessageInfo,
38 msg: RegisterMsg,
39) -> Result<Response, ContractError> {
40 set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
41
42 let admin = deps.api.addr_validate(&msg.admin)?;
43
44 let config = Config { admin };
45 CONFIG.save(deps.storage, &config)?;
46
47 HUB_ADDR.save(deps.storage, &info.sender)?;
48
49 EXECUTE_LOCK.save(deps.storage, &false)?;
50
51 Ok(ResponseHelper::new_module("fee", "instantiate").add_event(
52 EventHelper::new("fee_instantiate")
53 .add_attribute("admin", config.admin.to_string())
54 .add_attribute("hub_addr", info.sender)
55 .get(),
56 ))
57}
58
59#[cfg_attr(not(feature = "library"), entry_point)]
60pub fn execute(
61 deps: DepsMut,
62 env: Env,
63 info: MessageInfo,
64 msg: ExecuteMsg,
65) -> Result<Response, ContractError> {
66 let execute_lock = EXECUTE_LOCK.load(deps.storage)?;
67 if execute_lock {
68 return Err(ContractError::ExecuteLocked {});
69 };
70
71 match msg {
72 ExecuteMsg::SetFee {
73 fee_type,
74 module_name,
75 fee_name,
76 data,
77 } => execute_set_fee(deps, env, info, fee_type, module_name, fee_name, data),
78 ExecuteMsg::RemoveFee {
79 fee_type,
80 module_name,
81 fee_name,
82 } => execute_remove_fee(deps, env, info, fee_type, module_name, fee_name),
83 ExecuteMsg::Distribute {
84 fee_type,
85 module_name,
86 custom_payment_addresses,
87 } => execute_distribute(
88 deps,
89 env,
90 info,
91 fee_type,
92 module_name,
93 custom_payment_addresses,
94 None,
95 ),
96 ExecuteMsg::UpdateOperators { addrs } => {
97 let config = CONFIG.load(deps.storage)?;
98 let res = execute_update_operators(
99 deps,
100 info,
101 Modules::Fee.as_str(),
102 &env.contract.address,
103 &config.admin,
104 OPERATORS,
105 addrs,
106 );
107 match res {
108 Ok(res) => Ok(res),
109 Err(err) => Err(err.into()),
110 }
111 }
112 ExecuteMsg::LockExecute {} => {
113 let res = execute_lock_execute(
114 deps,
115 info,
116 Modules::Fee.as_str(),
117 &env.contract.address,
118 EXECUTE_LOCK,
119 );
120 match res {
121 Ok(res) => Ok(res),
122 Err(e) => Err(e.into()),
123 }
124 }
125 ExecuteMsg::Receive(msg) => execute_receive(deps, env, info, msg),
126 }
127}
128
129fn execute_set_fee(
130 deps: DepsMut,
131 env: Env,
132 info: MessageInfo,
133 fee_type: Fees,
134 module_name: String,
135 fee_name: String,
136 data: Binary,
137) -> Result<Response, ContractError> {
138 let hub_addr = HUB_ADDR.may_load(deps.storage)?;
139 let config = CONFIG.load(deps.storage)?;
140 let operators = OPERATORS.may_load(deps.storage)?;
141 check_admin_privileges(
142 &info.sender,
143 &env.contract.address,
144 &config.admin,
145 hub_addr,
146 operators,
147 )?;
148
149 let mut event_attributes: Vec<Attribute> = vec![];
150
151 match fee_type {
152 Fees::Fixed => {
153 let fixed_payment: FixedPayment = from_binary(&data)?;
154 if fixed_payment.value.is_zero() {
155 return Err(ContractError::InvalidFee {});
156 };
157
158 if fixed_payment.address.is_some() {
159 deps.api
160 .addr_validate(&fixed_payment.address.clone().unwrap())?;
161 }
162
163 FIXED_FEES.save(deps.storage, (&module_name, &fee_name), &fixed_payment)?;
164
165 event_attributes.push(Attribute {
166 key: "value".to_string(),
167 value: fixed_payment.value.to_string(),
168 });
169 if let Some(payment_address) = fixed_payment.address {
170 event_attributes.push(Attribute {
171 key: "address".to_string(),
172 value: payment_address,
173 });
174 }
175 }
176 Fees::Percentage => {
177 let percentage_payment: PercentagePayment = from_binary(&data)?;
178 if percentage_payment.value > Decimal::one() {
179 return Err(ContractError::InvalidFee {});
180 };
181
182 let current_percentage_payment =
186 PERCENTAGE_FEES.may_load(deps.storage, (&module_name, &fee_name))?;
187 let current_percentage_payment_value = match current_percentage_payment {
188 Some(p) => p.value,
189 None => Decimal::zero(),
190 };
191 let total_fee =
192 query_total_percentage_fees(deps.as_ref(), module_name.clone(), None, None)?;
193 if total_fee.data - current_percentage_payment_value + percentage_payment.value
194 >= Decimal::one()
195 {
196 return Err(ContractError::InvalidTotalFee {});
197 };
198
199 if percentage_payment.address.is_some() {
200 deps.api
201 .addr_validate(&percentage_payment.address.clone().unwrap())?;
202 };
203
204 PERCENTAGE_FEES.save(deps.storage, (&module_name, &fee_name), &percentage_payment)?;
205
206 event_attributes.push(Attribute {
207 key: "value".to_string(),
208 value: percentage_payment.value.to_string(),
209 });
210 if let Some(payment_address) = percentage_payment.address {
211 event_attributes.push(Attribute {
212 key: "address".to_string(),
213 value: payment_address,
214 });
215 }
216 }
217 }
218
219 Ok(ResponseHelper::new_module("fee", "set_fee").add_event(
220 EventHelper::new("fee_set_fee")
221 .add_attribute("fee_type", fee_type.as_str())
222 .add_attribute("module_name", &module_name)
223 .add_attribute("fee_name", &fee_name)
224 .add_attributes(event_attributes)
225 .get(),
226 ))
227}
228
229fn execute_remove_fee(
230 deps: DepsMut,
231 env: Env,
232 info: MessageInfo,
233 fee_type: Fees,
234 module_name: String,
235 fee_name: String,
236) -> Result<Response, ContractError> {
237 let hub_addr = HUB_ADDR.may_load(deps.storage)?;
238 let config = CONFIG.load(deps.storage)?;
239 let operators = OPERATORS.may_load(deps.storage)?;
240 check_admin_privileges(
241 &info.sender,
242 &env.contract.address,
243 &config.admin,
244 hub_addr,
245 operators,
246 )?;
247
248 match fee_type {
249 Fees::Fixed => FIXED_FEES.remove(deps.storage, (&module_name, &fee_name)),
250 Fees::Percentage => PERCENTAGE_FEES.remove(deps.storage, (&module_name, &fee_name)),
251 }
252
253 Ok(ResponseHelper::new_module("fee", "remove_fee").add_event(
254 EventHelper::new("fee_remove_fee")
255 .add_attribute("fee_type", fee_type.as_str())
256 .add_attribute("module_name", &module_name)
257 .add_attribute("fee_name", &fee_name)
258 .get(),
259 ))
260}
261
262fn execute_distribute(
263 deps: DepsMut,
264 _env: Env,
265 info: MessageInfo,
266 fee_type: Fees,
267 module_name: String,
268 custom_payment_addresses: Option<Vec<CustomPaymentAddress>>,
269 cw20_token_amount: Option<Uint128>,
270) -> Result<Response, ContractError> {
271 let msgs = match fee_type {
272 Fees::Fixed => _distribute_fixed_fee(
273 deps,
274 info,
275 &module_name,
276 custom_payment_addresses,
277 cw20_token_amount,
278 )?,
279 Fees::Percentage => _distribute_percentage_fee(
280 deps,
281 info,
282 &module_name,
283 custom_payment_addresses,
284 cw20_token_amount,
285 )?,
286 };
287
288 Ok(ResponseHelper::new_module("fee", "distribute")
289 .add_messages(msgs)
290 .add_event(
291 EventHelper::new("fee_distribute")
292 .add_attribute("fee_type", fee_type.as_str())
293 .add_attribute("module_name", &module_name)
294 .get(),
295 ))
296}
297
298fn _distribute_fixed_fee(
299 deps: DepsMut,
300 info: MessageInfo,
301 module_name: &str,
302 custom_payment_addresses: Option<Vec<CustomPaymentAddress>>,
303 cw20_token_amount: Option<Uint128>,
304) -> Result<Vec<CosmosMsg>, ContractError> {
305 let mut msgs: Vec<CosmosMsg> = vec![];
306
307 let amounts = FIXED_FEES
309 .prefix(&module_name)
310 .range(deps.storage, None, None, Order::Ascending)
311 .map(|item| {
312 let (fee_name, fixed_payment) = item.unwrap();
313 FixedFeeResponse {
314 module_name: module_name.to_string(),
315 fee_name,
316 address: fixed_payment.address,
317 value: fixed_payment.value,
318 }
319 })
320 .collect::<Vec<FixedFeeResponse>>();
321
322 if amounts.is_empty() {
323 return Err(ContractError::NoPaymentsFound {});
324 }
325
326 let total_amount = amounts.iter().map(|item| item.value).sum::<Uint128>();
328 if cw20_token_amount.is_none() {
329 check_single_amount(&info, total_amount)?;
330 } else {
331 if cw20_token_amount.unwrap() != total_amount {
332 return Err(FundsError::InvalidFunds {
333 got: cw20_token_amount.unwrap().to_string(),
334 expected: total_amount.to_string(),
335 }
336 .into());
337 };
338 }
339
340 for amount in amounts {
342 let mut is_custom_address = false;
343
344 if let Some(custom_payment_addresses) = custom_payment_addresses.clone() {
346 let custom_payment_address = custom_payment_addresses
348 .iter()
349 .find(|item| amount.fee_name == item.fee_name);
350 if let Some(custom_payment_address) = custom_payment_address {
351 is_custom_address = true;
352 let msg = match cw20_token_amount.is_none() {
353 true => CosmosMsg::Bank(BankMsg::Send {
354 to_address: custom_payment_address.address.to_string(),
355 amount: vec![coin(amount.value.u128(), info.funds[0].denom.clone())],
356 }),
357 false => CosmosMsg::Wasm(WasmMsg::Execute {
358 contract_addr: info.sender.to_string(),
359 msg: to_binary(&Cw20ExecuteMsg::Transfer {
360 recipient: custom_payment_address.address.to_string(),
361 amount: amount.value,
362 })?,
363 funds: vec![],
364 }),
365 };
366 msgs.push(msg);
367 }
368 }
369
370 if !is_custom_address {
372 if let Some(payment_address) = amount.address {
373 let msg = match cw20_token_amount.is_none() {
374 true => CosmosMsg::Bank(BankMsg::Send {
375 to_address: payment_address.to_string(),
376 amount: vec![coin(amount.value.u128(), info.funds[0].denom.clone())],
377 }),
378 false => CosmosMsg::Wasm(WasmMsg::Execute {
379 contract_addr: info.sender.to_string(),
380 msg: to_binary(&Cw20ExecuteMsg::Transfer {
381 recipient: payment_address.to_string(),
382 amount: amount.value,
383 })?,
384 funds: vec![],
385 }),
386 };
387 msgs.push(msg);
388 };
389 }
390 }
391
392 Ok(msgs)
393}
394
395fn _distribute_percentage_fee(
396 deps: DepsMut,
397 info: MessageInfo,
398 module_name: &str,
399 custom_payment_addresses: Option<Vec<CustomPaymentAddress>>,
400 cw20_token_amount: Option<Uint128>,
401) -> Result<Vec<CosmosMsg>, ContractError> {
402 let mut msgs: Vec<CosmosMsg> = vec![];
403
404 let percentages = PERCENTAGE_FEES
406 .prefix(&module_name)
407 .range(deps.storage, None, None, Order::Ascending)
408 .map(|item| {
409 let (fee_name, percentage_payment) = item.unwrap();
410 PercentageFeeResponse {
411 module_name: module_name.to_string(),
412 fee_name,
413 address: percentage_payment.address,
414 value: percentage_payment.value,
415 }
416 })
417 .collect::<Vec<PercentageFeeResponse>>();
418
419 if percentages.is_empty() {
420 return Err(ContractError::NoPaymentsFound {});
421 }
422
423 let total_fee = percentages
425 .iter()
426 .map(|item| item.value)
427 .sum::<Decimal>()
428 .mul(Uint128::new(100));
429
430 for percentage in percentages {
432 let mut is_custom_address = false;
433
434 let payment_amount = match cw20_token_amount {
436 Some(amount) => amount
437 .mul(percentage.value.mul(Uint128::new(100)))
438 .checked_div(total_fee)?,
439 None => info.funds[0]
440 .amount
441 .mul(percentage.value.mul(Uint128::new(100)))
442 .checked_div(total_fee)?,
443 };
444
445 if let Some(custom_payment_addresses) = custom_payment_addresses.clone() {
447 let custom_payment_address = custom_payment_addresses
449 .iter()
450 .find(|item| percentage.fee_name == item.fee_name);
451 if let Some(custom_payment_address) = custom_payment_address {
452 is_custom_address = true;
453 let msg = match cw20_token_amount.is_none() {
454 true => CosmosMsg::Bank(BankMsg::Send {
455 to_address: custom_payment_address.address.to_string(),
456 amount: vec![coin(payment_amount.u128(), info.funds[0].denom.clone())],
457 }),
458 false => CosmosMsg::Wasm(WasmMsg::Execute {
459 contract_addr: info.sender.to_string(),
460 msg: to_binary(&Cw20ExecuteMsg::Transfer {
461 recipient: custom_payment_address.address.to_string(),
462 amount: payment_amount,
463 })?,
464 funds: vec![],
465 }),
466 };
467 msgs.push(msg);
468 }
469 }
470
471 if !is_custom_address {
473 if let Some(payment_address) = percentage.address {
474 let msg = match cw20_token_amount.is_none() {
475 true => CosmosMsg::Bank(BankMsg::Send {
476 to_address: payment_address.to_string(),
477 amount: vec![coin(payment_amount.u128(), info.funds[0].denom.clone())],
478 }),
479 false => CosmosMsg::Wasm(WasmMsg::Execute {
480 contract_addr: info.sender.to_string(),
481 msg: to_binary(&Cw20ExecuteMsg::Transfer {
482 recipient: payment_address.to_string(),
483 amount: payment_amount,
484 })?,
485 funds: vec![],
486 }),
487 };
488 msgs.push(msg);
489 };
490 }
491 }
492
493 Ok(msgs)
494}
495
496fn execute_receive(
497 deps: DepsMut,
498 env: Env,
499 info: MessageInfo,
500 cw20_receive_msg: Cw20ReceiveMsg,
501) -> Result<Response, ContractError> {
502 let msg: ReceiveMsg = from_binary(&cw20_receive_msg.msg)?;
503 let amount = cw20_receive_msg.amount;
504
505 match msg {
506 ReceiveMsg::Distribute {
507 fee_type,
508 module_name,
509 custom_payment_addresses,
510 } => execute_distribute(
511 deps,
512 env,
513 info,
514 fee_type,
515 module_name,
516 custom_payment_addresses,
517 Some(amount),
518 ),
519 }
520}
521
522#[cfg_attr(not(feature = "library"), entry_point)]
523pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
524 match msg {
525 QueryMsg::Config {} => to_binary(&query_config(deps)?),
526 QueryMsg::PercentageFee {
527 module_name,
528 fee_name,
529 } => to_binary(&query_percentage_fee(deps, module_name, fee_name)?),
530 QueryMsg::FixedFee {
531 module_name,
532 fee_name,
533 } => to_binary(&query_fixed_fee(deps, module_name, fee_name)?),
534 QueryMsg::PercentageFees {
535 module_name,
536 start_after,
537 limit,
538 } => to_binary(&query_percentage_fees(
539 deps,
540 module_name,
541 start_after,
542 limit,
543 )?),
544 QueryMsg::FixedFees {
545 module_name,
546 start_after,
547 limit,
548 } => to_binary(&query_fixed_fees(deps, module_name, start_after, limit)?),
549 QueryMsg::TotalPercentageFees {
550 module_name,
551 start_after,
552 limit,
553 } => to_binary(&query_total_percentage_fees(
554 deps,
555 module_name,
556 start_after,
557 limit,
558 )?),
559 QueryMsg::TotalFixedFees {
560 module_name,
561 start_after,
562 limit,
563 } => to_binary(&query_total_fixed_fees(
564 deps,
565 module_name,
566 start_after,
567 limit,
568 )?),
569 QueryMsg::Keys {
570 fee_type,
571 start_after,
572 limit,
573 } => to_binary(&query_keys(deps, fee_type, start_after, limit)?),
574 QueryMsg::Operators {} => to_binary(&query_operators(deps)?),
575 }
576}
577
578fn query_config(deps: Deps) -> StdResult<ResponseWrapper<Config>> {
579 let config = CONFIG.load(deps.storage)?;
580 Ok(ResponseWrapper {
581 query: "config".to_string(),
582 data: config,
583 })
584}
585
586fn query_percentage_fee(
587 deps: Deps,
588 module_name: String,
589 fee_name: String,
590) -> StdResult<ResponseWrapper<PercentageFeeResponse>> {
591 let percentage_fee = PERCENTAGE_FEES.load(deps.storage, (&module_name, &fee_name))?;
592 Ok(ResponseWrapper {
593 query: "percentage_fee".to_string(),
594 data: PercentageFeeResponse {
595 module_name,
596 fee_name,
597 address: percentage_fee.address,
598 value: percentage_fee.value,
599 },
600 })
601}
602
603fn query_fixed_fee(
604 deps: Deps,
605 module_name: String,
606 fee_name: String,
607) -> StdResult<ResponseWrapper<FixedFeeResponse>> {
608 let fixed_fee = FIXED_FEES.load(deps.storage, (&module_name, &fee_name))?;
609 Ok(ResponseWrapper {
610 query: "fixed_fee".to_string(),
611 data: FixedFeeResponse {
612 module_name,
613 fee_name,
614 address: fixed_fee.address,
615 value: fixed_fee.value,
616 },
617 })
618}
619
620fn query_percentage_fees(
621 deps: Deps,
622 module_name: String,
623 start_after: Option<String>,
624 limit: Option<u32>,
625) -> StdResult<ResponseWrapper<Vec<PercentageFeeResponse>>> {
626 let limit = limit.unwrap_or(30) as usize;
627 let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
628
629 let percentage_fees = PERCENTAGE_FEES
630 .prefix(&module_name)
631 .range(deps.storage, start, None, Order::Ascending)
632 .take(limit)
633 .map(|item| {
634 let (fee_name, percentage_payment) = item.unwrap();
635 PercentageFeeResponse {
636 module_name: module_name.clone(),
637 fee_name,
638 address: percentage_payment.address,
639 value: percentage_payment.value,
640 }
641 })
642 .collect::<Vec<PercentageFeeResponse>>();
643
644 Ok(ResponseWrapper {
645 query: "percentage_fees".to_string(),
646 data: percentage_fees,
647 })
648}
649
650fn query_fixed_fees(
651 deps: Deps,
652 module_name: String,
653 start_after: Option<String>,
654 limit: Option<u32>,
655) -> StdResult<ResponseWrapper<Vec<FixedFeeResponse>>> {
656 let limit = limit.unwrap_or(30) as usize;
657 let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
658
659 let fixed_fees = FIXED_FEES
660 .prefix(&module_name)
661 .range(deps.storage, start, None, Order::Ascending)
662 .take(limit)
663 .map(|item| {
664 let (fee_name, fixed_payment) = item.unwrap();
665 FixedFeeResponse {
666 module_name: module_name.clone(),
667 fee_name,
668 address: fixed_payment.address,
669 value: fixed_payment.value,
670 }
671 })
672 .collect::<Vec<FixedFeeResponse>>();
673
674 Ok(ResponseWrapper {
675 query: "fixed_fees".to_string(),
676 data: fixed_fees,
677 })
678}
679
680fn query_total_percentage_fees(
681 deps: Deps,
682 module_name: String,
683 start_after: Option<String>,
684 limit: Option<u32>,
685) -> StdResult<ResponseWrapper<Decimal>> {
686 let limit = limit.unwrap_or(30) as usize;
687 let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
688
689 let total_percentage = PERCENTAGE_FEES
690 .prefix(&module_name)
691 .range(deps.storage, start, None, Order::Ascending)
692 .take(limit)
693 .map(|item| {
694 let (_, percentage_payment) = item.unwrap();
695 percentage_payment.value
696 })
697 .sum::<Decimal>();
698
699 Ok(ResponseWrapper {
700 query: "total_percentage_fees".to_string(),
701 data: total_percentage,
702 })
703}
704
705fn query_total_fixed_fees(
706 deps: Deps,
707 module_name: String,
708 start_after: Option<String>,
709 limit: Option<u32>,
710) -> StdResult<ResponseWrapper<Uint128>> {
711 let limit = limit.unwrap_or(30) as usize;
712 let start = start_after.map(|s| Bound::ExclusiveRaw(s.into()));
713
714 let total_fixed = FIXED_FEES
715 .prefix(&module_name)
716 .range(deps.storage, start, None, Order::Ascending)
717 .take(limit)
718 .map(|item| {
719 let (_, fixed_payment) = item.unwrap();
720 fixed_payment.value
721 })
722 .sum::<Uint128>();
723
724 Ok(ResponseWrapper {
725 query: "total_fixed_fees".to_string(),
726 data: total_fixed,
727 })
728}
729
730fn query_keys(
733 deps: Deps,
734 fee_type: Fees,
735 _start_after: Option<String>,
736 limit: Option<u32>,
737) -> StdResult<ResponseWrapper<Vec<String>>> {
738 let limit = limit.unwrap_or(30) as usize;
739 let modules = match fee_type {
742 Fees::Fixed => FIXED_FEES
743 .keys(deps.storage, None, None, Order::Descending)
744 .take(limit)
745 .map(|item| {
746 let (module_name, _) = item.unwrap();
747 module_name
748 })
749 .collect::<Vec<String>>(),
750 Fees::Percentage => PERCENTAGE_FEES
751 .keys(deps.storage, None, None, Order::Descending)
752 .take(limit)
753 .map(|item| {
754 let (module_name, _) = item.unwrap();
755 module_name
756 })
757 .collect::<Vec<String>>(),
758 };
759
760 Ok(ResponseWrapper {
761 query: "modules".to_string(),
762 data: modules,
763 })
764}
765
766fn query_operators(deps: Deps) -> StdResult<ResponseWrapper<Vec<String>>> {
767 let addrs = OPERATORS.may_load(deps.storage)?;
768 let addrs = match addrs {
769 Some(addrs) => addrs.iter().map(|a| a.to_string()).collect(),
770 None => vec![],
771 };
772 Ok(ResponseWrapper::new("operators", addrs))
773}