Add sale_sealed (need to add test)
This commit is contained in:
parent
fba9fc57f3
commit
f0872f8ac6
|
@ -1,8 +1,10 @@
|
|||
use cosmwasm_std::{
|
||||
entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, StdResult,
|
||||
};
|
||||
use icco::common::CHAIN_ID;
|
||||
|
||||
use crate::{
|
||||
error::ContributorError,
|
||||
execute::{
|
||||
attest_contributions, claim_allocation, claim_refund, contribute, init_sale, sale_aborted,
|
||||
sale_sealed,
|
||||
|
@ -30,16 +32,23 @@ pub fn instantiate(
|
|||
) -> StdResult<Response> {
|
||||
let wormhole = deps.api.addr_validate(msg.wormhole.as_str())?;
|
||||
let token_bridge = deps.api.addr_validate(msg.token_bridge.as_str())?;
|
||||
let cfg = Config {
|
||||
wormhole,
|
||||
token_bridge,
|
||||
conductor_chain: msg.conductor_chain,
|
||||
conductor_address: msg.conductor_address.into(),
|
||||
owner: info.sender,
|
||||
};
|
||||
CONFIG.save(deps.storage, &cfg)?;
|
||||
|
||||
Ok(Response::default())
|
||||
// we know there is no terra conductor existing. So prevent user
|
||||
// from instantiating with one defined
|
||||
match msg.conductor_chain {
|
||||
CHAIN_ID => return ContributorError::UnsupportedConductor.std_err(),
|
||||
_ => {
|
||||
let cfg = Config {
|
||||
wormhole,
|
||||
token_bridge,
|
||||
conductor_chain: msg.conductor_chain,
|
||||
conductor_address: msg.conductor_address.into(),
|
||||
owner: info.sender,
|
||||
};
|
||||
CONFIG.save(deps.storage, &cfg)?;
|
||||
Ok(Response::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When CW20 transfers complete, we need to verify the actual amount that is being transferred out
|
||||
|
|
|
@ -48,15 +48,24 @@ pub enum ContributorError {
|
|||
#[error("SaleEnded")]
|
||||
SaleEnded,
|
||||
|
||||
#[error("SaleNonexistent")]
|
||||
SaleNonexistent,
|
||||
|
||||
#[error("SaleNotFinished")]
|
||||
SaleNotFinished,
|
||||
|
||||
#[error("SaleNotStarted")]
|
||||
SaleNotStarted,
|
||||
|
||||
#[error("SaleStillActive")]
|
||||
SaleStillActive,
|
||||
|
||||
#[error("TooManyAcceptedTokens")]
|
||||
TooManyAcceptedTokens,
|
||||
|
||||
#[error("UnsupportedConductor")]
|
||||
UnsupportedConductor,
|
||||
|
||||
#[error("WrongBuyerStatus")]
|
||||
WrongBuyerStatus,
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use cosmwasm_std::{
|
||||
to_binary, Addr, Binary, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QuerierWrapper,
|
||||
QueryRequest, Response, StdError, StdResult, Storage, Uint128, WasmMsg, WasmQuery,
|
||||
to_binary, Addr, Binary, Coin, CosmosMsg, Deps, DepsMut, Env, MessageInfo, QuerierWrapper,
|
||||
QueryRequest, Response, StdError, StdResult, Uint128, Uint256, WasmMsg, WasmQuery,
|
||||
};
|
||||
use cw20::Cw20ExecuteMsg;
|
||||
use cw20::{Cw20ExecuteMsg, Cw20QueryMsg, TokenInfoResponse};
|
||||
use serde::de::DeserializeOwned;
|
||||
use terraswap::{
|
||||
asset::AssetInfo,
|
||||
asset::{Asset, AssetInfo},
|
||||
querier::{query_balance, query_token_balance},
|
||||
};
|
||||
|
||||
|
@ -17,16 +17,20 @@ use wormhole::{
|
|||
state::ParsedVAA,
|
||||
};
|
||||
|
||||
use icco::common::{
|
||||
make_asset_info, ContributionsSealed, SaleAborted, SaleInit, SaleSealed, SaleStatus, CHAIN_ID,
|
||||
use icco::{
|
||||
common::{
|
||||
make_asset_info, to_uint128, AssetAllocation, ContributionsSealed, SaleAborted, SaleInit,
|
||||
SaleSealed, SaleStatus, CHAIN_ID,
|
||||
},
|
||||
error::CommonError,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::ContributorError,
|
||||
msg::ExecuteMsg,
|
||||
state::{
|
||||
is_sale_active, sale_asset_indices, update_buyer_contribution, AssetKey, BuyerStatus,
|
||||
PendingContributeToken, TokenIndexKey, ACCEPTED_ASSETS, ASSET_INDICES, CONFIG,
|
||||
sale_asset_indices, throw_if_active, throw_if_inactive, update_buyer_contribution,
|
||||
BuyerStatus, PendingContributeToken, TokenIndexKey, ACCEPTED_ASSETS, ASSET_INDICES, CONFIG,
|
||||
PENDING_CONTRIBUTE_TOKEN, SALES, SALE_STATUSES, SALE_TIMES, TOTAL_ALLOCATIONS,
|
||||
TOTAL_CONTRIBUTIONS,
|
||||
},
|
||||
|
@ -44,7 +48,7 @@ pub fn init_sale(
|
|||
let payload = parse_and_verify_vaa(deps.as_ref(), &env, signed_vaa)?;
|
||||
let sale_id = SaleInit::get_sale_id(&payload)?;
|
||||
|
||||
if is_sale_active(deps.storage, sale_id) {
|
||||
if let Ok(_) = SALE_STATUSES.load(deps.storage, sale_id) {
|
||||
return ContributorError::SaleAlreadyExists.std_err();
|
||||
}
|
||||
|
||||
|
@ -99,7 +103,6 @@ pub fn init_sale(
|
|||
|
||||
// store other things associated with accepted tokens
|
||||
TOTAL_CONTRIBUTIONS.save(deps.storage, token_key.clone(), &Uint128::zero())?;
|
||||
TOTAL_ALLOCATIONS.save(deps.storage, token_key, &Uint128::zero())?;
|
||||
}
|
||||
|
||||
let sale = &sale_init.core;
|
||||
|
@ -123,9 +126,7 @@ pub fn contribute(
|
|||
token_index: u8,
|
||||
amount: Uint128,
|
||||
) -> StdResult<Response> {
|
||||
if !is_sale_active(deps.storage, sale_id) {
|
||||
return ContributorError::SaleEnded.std_err();
|
||||
}
|
||||
throw_if_inactive(deps.storage, sale_id)?;
|
||||
|
||||
let times = SALE_TIMES.load(deps.storage, sale_id)?;
|
||||
let now = env.block.time.seconds();
|
||||
|
@ -158,15 +159,19 @@ pub fn attest_contributions(
|
|||
_info: MessageInfo,
|
||||
sale_id: &[u8],
|
||||
) -> StdResult<Response> {
|
||||
if !is_sale_active(deps.storage, sale_id) {
|
||||
return ContributorError::SaleEnded.std_err();
|
||||
}
|
||||
throw_if_inactive(deps.storage, sale_id)?;
|
||||
|
||||
let times = SALE_TIMES.load(deps.storage, sale_id)?;
|
||||
if env.block.time.seconds() <= times.end {
|
||||
return ContributorError::SaleNotFinished.std_err();
|
||||
}
|
||||
|
||||
// Do we care if the orchestrator can attest contributions multiple times?
|
||||
// The conductor can only collect contributions once per chain, so there
|
||||
// is no need to add a protection against attesting more than once.
|
||||
// If we want to add a protection, we can cache the serialized payload
|
||||
// and check if this is already in storage per sale_id.
|
||||
|
||||
let asset_indices = sale_asset_indices(deps.storage, sale_id);
|
||||
|
||||
let mut contribution_sealed = ContributionsSealed::new(sale_id, CHAIN_ID, asset_indices.len());
|
||||
|
@ -181,7 +186,7 @@ pub fn attest_contributions(
|
|||
|
||||
let cfg = CONFIG.load(deps.storage)?;
|
||||
Ok(Response::new()
|
||||
.add_message(execute_contract(
|
||||
.add_message(execute_contract_without_funds(
|
||||
&cfg.wormhole,
|
||||
to_binary(&WormholeExecuteMsg::PostMessage {
|
||||
message: Binary::from(serialized),
|
||||
|
@ -201,12 +206,9 @@ pub fn sale_sealed(
|
|||
let payload = parse_and_verify_vaa(deps.as_ref(), &env, signed_vaa)?;
|
||||
let sale_id = SaleSealed::get_sale_id(&payload)?;
|
||||
|
||||
let sale = match is_sale_active(deps.storage, sale_id) {
|
||||
true => SALES.load(deps.storage, sale_id)?,
|
||||
_ => {
|
||||
return ContributorError::SaleEnded.std_err();
|
||||
}
|
||||
};
|
||||
throw_if_inactive(deps.storage, sale_id)?;
|
||||
|
||||
let sale = SALES.load(deps.storage, sale_id)?;
|
||||
|
||||
// sale token handling
|
||||
let asset_info = match sale.token_chain {
|
||||
|
@ -221,17 +223,27 @@ pub fn sale_sealed(
|
|||
}
|
||||
};
|
||||
|
||||
let balance = match asset_info {
|
||||
let (balance, token_decimals) = match asset_info {
|
||||
AssetInfo::NativeToken { denom } => {
|
||||
// this should never happen, but...
|
||||
query_balance(&deps.querier, env.contract.address, denom)?
|
||||
(
|
||||
query_balance(&deps.querier, env.contract.address, denom)?,
|
||||
6u8,
|
||||
)
|
||||
}
|
||||
AssetInfo::Token { contract_addr } => {
|
||||
let contract_addr = Addr::unchecked(contract_addr);
|
||||
let token_info: TokenInfoResponse = query_contract(
|
||||
&deps.querier,
|
||||
&contract_addr,
|
||||
to_binary(&Cw20QueryMsg::TokenInfo {})?,
|
||||
)?;
|
||||
|
||||
let balance = query_token_balance(&deps.querier, contract_addr, env.contract.address)?;
|
||||
(balance, token_info.decimals)
|
||||
}
|
||||
AssetInfo::Token { contract_addr } => query_token_balance(
|
||||
&deps.querier,
|
||||
Addr::unchecked(contract_addr),
|
||||
env.contract.address,
|
||||
)?,
|
||||
};
|
||||
|
||||
// save some work if we don't have any of the sale tokens
|
||||
if balance == Uint128::zero() {
|
||||
return ContributorError::InsufficientSaleTokens.std_err();
|
||||
|
@ -243,10 +255,14 @@ pub fn sale_sealed(
|
|||
let parsed_allocations =
|
||||
SaleSealed::deserialize_allocations_safely(&payload, sale.num_accepted, &asset_indices)?;
|
||||
|
||||
// sum total allocations and check against balance in the contract
|
||||
// Sum total allocations and check against balance in the contract.
|
||||
// Keep in mind, these are Uint256. Need to adjust this down to Uint128
|
||||
let total_allocations = parsed_allocations
|
||||
.iter()
|
||||
.fold(Uint128::zero(), |total, (_, next)| total + next.allocated);
|
||||
.fold(Uint256::zero(), |total, (_, next)| total + next.allocated);
|
||||
|
||||
// need to adjust total_allocations based on decimal difference
|
||||
let total_allocations = sale.adjust_token_amount(total_allocations, token_decimals)?;
|
||||
|
||||
if balance < total_allocations {
|
||||
return ContributorError::InsufficientSaleTokens.std_err();
|
||||
|
@ -258,19 +274,51 @@ pub fn sale_sealed(
|
|||
// transfer contributions to conductor
|
||||
let cfg = CONFIG.load(deps.storage)?;
|
||||
if cfg.conductor_chain == CHAIN_ID {
|
||||
// lol
|
||||
} else {
|
||||
for &token_index in asset_indices.iter() {
|
||||
// do token bridge transfers here
|
||||
}
|
||||
return ContributorError::UnsupportedConductor.std_err();
|
||||
}
|
||||
|
||||
let mut token_bridge_msgs: Vec<CosmosMsg> = Vec::with_capacity(sale.num_accepted as usize);
|
||||
for &token_index in asset_indices.iter() {
|
||||
// bridge assets to conductor
|
||||
let amount = TOTAL_CONTRIBUTIONS.load(deps.storage, (sale_id, token_index.into()))?;
|
||||
let asset = Asset {
|
||||
info: ACCEPTED_ASSETS.load(deps.storage, (sale_id, token_index.into()))?,
|
||||
amount,
|
||||
};
|
||||
|
||||
token_bridge_msgs.push(execute_contract_without_funds(
|
||||
&cfg.token_bridge,
|
||||
to_binary(&TokenBridgeExecuteMsg::InitiateTransfer {
|
||||
asset,
|
||||
recipient_chain: cfg.conductor_chain,
|
||||
recipient: Binary::from(cfg.conductor_address.clone()),
|
||||
fee: Uint128::zero(), // does this matter?
|
||||
nonce: WORMHOLE_NONCE,
|
||||
})?,
|
||||
));
|
||||
}
|
||||
|
||||
// now update TOTAL_ALLOCATIONS (fix to use AssetAllocation)
|
||||
for (token_index, allocation) in parsed_allocations.iter() {
|
||||
//TOTAL_ALLOCATIONS.save(deps.storage, (sale_id, token_index.into()), )
|
||||
// adjust values from uint256 to uint128
|
||||
let allocated = sale.adjust_token_amount(allocation.allocated, token_decimals)?;
|
||||
let excess_contributed = match to_uint128(allocation.excess_contributed) {
|
||||
Some(value) => value,
|
||||
None => return CommonError::AmountExceedsUint128Max.std_err(),
|
||||
};
|
||||
|
||||
TOTAL_ALLOCATIONS.save(
|
||||
deps.storage,
|
||||
(sale_id, (*token_index).into()),
|
||||
&AssetAllocation {
|
||||
allocated,
|
||||
excess_contributed,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(Response::new()
|
||||
.add_messages(token_bridge_msgs)
|
||||
.add_attribute("action", "sale_sealed")
|
||||
.add_attribute("sale_id", Binary::from(sale_id).to_base64())
|
||||
.add_attribute("total_allocations", total_allocations.to_string()))
|
||||
|
@ -283,7 +331,7 @@ pub fn claim_allocation(
|
|||
sale_id: &Binary,
|
||||
token_index: u8,
|
||||
) -> StdResult<Response> {
|
||||
// TODO devs do something
|
||||
throw_if_active(deps.storage, sale_id)?;
|
||||
|
||||
Ok(Response::new().add_attribute("action", "claim_allocation"))
|
||||
}
|
||||
|
@ -297,9 +345,7 @@ pub fn sale_aborted(
|
|||
let payload = parse_and_verify_vaa(deps.as_ref(), &env, signed_vaa)?;
|
||||
let sale_id = SaleAborted::get_sale_id(&payload)?;
|
||||
|
||||
if !is_sale_active(deps.storage, sale_id) {
|
||||
return ContributorError::SaleEnded.std_err();
|
||||
};
|
||||
throw_if_inactive(deps.storage, sale_id)?;
|
||||
|
||||
SALE_STATUSES.save(deps.storage, sale_id, &SaleStatus::Aborted)?;
|
||||
|
||||
|
@ -315,7 +361,7 @@ pub fn claim_refund(
|
|||
sale_id: &Binary,
|
||||
token_index: u8,
|
||||
) -> StdResult<Response> {
|
||||
// TODO devs do something
|
||||
throw_if_active(deps.storage, sale_id)?;
|
||||
|
||||
Ok(Response::new().add_attribute("action", "claim_refund"))
|
||||
}
|
||||
|
@ -417,7 +463,7 @@ fn contribute_token(
|
|||
)?;
|
||||
|
||||
Ok(Response::new()
|
||||
.add_message(execute_contract(
|
||||
.add_message(execute_contract_without_funds(
|
||||
&contract_addr,
|
||||
to_binary(&Cw20ExecuteMsg::TransferFrom {
|
||||
owner: info.sender.to_string(),
|
||||
|
@ -425,7 +471,7 @@ fn contribute_token(
|
|||
amount,
|
||||
})?,
|
||||
))
|
||||
.add_message(execute_contract(
|
||||
.add_message(execute_contract_without_funds(
|
||||
&env.contract.address,
|
||||
to_binary(&ExecuteMsg::EscrowUserContributionHook)?,
|
||||
))
|
||||
|
@ -490,8 +536,17 @@ fn query_contract<T: DeserializeOwned>(
|
|||
}))
|
||||
}
|
||||
|
||||
// assume only one coin for funds
|
||||
fn execute_contract_with_funds(contract: &Addr, msg: Binary, funds: Coin) -> CosmosMsg {
|
||||
CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: contract.to_string(),
|
||||
funds: vec![funds],
|
||||
msg,
|
||||
})
|
||||
}
|
||||
|
||||
// no need for funds
|
||||
fn execute_contract(contract: &Addr, msg: Binary) -> CosmosMsg {
|
||||
fn execute_contract_without_funds(contract: &Addr, msg: Binary) -> CosmosMsg {
|
||||
CosmosMsg::Wasm(WasmMsg::Execute {
|
||||
contract_addr: contract.to_string(),
|
||||
funds: vec![],
|
||||
|
|
|
@ -3,7 +3,7 @@ use schemars::JsonSchema;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use terraswap::asset::AssetInfo;
|
||||
|
||||
use icco::common::{SaleCore, SaleStatus, SaleTimes};
|
||||
use icco::common::{AssetAllocation, SaleCore, SaleStatus, SaleTimes};
|
||||
|
||||
use crate::state::BuyerStatus;
|
||||
|
||||
|
@ -136,7 +136,7 @@ pub struct TotalContributionResponse {
|
|||
pub struct TotalAllocationResponse {
|
||||
pub id: Vec<u8>,
|
||||
pub token_index: u8,
|
||||
pub amount: Uint128,
|
||||
pub allocation: AssetAllocation,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
|
|
|
@ -60,7 +60,7 @@ pub fn query_total_allocation(
|
|||
Ok(TotalAllocationResponse {
|
||||
id: sale_id.to_vec(),
|
||||
token_index,
|
||||
amount: TOTAL_ALLOCATIONS.load(deps.storage, (sale_id, token_index.into()))?,
|
||||
allocation: TOTAL_ALLOCATIONS.load(deps.storage, (sale_id, token_index.into()))?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use schemars::JsonSchema;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use terraswap::asset::AssetInfo;
|
||||
|
||||
use icco::common::{SaleCore, SaleStatus, SaleTimes};
|
||||
use icco::common::{AssetAllocation, SaleCore, SaleStatus, SaleTimes};
|
||||
|
||||
use crate::error::ContributorError;
|
||||
|
||||
|
@ -73,7 +73,7 @@ pub const SALE_STATUSES: Map<SaleId, SaleStatus> = Map::new("sale_statuses");
|
|||
pub const SALE_TIMES: Map<SaleId, SaleTimes> = Map::new("sale_times");
|
||||
pub const ACCEPTED_ASSETS: Map<TokenIndexKey, AssetInfo> = Map::new("accepted_assets");
|
||||
pub const TOTAL_CONTRIBUTIONS: Map<TokenIndexKey, Uint128> = Map::new("total_contributions");
|
||||
pub const TOTAL_ALLOCATIONS: Map<TokenIndexKey, Uint128> = Map::new("total_allocations");
|
||||
pub const TOTAL_ALLOCATIONS: Map<TokenIndexKey, AssetAllocation> = Map::new("total_allocations");
|
||||
|
||||
// per buyer
|
||||
pub const BUYER_STATUSES: Map<BuyerTokenIndexKey, BuyerStatus> = Map::new("buyer_statuses");
|
||||
|
@ -175,10 +175,27 @@ pub fn refund_is_claimed(
|
|||
)
|
||||
}
|
||||
|
||||
pub fn is_sale_active(storage: &dyn Storage, sale_id: &[u8]) -> bool {
|
||||
pub fn throw_if_active(storage: &dyn Storage, sale_id: &[u8]) -> StdResult<()> {
|
||||
match SALE_STATUSES.load(storage, sale_id) {
|
||||
Ok(status) => status == SaleStatus::Active,
|
||||
Err(_) => false,
|
||||
Ok(active) => {
|
||||
if active == SaleStatus::Active {
|
||||
return ContributorError::SaleStillActive.std_err();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => return ContributorError::SaleNonexistent.std_err(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn throw_if_inactive(storage: &dyn Storage, sale_id: &[u8]) -> StdResult<()> {
|
||||
match SALE_STATUSES.load(storage, sale_id) {
|
||||
Ok(active) => {
|
||||
if active != SaleStatus::Active {
|
||||
return ContributorError::SaleEnded.std_err();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => return ContributorError::SaleNonexistent.std_err(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -208,7 +208,7 @@ fn init_sale() -> StdResult<()> {
|
|||
3057543273fcb20100000001000000020002000000000000000000000000f19a\
|
||||
2a01b70519f67adb309a994ec8c69a967e8b0000000000000003010100000000\
|
||||
0000000000000000000000000000000000000000000000000000000200000000\
|
||||
000000000000000083752ecafebf4707258dedffbd9c7443148169db00020000\
|
||||
000000000000000083752ecafebf4707258dedffbd9c7443148169db0002120000\
|
||||
000000000000000000000000000000000000000000000de0b6b3a76400000000\
|
||||
000000000000000000000000000000000000000000008ac7230489e800000000\
|
||||
00000000000000000000000000000000000000000000c249fdd3277800000000\
|
||||
|
@ -407,21 +407,6 @@ fn init_sale() -> StdResult<()> {
|
|||
Uint128::zero(),
|
||||
"total_contribution.amount != 0"
|
||||
);
|
||||
|
||||
let response = query(
|
||||
deps.as_ref(),
|
||||
mock_env(),
|
||||
QueryMsg::TotalAllocation {
|
||||
sale_id: Binary::from(sale_id),
|
||||
token_index,
|
||||
},
|
||||
)?;
|
||||
let total_allocation: TotalAllocationResponse = from_binary(&response)?;
|
||||
assert_eq!(
|
||||
total_allocation.amount,
|
||||
Uint128::zero(),
|
||||
"total_allocation.amount != 0"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -19,6 +19,7 @@ pub struct SaleTimes {
|
|||
pub struct SaleCore {
|
||||
pub token_address: Vec<u8>,
|
||||
pub token_chain: u16,
|
||||
pub token_decimals: u8,
|
||||
pub token_amount: Uint256,
|
||||
pub min_raise: Uint256,
|
||||
pub max_raise: Uint256,
|
||||
|
@ -50,7 +51,7 @@ pub struct Contribution {
|
|||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct Allocation {
|
||||
pub allocated: Uint256, // actually Uint128, but will be serialized as Uint256
|
||||
pub allocated: Uint256,
|
||||
pub excess_contributed: Uint256,
|
||||
}
|
||||
|
||||
|
@ -85,6 +86,24 @@ pub struct SaleAborted<'a> {
|
|||
pub id: &'a [u8],
|
||||
}
|
||||
|
||||
impl SaleCore {
|
||||
pub fn adjust_token_amount(&self, amount: Uint256, decimals: u8) -> StdResult<Uint128> {
|
||||
let adjusted: Uint256;
|
||||
if self.token_decimals > decimals {
|
||||
let x = self.token_decimals - decimals;
|
||||
adjusted = amount / Uint256::from(10u128).pow(x as u32);
|
||||
} else {
|
||||
let x = decimals - self.token_decimals;
|
||||
adjusted = amount * Uint256::from(10u128).pow(x as u32);
|
||||
}
|
||||
|
||||
match to_uint128(adjusted) {
|
||||
Some(value) => return Ok(value),
|
||||
None => return CommonError::AmountExceedsUint128Max.std_err(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AcceptedToken {
|
||||
pub const N_BYTES: usize = 50;
|
||||
|
||||
|
@ -107,14 +126,6 @@ impl AcceptedToken {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn to_const_bytes32(data: &[u8], index: usize) -> [u8; 32] {
|
||||
data.get_bytes32(index).get_const_bytes(0)
|
||||
}
|
||||
|
||||
pub fn to_u256(data: &[u8], index: usize) -> Uint256 {
|
||||
Uint256::new(to_const_bytes32(data, index))
|
||||
}
|
||||
|
||||
impl Contribution {
|
||||
pub const NUM_BYTES: usize = 33; // 1 + 32
|
||||
}
|
||||
|
@ -125,7 +136,7 @@ impl Allocation {
|
|||
|
||||
impl<'a> SaleInit<'a> {
|
||||
pub const PAYLOAD_ID: u8 = 1;
|
||||
const INDEX_ACCEPTED_TOKENS_START: usize = 227;
|
||||
const INDEX_ACCEPTED_TOKENS_START: usize = 228;
|
||||
|
||||
pub fn get_sale_id(data: &'a [u8]) -> StdResult<&[u8]> {
|
||||
match data[0] {
|
||||
|
@ -138,11 +149,12 @@ impl<'a> SaleInit<'a> {
|
|||
pub fn deserialize(id: &'a [u8], data: &'a [u8]) -> StdResult<Self> {
|
||||
let token_address = data.get_bytes32(33).to_vec();
|
||||
let token_chain = data.get_u16(65);
|
||||
let token_amount = to_u256(data, 67);
|
||||
let min_raise = to_u256(data, 99);
|
||||
let max_raise = to_u256(data, 131);
|
||||
let start = data.get_u64(163 + 24); // encoded as u256, but we only care about u64 time
|
||||
let end = data.get_u64(195 + 24); // encoded as u256, but we only care about u64 for time
|
||||
let token_decimals = data[67]; // TODO: double-check this is correct
|
||||
let token_amount = to_u256(data, 68);
|
||||
let min_raise = to_u256(data, 100);
|
||||
let max_raise = to_u256(data, 132);
|
||||
let start = data.get_u64(164 + 24); // encoded as u256, but we only care about u64 time
|
||||
let end = data.get_u64(196 + 24); // encoded as u256, but we only care about u64 for time
|
||||
|
||||
let accepted_tokens =
|
||||
SaleInit::deserialize_tokens(&data[SaleInit::INDEX_ACCEPTED_TOKENS_START..])?;
|
||||
|
@ -159,6 +171,7 @@ impl<'a> SaleInit<'a> {
|
|||
core: SaleCore {
|
||||
token_address,
|
||||
token_chain,
|
||||
token_decimals,
|
||||
token_amount,
|
||||
min_raise,
|
||||
max_raise,
|
||||
|
@ -208,8 +221,7 @@ impl<'a> ContributionsSealed<'a> {
|
|||
return Err(StdError::generic_err("cannot exceed length 256"));
|
||||
}
|
||||
|
||||
let result = contributions.iter().find(|c| c.token_index == token_index);
|
||||
if result != None {
|
||||
if let Some(_) = contributions.iter().find(|c| c.token_index == token_index) {
|
||||
return Err(StdError::generic_err(
|
||||
"token_index already in contributions",
|
||||
));
|
||||
|
@ -278,31 +290,19 @@ impl<'a> SaleSealed<'a> {
|
|||
data: &[u8],
|
||||
expected_num_allocations: u8,
|
||||
indices: &Vec<u8>,
|
||||
) -> StdResult<Vec<(u8, AssetAllocation)>> {
|
||||
) -> StdResult<Vec<(u8, Allocation)>> {
|
||||
if data[33] != expected_num_allocations {
|
||||
return Err(StdError::generic_err("encoded num_allocations != expected"));
|
||||
}
|
||||
|
||||
let mut parsed: Vec<(u8, AssetAllocation)> = Vec::with_capacity(indices.len());
|
||||
let mut parsed: Vec<(u8, Allocation)> = Vec::with_capacity(indices.len());
|
||||
for &token_index in indices {
|
||||
let i = SaleSealed::HEADER_LEN + (token_index as usize) * Allocation::NUM_BYTES;
|
||||
|
||||
// allocated
|
||||
let (invalid, allocated) = data.get_u256(i);
|
||||
if invalid > 0 {
|
||||
return Err(StdError::generic_err("allocated too large"));
|
||||
}
|
||||
|
||||
// excess_contribution
|
||||
let (invalid, excess_contributed) = data.get_u256(i + 32);
|
||||
if invalid > 0 {
|
||||
return Err(StdError::generic_err("excess_contributed too large"));
|
||||
}
|
||||
parsed.push((
|
||||
token_index,
|
||||
AssetAllocation {
|
||||
allocated: allocated.into(),
|
||||
excess_contributed: excess_contributed.into(),
|
||||
Allocation {
|
||||
allocated: to_u256(data, i + 1),
|
||||
excess_contributed: to_u256(data, i + 33),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
@ -337,6 +337,14 @@ impl<'a> SaleAborted<'a> {
|
|||
*/
|
||||
}
|
||||
|
||||
fn to_const_bytes32(data: &[u8], index: usize) -> [u8; 32] {
|
||||
data.get_bytes32(index).get_const_bytes(0)
|
||||
}
|
||||
|
||||
fn to_u256(data: &[u8], index: usize) -> Uint256 {
|
||||
Uint256::new(to_const_bytes32(data, index))
|
||||
}
|
||||
|
||||
pub fn make_asset_info(api: &dyn Api, addr: &[u8]) -> StdResult<AssetInfo> {
|
||||
match addr[0] {
|
||||
1u8 => {
|
||||
|
@ -361,3 +369,13 @@ pub fn make_asset_info(api: &dyn Api, addr: &[u8]) -> StdResult<AssetInfo> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_uint128(value: Uint256) -> Option<Uint128> {
|
||||
if value > Uint256::from(u128::MAX) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let bytes = value.to_be_bytes();
|
||||
let (_, value) = bytes.as_slice().get_u256(0);
|
||||
Some(value.into())
|
||||
}
|
||||
|
|
|
@ -3,6 +3,9 @@ use thiserror::Error;
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CommonError {
|
||||
#[error("AmountExceedsUint128Max")]
|
||||
AmountExceedsUint128Max,
|
||||
|
||||
#[error("InvalidVaaAction")]
|
||||
InvalidVaaAction,
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@ import {
|
|||
Wallet,
|
||||
} from "@terra-money/terra.js";
|
||||
import { readFileSync, writeFileSync, existsSync } from "fs";
|
||||
import path from "path";
|
||||
import { CustomError } from "ts-custom-error";
|
||||
|
||||
const DEFAULT_GAS_CURRENCY = "uusd";
|
||||
const DEFAULT_GAS_PRICE = 0.15;
|
||||
|
@ -49,10 +47,7 @@ export function newLocalClient(): Client {
|
|||
return newClient("localterra", undefined);
|
||||
}
|
||||
|
||||
export function newClient(
|
||||
network: string,
|
||||
mnemonic: string | undefined
|
||||
): Client {
|
||||
export function newClient(network: string, mnemonic: string | undefined): Client {
|
||||
if (network == "mainnet" || network == "testnet") {
|
||||
if (mnemonic === undefined) {
|
||||
throw Error("mnemonic undefined");
|
||||
|
@ -90,16 +85,6 @@ export async function sleep(timeout: number) {
|
|||
await new Promise((resolve) => setTimeout(resolve, timeout));
|
||||
}
|
||||
|
||||
export class TransactionError extends CustomError {
|
||||
public constructor(
|
||||
public code: number,
|
||||
public codespace: string | undefined,
|
||||
public rawLog: string
|
||||
) {
|
||||
super("transaction failed");
|
||||
}
|
||||
}
|
||||
|
||||
export async function createTransaction(wallet: Wallet, msg: Msg) {
|
||||
let gas_currency = process.env.GAS_CURRENCY! || DEFAULT_GAS_CURRENCY;
|
||||
let gas_price = process.env.GAS_PRICE! || DEFAULT_GAS_PRICE;
|
||||
|
@ -115,24 +100,12 @@ export async function broadcastTransaction(terra: LCDClient, signedTx: Tx) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export async function performTransaction(
|
||||
terra: LCDClient,
|
||||
wallet: Wallet,
|
||||
msg: Msg
|
||||
) {
|
||||
export async function performTransaction(terra: LCDClient, wallet: Wallet, msg: Msg) {
|
||||
const signedTx = await createTransaction(wallet, msg);
|
||||
const result = await broadcastTransaction(terra, signedTx);
|
||||
//if (isTxError(result)) {
|
||||
// throw new TransactionError(parseInt(result.code), result.codespace, result.raw_log);
|
||||
//}
|
||||
return result;
|
||||
return broadcastTransaction(terra, signedTx);
|
||||
}
|
||||
|
||||
export async function uploadContract(
|
||||
terra: LCDClient,
|
||||
wallet: Wallet,
|
||||
filepath: string
|
||||
) {
|
||||
export async function uploadContract(terra: LCDClient, wallet: Wallet, filepath: string) {
|
||||
const contract = readFileSync(filepath, "base64");
|
||||
const uploadMsg = new MsgStoreCode(wallet.key.accAddress, contract);
|
||||
const receipt = await performTransaction(terra, wallet, uploadMsg);
|
||||
|
@ -164,19 +137,14 @@ export async function instantiateContract(
|
|||
}
|
||||
|
||||
export async function executeContract(
|
||||
terra: LCDClient,
|
||||
//terra: LCDClient,
|
||||
wallet: Wallet,
|
||||
contractAddress: string,
|
||||
msg: object,
|
||||
coins?: Coins.Input
|
||||
) {
|
||||
const executeMsg = new MsgExecuteContract(
|
||||
wallet.key.accAddress,
|
||||
contractAddress,
|
||||
msg,
|
||||
coins
|
||||
);
|
||||
return await performTransaction(terra, wallet, executeMsg);
|
||||
const executeMsg = new MsgExecuteContract(wallet.key.accAddress, contractAddress, msg, coins);
|
||||
return await performTransaction(wallet.lcd, wallet, executeMsg);
|
||||
}
|
||||
|
||||
export async function queryContract(
|
||||
|
@ -195,13 +163,7 @@ export async function deployContract(
|
|||
initMsg: object
|
||||
) {
|
||||
const codeId = await uploadContract(terra, wallet, filepath);
|
||||
return await instantiateContract(
|
||||
terra,
|
||||
wallet,
|
||||
admin_address,
|
||||
codeId,
|
||||
initMsg
|
||||
);
|
||||
return await instantiateContract(terra, wallet, admin_address, codeId, initMsg);
|
||||
}
|
||||
|
||||
export async function migrate(
|
||||
|
@ -211,12 +173,7 @@ export async function migrate(
|
|||
newCodeId: number,
|
||||
msg: object
|
||||
) {
|
||||
const migrateMsg = new MsgMigrateContract(
|
||||
wallet.key.accAddress,
|
||||
contractAddress,
|
||||
newCodeId,
|
||||
msg
|
||||
);
|
||||
const migrateMsg = new MsgMigrateContract(wallet.key.accAddress, contractAddress, newCodeId, msg);
|
||||
return await performTransaction(terra, wallet, migrateMsg);
|
||||
}
|
||||
|
||||
|
@ -231,11 +188,7 @@ export async function update_contract_admin(
|
|||
contract_address: string,
|
||||
admin_address: string
|
||||
) {
|
||||
let msg = new MsgUpdateContractAdmin(
|
||||
wallet.key.accAddress,
|
||||
admin_address,
|
||||
contract_address
|
||||
);
|
||||
let msg = new MsgUpdateContractAdmin(wallet.key.accAddress, admin_address, contract_address);
|
||||
|
||||
return await performTransaction(terra, wallet, msg);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -14,8 +14,11 @@
|
|||
"bignumber.js": "^9.0.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"elliptic": "^6.5.4",
|
||||
"ethers": "^5.6.4",
|
||||
"prettier": "^2.6.1",
|
||||
"ts-custom-error": "^3.2.0",
|
||||
"web3-eth-abi": "^1.7.3",
|
||||
"web3-utils": "^1.7.3",
|
||||
"yargs": "^17.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -6,24 +6,38 @@ This is a work-in-progress.
|
|||
|
||||
Tests will cover the following scenarios in `contributor.ts` (see [whitepaper](../../../WHITEPAPER.md) for details on how the Contributor works).
|
||||
|
||||
### Preparation
|
||||
|
||||
- [x] Mint CW20 Mock Token
|
||||
- [x] Query Balance of Mock Token
|
||||
|
||||
### Deployment
|
||||
|
||||
- [x] Deploy Contract
|
||||
- [ ] Expect Error when Non-Owner Attempts to Upgrade Contract
|
||||
- [ ] Non-Owner Cannot Upgrade Contract
|
||||
- [ ] Upgrade Contract
|
||||
|
||||
### Conduct Successful Sale
|
||||
|
||||
- [ ] Orchestrator Initializes Sale
|
||||
- [ ] User Contributes to Sale
|
||||
- [ ] Orchestrator Attests Contributions
|
||||
- [ ] Orchestrator Seals Sale
|
||||
- [ ] User Claims Allocations
|
||||
- [x] 1. Orchestrator Initializes Sale
|
||||
- [x] 2. Orchestrator Cannot Intialize Sale Again
|
||||
- [x] 3. User Contributes to Sale (Native)
|
||||
- [x] 4. User Contributes to Sale (CW20)
|
||||
- [x] 5. User Cannot Contribute for Non-existent Token Index
|
||||
- [x] 6. Orchestrator Cannot Attest Contributions Too Early
|
||||
- [x] 7. User Cannot Contribute After Sale Ended
|
||||
- [x] 8. Orchestrator Attests Contributions
|
||||
- [ ] 9. Orchestrator Seals Sale
|
||||
- [ ] 10. Orchestrator Cannot Seal Sale Again
|
||||
- [ ] 11. User Claims Allocations
|
||||
- [ ] 12. User Cannot Claim Allocations Again
|
||||
|
||||
### Conduct Aborted Sale
|
||||
|
||||
- [ ] Orchestrator Initializes Sale
|
||||
- [ ] User Contributes to Sale
|
||||
- [ ] Orchestrator Attests Contributions
|
||||
- [ ] Orchestrator Aborts Sale
|
||||
- [ ] User Claims Refunds
|
||||
- [x] 1. Orchestrator Initializes Sale
|
||||
- [x] 2. User Contributes to Sale (Native)
|
||||
- [x] 3. User Contributes to Sale (CW20)
|
||||
- [ ] 4. Orchestrator Aborts Sale
|
||||
- [ ] 5. Orchestrator Cannot Abort Sale Again
|
||||
- [ ] 6. User Claims Refunds
|
||||
- [ ] 7. User Cannot Claims Refunds Again
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,298 @@
|
|||
import { Int, LCDClient, LocalTerra, Wallet } from "@terra-money/terra.js";
|
||||
import { BigNumber, BigNumberish } from "ethers";
|
||||
import { soliditySha3 } from "web3-utils";
|
||||
import { queryContract } from "../helpers";
|
||||
|
||||
const elliptic = require("elliptic");
|
||||
|
||||
// sale struct info
|
||||
const NUM_BYTES_ACCEPTED_TOKEN = 50;
|
||||
const NUM_BYTES_ALLOCATION = 65;
|
||||
|
||||
// conductor info
|
||||
export const CONDUCTOR_CHAIN = 2;
|
||||
export const CONDUCTOR_ADDRESS = "000000000000000000000000f19a2a01b70519f67adb309a994ec8c69a967e8b";
|
||||
|
||||
export interface Actors {
|
||||
owner: Wallet;
|
||||
seller: Wallet;
|
||||
buyers: Wallet[];
|
||||
}
|
||||
|
||||
export function makeClientAndWallets(): [LCDClient, Actors] {
|
||||
const terra = new LocalTerra();
|
||||
const wallets = terra.wallets;
|
||||
return [
|
||||
terra,
|
||||
{
|
||||
owner: wallets.test1,
|
||||
seller: wallets.test2,
|
||||
buyers: [wallets.test3, wallets.test4],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function signAndEncodeConductorVaa(
|
||||
timestamp: number,
|
||||
nonce: number,
|
||||
sequence: number,
|
||||
data: Buffer
|
||||
): Buffer {
|
||||
return signAndEncodeVaaBeta(
|
||||
//return signAndEncodeVaaLegacy(
|
||||
timestamp,
|
||||
nonce,
|
||||
CONDUCTOR_CHAIN,
|
||||
CONDUCTOR_ADDRESS,
|
||||
sequence,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export function signAndEncodeVaaBeta(
|
||||
timestamp: number,
|
||||
nonce: number,
|
||||
emitterChainId: number,
|
||||
emitterAddress: string,
|
||||
sequence: number,
|
||||
data: Buffer
|
||||
): Buffer {
|
||||
if (Buffer.from(emitterAddress, "hex").length != 32) {
|
||||
throw Error("emitterAddress != 32 bytes");
|
||||
}
|
||||
|
||||
// wormhole initialized with only one guardian in devnet
|
||||
const signers = ["cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"];
|
||||
|
||||
const sigStart = 6;
|
||||
const numSigners = signers.length;
|
||||
const sigLength = 66;
|
||||
const bodyStart = sigStart + sigLength * numSigners;
|
||||
const bodyHeaderLength = 51;
|
||||
const vm = Buffer.alloc(bodyStart + bodyHeaderLength + data.length);
|
||||
|
||||
// header
|
||||
const guardianSetIndex = 0;
|
||||
|
||||
vm.writeUInt8(1, 0);
|
||||
vm.writeUInt32BE(guardianSetIndex, 1);
|
||||
vm.writeUInt8(numSigners, 5);
|
||||
|
||||
// encode body with arbitrary consistency level
|
||||
const consistencyLevel = 1;
|
||||
|
||||
vm.writeUInt32BE(timestamp, bodyStart);
|
||||
vm.writeUInt32BE(nonce, bodyStart + 4);
|
||||
vm.writeUInt16BE(emitterChainId, bodyStart + 8);
|
||||
vm.write(emitterAddress, bodyStart + 10, "hex");
|
||||
vm.writeBigUInt64BE(BigInt(sequence), bodyStart + 42);
|
||||
vm.writeUInt8(consistencyLevel, bodyStart + 50);
|
||||
vm.write(data.toString("hex"), bodyStart + bodyHeaderLength, "hex");
|
||||
|
||||
// signatures
|
||||
const body = vm.subarray(bodyStart).toString("hex");
|
||||
const hash = soliditySha3(soliditySha3("0x" + body)!)!.substring(2);
|
||||
|
||||
for (let i = 0; i < numSigners; ++i) {
|
||||
const ec = new elliptic.ec("secp256k1");
|
||||
const key = ec.keyFromPrivate(signers[i]);
|
||||
const signature = key.sign(hash, { canonical: true });
|
||||
|
||||
const start = sigStart + i * sigLength;
|
||||
vm.writeUInt8(i, start);
|
||||
vm.write(signature.r.toString(16).padStart(64, "0"), start + 1, "hex");
|
||||
vm.write(signature.s.toString(16).padStart(64, "0"), start + 33, "hex");
|
||||
vm.writeUInt8(signature.recoveryParam, start + 65);
|
||||
//console.log(" beta signature", vm.subarray(start, start + 66).toString("hex"));
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
||||
export interface AcceptedToken {
|
||||
address: string; // 32 bytes
|
||||
chain: number; // uint16
|
||||
conversionRate: string; // uint128
|
||||
}
|
||||
|
||||
export function encodeAcceptedTokens(acceptedTokens: AcceptedToken[]): Buffer {
|
||||
const n = acceptedTokens.length;
|
||||
const encoded = Buffer.alloc(NUM_BYTES_ACCEPTED_TOKEN * n);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const token = acceptedTokens[i];
|
||||
const start = i * NUM_BYTES_ACCEPTED_TOKEN;
|
||||
encoded.write(token.address, start, "hex");
|
||||
encoded.writeUint16BE(token.chain, start + 32);
|
||||
encoded.write(toBigNumberHex(token.conversionRate, 16), start + 34, "hex");
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
export function encodeSaleInit(
|
||||
saleId: number,
|
||||
tokenAddress: string, // 32 bytes
|
||||
tokenChain: number,
|
||||
tokenAmount: string, // uint256
|
||||
minRaise: string, // uint256
|
||||
maxRaise: string, // uint256
|
||||
saleStart: number,
|
||||
saleEnd: number,
|
||||
acceptedTokens: AcceptedToken[], // 50 * n_tokens
|
||||
recipient: string, // 32 bytes
|
||||
refundRecipient: string // 32 bytes
|
||||
): Buffer {
|
||||
const numTokens = acceptedTokens.length;
|
||||
const encoded = Buffer.alloc(292 + numTokens * NUM_BYTES_ACCEPTED_TOKEN);
|
||||
|
||||
encoded.writeUInt8(1, 0); // initSale payload = 1
|
||||
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
|
||||
encoded.write(tokenAddress, 33, "hex");
|
||||
encoded.writeUint16BE(tokenChain, 65);
|
||||
encoded.write(toBigNumberHex(tokenAmount, 32), 67, "hex");
|
||||
encoded.write(toBigNumberHex(minRaise, 32), 99, "hex");
|
||||
encoded.write(toBigNumberHex(maxRaise, 32), 131, "hex");
|
||||
encoded.write(toBigNumberHex(saleStart, 32), 163, "hex");
|
||||
encoded.write(toBigNumberHex(saleEnd, 32), 195, "hex");
|
||||
encoded.writeUInt8(numTokens, 227);
|
||||
encoded.write(encodeAcceptedTokens(acceptedTokens).toString("hex"), 228, "hex");
|
||||
|
||||
const recipientIndex = 228 + numTokens * NUM_BYTES_ACCEPTED_TOKEN;
|
||||
encoded.write(recipient, recipientIndex, "hex");
|
||||
encoded.write(refundRecipient, recipientIndex + 32, "hex");
|
||||
return encoded;
|
||||
}
|
||||
|
||||
export interface Allocation {
|
||||
allocation: BigNumber; // uint256
|
||||
excessContribution: BigNumber; // uint256
|
||||
}
|
||||
|
||||
export function encodeAllocations(allocations: Allocation[]): Buffer {
|
||||
const n = allocations.length;
|
||||
const encoded = Buffer.alloc(NUM_BYTES_ALLOCATION * n);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const item = allocations[i];
|
||||
const start = i * NUM_BYTES_ALLOCATION;
|
||||
encoded.writeUint8(i, start);
|
||||
encoded.write(toBigNumberHex(item.allocation, 32), start + 1, "hex");
|
||||
encoded.write(toBigNumberHex(item.excessContribution, 32), start + 33, "hex");
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
export function encodeSaleSealed(
|
||||
saleId: number,
|
||||
allocations: Allocation[] // 65 * n_allocations
|
||||
): Buffer {
|
||||
const headerLen = 33;
|
||||
const numAllocations = allocations.length;
|
||||
const encoded = Buffer.alloc(headerLen + numAllocations * NUM_BYTES_ALLOCATION);
|
||||
|
||||
encoded.writeUInt8(3, 0); // saleSealed payload = 3
|
||||
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
|
||||
encoded.write(encodeAllocations(allocations).toString("hex"), headerLen, "hex");
|
||||
|
||||
return encoded;
|
||||
}
|
||||
|
||||
export function encodeSaleAborted(saleId: number): Buffer {
|
||||
const encoded = Buffer.alloc(33);
|
||||
encoded.writeUInt8(4, 0); // saleSealed payload = 4
|
||||
encoded.write(toBigNumberHex(saleId, 32), 1, "hex");
|
||||
return encoded;
|
||||
}
|
||||
|
||||
function toBigNumberHex(value: BigNumberish, numBytes: number): string {
|
||||
return BigNumber.from(value)
|
||||
.toHexString()
|
||||
.substring(2)
|
||||
.padStart(numBytes * 2, "0");
|
||||
}
|
||||
|
||||
// misc
|
||||
export async function getBlockTime(terra: LCDClient): Promise<number> {
|
||||
const info = await terra.tendermint.blockInfo();
|
||||
const time = new Date(info.block.header.time);
|
||||
return Math.floor(time.getTime() / 1000);
|
||||
}
|
||||
|
||||
export function getErrorMessage(error: any): string {
|
||||
return error.response.data.message;
|
||||
}
|
||||
|
||||
// contract queries
|
||||
export async function getBalance(terra: LCDClient, asset: string, account: string): Promise<Int> {
|
||||
if (asset.startsWith("u")) {
|
||||
const [balance] = await terra.bank.balance(account);
|
||||
const coin = balance.get(asset);
|
||||
if (coin !== undefined) {
|
||||
return new Int(coin.amount);
|
||||
}
|
||||
return new Int(0);
|
||||
}
|
||||
|
||||
const msg: any = {
|
||||
balance: {
|
||||
address: account,
|
||||
},
|
||||
};
|
||||
const result = await queryContract(terra, asset, msg);
|
||||
return new Int(result.balance);
|
||||
}
|
||||
|
||||
export async function getBuyerStatus(
|
||||
terra: LCDClient,
|
||||
contributor: string,
|
||||
saleId: Buffer,
|
||||
tokenIndex: number,
|
||||
buyer: string
|
||||
): Promise<any> {
|
||||
const msg: any = {
|
||||
buyer_status: {
|
||||
sale_id: saleId.toString("base64"),
|
||||
token_index: tokenIndex,
|
||||
buyer,
|
||||
},
|
||||
};
|
||||
const result = await queryContract(terra, contributor, msg);
|
||||
|
||||
// verify header
|
||||
for (let i = 0; i < 32; ++i) {
|
||||
if (result.id[i] != saleId[i]) {
|
||||
throw Error("id != expected");
|
||||
}
|
||||
}
|
||||
if (result.token_index != tokenIndex) {
|
||||
throw Error("token_index != expected");
|
||||
}
|
||||
if (result.buyer != buyer) {
|
||||
throw Error("buyer != expected");
|
||||
}
|
||||
return result.status;
|
||||
}
|
||||
|
||||
export async function getTotalContribution(
|
||||
terra: LCDClient,
|
||||
contributor: string,
|
||||
saleId: Buffer,
|
||||
tokenIndex: number
|
||||
): Promise<Int> {
|
||||
const msg: any = {
|
||||
total_contribution: {
|
||||
sale_id: saleId.toString("base64"),
|
||||
token_index: tokenIndex,
|
||||
},
|
||||
};
|
||||
const result = await queryContract(terra, contributor, msg);
|
||||
|
||||
// verify header
|
||||
for (let i = 0; i < 32; ++i) {
|
||||
if (result.id[i] != saleId[i]) {
|
||||
throw Error("id != expected");
|
||||
}
|
||||
}
|
||||
if (result.token_index != tokenIndex) {
|
||||
throw Error("token_index != expected");
|
||||
}
|
||||
return new Int(result.amount);
|
||||
}
|
Loading…
Reference in New Issue