Bionttestnet
Address

octaNNhs…NNEhgC

octaNNhsFYn9JFbktDg9k7GbpB1BFEKAkcCbxRJgbNNEhgC
State
OCT balance
0OCT
wallet balance
Type
Contract
smart contract on chain
Chain-level
View on Octrascan · devnet
raw txs · nonce · pubkey
Pipoke (no profile)
this wallet has not registered a Pipoke profile
Contract
balance0 OCT
version1.0 Rehovot
code hash10cb39…6432fe
History
live · 20s · last 0
Source · ABI · Bytecode
✓ verified
expand →
✓ verified
contract Xcollection {

  struct TokenInfo {
    owner:        address
    minted_epoch: int
  }

  event Initialized(owner: address, name: string, max_supply: int)
  event Minted(minter: address, token_id: int, phase: int, price: int)
  event Transferred(token_id: int, from: address, to: address)
  event Approved(token_id: int, approved: address)
  event ApprovalRevoked(token_id: int)
  event OperatorSet(owner: address, operator: address, approved: int)
  event Revealed(base_uri: string)
  event BaseUriUpdated(base_uri: string)
  event PhaseOverrideSet(phase: int)
  event PhaseOverrideCleared()
  event ProceedsClaimed(recipient: address, amount: int)
  event XpectrumWalletUpdated(new_wallet: address)
  event OwnershipTransferred(old_owner: address, new_owner: address)

  const PLATFORM_FEE_BPS: int = 250

  const MAX_ROYALTY_BPS: int  = 1000

  state {
    owner:                address   // the collection creator
    admin:                address   // the Xpectrum operator (can assist but not withdraw)
    xpectrum_wallet:      address   // where platform fees land

    name:                 string
    symbol:               string
    base_uri:             string    // set on reveal
    unrevealed_uri:       string    // shown before reveal
    revealed:             int       // 0 = hidden, 1 = live

    royalty_bps:          int
    royalty_receiver:     address   // usually the creator

    max_supply:           int
    total_minted:         int

    max_per_wallet:       int
    minted_per_wallet:    map[address]int

    initialized:          int
    paused:               int

    phase_override_active: int
    phase_override_value:  int      // 3 = closed by default

    gtd_price:            int
    gtd_start_epoch:      int
    gtd_end_epoch:        int
    gtd_cap:              int
    gtd_minted:           int

    fcfs_price:           int
    fcfs_start_epoch:     int
    fcfs_end_epoch:       int
    fcfs_cap:             int
    fcfs_minted:          int

    public_price:         int
    public_start_epoch:   int
    public_end_epoch:     int
    public_cap:           int
    public_minted:        int

    xlist_addr:           address

    balances:             map[address]int
    tokens:               map[int]TokenInfo
    approvals:            map[int]address
    operator_approvals:   map[address]map[address]int

    reserve:              int
    pending_proceeds:     map[address]int
  }

  constructor() {
    self.owner                 = caller
    self.initialized           = 0
    self.revealed              = 0
    self.total_minted          = 0
    self.paused                = 0
    self.phase_override_active = 0
    self.phase_override_value  = 3
    self.reserve               = 0
  }

  private fn only_initialized() {
    require(self.initialized == 1, "xcollection: not initialized")
  }

  private fn only_owner_or_admin() {
    require(caller == self.owner || caller == self.admin, "xcollection: not authorized")
  }

  fn init(
    collection_name:          string,
    collection_symbol:        string,
    collection_unrevealed_uri: string,
    collection_max_supply:    int,
    collection_royalty_bps:   int,
    collection_max_per_wallet: int,
    xpectrum_addr:            address,   // operator wallet / fee destination
    creator:                  address,   // becomes the collection owner
    xlist:                    address    // the Xlist contract for this drop
  ): bool {
    require(self.initialized == 0,                        "xcollection: already initialized")
    require(caller == self.owner,                         "xcollection: not authorized")
    require(is_address(xpectrum_addr) && xpectrum_addr != 0, "xcollection: invalid platform addr")
    require(is_address(creator) && creator != 0,          "xcollection: invalid creator")
    require(is_address(xlist) && xlist != 0,              "xcollection: invalid xlist addr")
    require(collection_max_supply > 0,                    "xcollection: invalid supply")
    require(
      collection_royalty_bps >= 0 &&
      collection_royalty_bps <= MAX_ROYALTY_BPS,
      "xcollection: invalid royalty"
    )
    require(collection_max_per_wallet > 0,                "xcollection: invalid wallet cap")

    self.name              = collection_name
    self.symbol            = collection_symbol
    self.unrevealed_uri    = collection_unrevealed_uri
    self.max_supply        = collection_max_supply
    self.royalty_bps       = collection_royalty_bps
    self.max_per_wallet    = collection_max_per_wallet
    self.admin             = xpectrum_addr
    self.xpectrum_wallet   = xpectrum_addr
    self.royalty_receiver  = creator
    self.xlist_addr        = xlist

    self.initialized = 1
    self.owner       = creator

    emit Initialized(creator, collection_name, collection_max_supply)
    return true
  }

  view fn get_current_phase(): int {
    if self.phase_override_active == 1 {
      return self.phase_override_value
    }
    if epoch >= self.gtd_start_epoch && epoch < self.gtd_end_epoch {
      return 0
    }
    if epoch >= self.fcfs_start_epoch && epoch < self.fcfs_end_epoch {
      return 1
    }
    if epoch >= self.public_start_epoch && epoch < self.public_end_epoch {
      return 2
    }
    return 3
  }

  fn mint(quantity: int): bool {
    only_initialized()
    require(self.paused == 0,    "xcollection: minting paused")
    require(quantity > 0,        "xcollection: zero quantity")
    require(quantity <= 5,       "xcollection: max 5 per tx")
    require(
      self.total_minted + quantity <= self.max_supply,
      "xcollection: exceeds max supply"
    )
    require(
      self.minted_per_wallet[caller] + quantity <= self.max_per_wallet,
      "xcollection: wallet cap exceeded"
    )

    let phase = get_current_phase()
    require(phase != 3, "xcollection: minting closed")

    let price = 0

    if phase == 0 {
      let wl = call(self.xlist_addr, "is_whitelisted", 0, caller)
      require(wl == 1, "xcollection: not on GTD list")
      require(self.gtd_minted + quantity <= self.gtd_cap, "xcollection: GTD cap reached")
      price = self.gtd_price
      self.gtd_minted += quantity
    }
    if phase == 1 {
      let wl = call(self.xlist_addr, "is_whitelisted", 1, caller)
      require(wl == 1, "xcollection: not on FCFS list")
      require(self.fcfs_minted + quantity <= self.fcfs_cap, "xcollection: FCFS cap reached")
      price = self.fcfs_price
      self.fcfs_minted += quantity
    }
    if phase == 2 {
      require(self.public_minted + quantity <= self.public_cap, "xcollection: public cap reached")
      price = self.public_price
      self.public_minted += quantity
    }

    let cost = price * quantity
    require(value >= cost, "xcollection: insufficient payment")

    let fee              = cost * PLATFORM_FEE_BPS / 10000
    let creator_proceeds = cost - fee

    self.reserve += value
    self.pending_proceeds[self.owner]            += creator_proceeds
    self.pending_proceeds[self.xpectrum_wallet]  += fee

    self.minted_per_wallet[caller] += quantity
    self.balances[caller]          += quantity

    let start_id      = self.total_minted
    self.total_minted += quantity

    let idx = 0
    for idx in 0..quantity {
      self.tokens[start_id + idx].owner        = caller
      self.tokens[start_id + idx].minted_epoch = epoch
      emit Minted(caller, start_id + idx, phase, price)
      emit Transferred(start_id + idx, 0, caller)
    }

    if value > cost {
      let refund = value - cost
      self.reserve -= refund
      transfer(caller, refund)
    }

    return true
  }

  fn transfer(to: address, token_id: int): bool {
    only_initialized()
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")
    require(is_address(to) && to != 0,                     "xcollection: invalid address")

    let current_owner = self.tokens[token_id].owner
    require(caller == current_owner, "xcollection: not owner")
    require(to != current_owner,     "xcollection: already owner")

    self.tokens[token_id].owner   = to
    self.balances[current_owner] -= 1
    self.balances[to]            += 1
    self.approvals[token_id]      = 0

    emit Transferred(token_id, current_owner, to)
    return true
  }

  fn transfer_from(from: address, to: address, token_id: int): bool {
    only_initialized()
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")
    require(is_address(to) && to != 0,                     "xcollection: invalid address")

    let current_owner = self.tokens[token_id].owner
    require(from == current_owner, "xcollection: from is not owner")
    require(to != current_owner,   "xcollection: already owner")

    let approved    = self.approvals[token_id]
    let is_op       = self.operator_approvals[from][caller]
    require(
      caller == current_owner ||
      caller == approved      ||
      is_op == 1,
      "xcollection: not authorized"
    )

    self.tokens[token_id].owner   = to
    self.balances[from]          -= 1
    self.balances[to]            += 1
    self.approvals[token_id]      = 0

    emit Transferred(token_id, from, to)
    return true
  }

  fn approve(approved: address, token_id: int): bool {
    only_initialized()
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")
    require(is_address(approved) && approved != 0,         "xcollection: use revoke_approval to clear")

    let current_owner = self.tokens[token_id].owner
    let is_op         = self.operator_approvals[current_owner][caller]
    require(caller == current_owner || is_op == 1,         "xcollection: not authorized")

    self.approvals[token_id] = approved
    emit Approved(token_id, approved)
    return true
  }

  fn revoke_approval(token_id: int): bool {
    only_initialized()
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")

    let current_owner = self.tokens[token_id].owner
    let is_op         = self.operator_approvals[current_owner][caller]
    require(caller == current_owner || is_op == 1,         "xcollection: not authorized")

    self.approvals[token_id] = 0
    emit ApprovalRevoked(token_id)
    return true
  }

  fn set_operator(operator: address, approved: int): bool {
    only_initialized()
    require(operator != caller,                     "xcollection: cannot self-operate")
    require(is_address(operator) && operator != 0,  "xcollection: invalid operator")
    require(approved == 0 || approved == 1,          "xcollection: approved must be 0 or 1")

    self.operator_approvals[caller][operator] = approved
    emit OperatorSet(caller, operator, approved)
    return true
  }

  fn claim_proceeds(): bool {
    only_initialized()
    let amount = self.pending_proceeds[caller]
    require(amount > 0, "xcollection: nothing to claim")

    self.pending_proceeds[caller] = 0
    self.reserve                 -= amount
    transfer(caller, amount)

    emit ProceedsClaimed(caller, amount)
    return true
  }

  fn reveal(uri: string): bool {
    only_initialized()
    only_owner_or_admin()
    self.base_uri = uri
    self.revealed = 1
    emit Revealed(uri)
    return true
  }

  fn update_base_uri(uri: string): bool {
    only_initialized()
    only_owner_or_admin()
    self.base_uri = uri
    emit BaseUriUpdated(uri)
    return true
  }

  fn set_unrevealed_uri(uri: string): bool {
    only_initialized()
    only_owner_or_admin()
    self.unrevealed_uri = uri
    return true
  }

  fn update_phase_prices(gtd: int, fcfs: int, pub_price: int): bool {
    only_initialized()
    only_owner_or_admin()
    require(gtd >= 0 && fcfs >= 0 && pub_price >= 0, "xcollection: invalid price")
    self.gtd_price    = gtd
    self.fcfs_price   = fcfs
    self.public_price = pub_price
    return true
  }

  fn update_phase_caps(gtd: int, fcfs: int, pub_cap: int): bool {
    only_initialized()
    only_owner_or_admin()
    require(gtd >= 0 && fcfs >= 0 && pub_cap >= 0, "xcollection: invalid cap")
    require(gtd + fcfs + pub_cap <= self.max_supply, "xcollection: caps exceed max supply")
    self.gtd_cap    = gtd
    self.fcfs_cap   = fcfs
    self.public_cap = pub_cap
    return true
  }

  fn update_phase_epochs(
    gtd_start:  int,
    gtd_end:    int,
    fcfs_start: int,
    fcfs_end:   int,
    pub_start:  int,
    pub_end:    int
  ): bool {
    only_initialized()
    only_owner_or_admin()
    require(gtd_end >= gtd_start,     "xcollection: invalid GTD range")
    require(fcfs_end >= fcfs_start,   "xcollection: invalid FCFS range")
    require(pub_end >= pub_start,     "xcollection: invalid public range")
    require(fcfs_start >= gtd_end,    "xcollection: FCFS must start after GTD")
    require(pub_start >= fcfs_end,    "xcollection: public must start after FCFS")

    self.gtd_start_epoch    = gtd_start
    self.gtd_end_epoch      = gtd_end
    self.fcfs_start_epoch   = fcfs_start
    self.fcfs_end_epoch     = fcfs_end
    self.public_start_epoch = pub_start
    self.public_end_epoch   = pub_end
    return true
  }

  fn set_phase_override(phase_num: int): bool {
    only_initialized()
    only_owner_or_admin()
    require(phase_num >= 0 && phase_num <= 3, "xcollection: invalid phase")
    self.phase_override_active = 1
    self.phase_override_value  = phase_num
    emit PhaseOverrideSet(phase_num)
    return true
  }

  fn clear_phase_override(): bool {
    only_initialized()
    only_owner_or_admin()
    self.phase_override_active = 0
    self.phase_override_value  = 3
    emit PhaseOverrideCleared()
    return true
  }

  fn pause(): bool {
    only_initialized()
    only_owner_or_admin()
    self.paused = 1
    return true
  }

  fn unpause(): bool {
    only_initialized()
    only_owner_or_admin()
    self.paused = 0
    return true
  }

  fn set_max_per_wallet(max_val: int): bool {
    only_initialized()
    only_owner_or_admin()
    require(max_val > 0, "xcollection: invalid cap")
    self.max_per_wallet = max_val
    return true
  }

  fn update_xpectrum_wallet(new_wallet: address): bool {
    only_initialized()
    require(caller == self.admin, "xcollection: only admin")
    require(is_address(new_wallet) && new_wallet != 0, "xcollection: invalid address")
    self.xpectrum_wallet = new_wallet
    emit XpectrumWalletUpdated(new_wallet)
    return true
  }

  fn update_admin(new_admin: address): bool {
    only_initialized()
    require(caller == self.admin, "xcollection: only admin")
    require(is_address(new_admin) && new_admin != 0, "xcollection: invalid address")
    self.admin = new_admin
    return true
  }

  fn update_royalty_receiver(new_receiver: address): bool {
    only_initialized()
    require(
      caller == self.royalty_receiver || caller == self.owner,
      "xcollection: not authorized"
    )
    require(is_address(new_receiver) && new_receiver != 0, "xcollection: invalid address")
    self.royalty_receiver = new_receiver
    return true
  }

  fn transfer_ownership(new_owner: address): bool {
    only_initialized()
    require(caller == self.owner,                          "xcollection: not owner")
    require(is_address(new_owner) && new_owner != 0,       "xcollection: invalid address")
    let old      = self.owner
    self.owner   = new_owner
    emit OwnershipTransferred(old, new_owner)
    return true
  }

  fn update_xlist(new_xlist: address): bool {
    only_initialized()
    require(caller == self.admin, "xcollection: only admin")
    require(is_address(new_xlist) && new_xlist != 0, "xcollection: invalid address")
    self.xlist_addr = new_xlist
    return true
  }

  view fn owner_of(token_id: int): address {
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")
    return self.tokens[token_id].owner
  }

  view fn creator_of(token_id: int): address {
    return self.royalty_receiver
  }

  view fn royalty_of(token_id: int): int {
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")
    return self.royalty_bps
  }

  view fn token_uri(token_id: int): string {
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")
    if self.revealed == 1 {
      let uri = self.base_uri
      uri = concat(uri, to_string(token_id))
      uri = concat(uri, ".json")
      return uri
    }
    return self.unrevealed_uri
  }

  view fn collection_of(token_id: int): string {
    return self.name
  }

  view fn balance_of(addr: address): int {
    return self.balances[addr]
  }

  view fn get_approved(token_id: int): address {
    return self.approvals[token_id]
  }

  view fn total_supply(): int {
    return self.total_minted
  }

  view fn collection_name(): string {
    return self.name
  }

  view fn symbol(): string {
    return self.symbol
  }

  view fn is_approved_or_owner(token_id: int, addr: address): int {
    require(addr != 0,                                          "xcollection: zero address")
    require(token_id >= 0 && token_id < self.total_minted,     "xcollection: token not found")

    let current_owner = self.tokens[token_id].owner
    if addr == current_owner {
      return 1
    }
    if self.approvals[token_id] == addr {
      return 1
    }
    if self.operator_approvals[current_owner][addr] == 1 {
      return 1
    }
    return 0
  }

  view fn get_minted_by_wallet(addr: address): int {
    return self.minted_per_wallet[addr]
  }

  view fn get_pending_proceeds(addr: address): int {
    return self.pending_proceeds[addr]
  }

  view fn get_reserve(): int {
    return self.reserve
  }

  view fn get_token_info(token_id: int): string {
    require(token_id >= 0 && token_id < self.total_minted, "xcollection: token not found")

    let uri = self.unrevealed_uri
    if self.revealed == 1 {
      uri = concat(self.base_uri, to_string(token_id))
      uri = concat(uri, ".json")
    }

    let out = to_string(token_id)
    out = concat(out, "|")
    out = concat(out, to_string(self.tokens[token_id].owner))
    out = concat(out, "|")
    out = concat(out, to_string(self.royalty_receiver))
    out = concat(out, "|")
    out = concat(out, self.name)
    out = concat(out, "|")
    out = concat(out, to_string(self.royalty_bps))
    out = concat(out, "|")
    out = concat(out, to_string(self.tokens[token_id].minted_epoch))
    out = concat(out, "|")
    out = concat(out, uri)
    return out
  }

  view fn get_contract_info(): string {
    let out = self.name
    out = concat(out, "|")
    out = concat(out, self.symbol)
    out = concat(out, "|")
    out = concat(out, to_string(self.total_minted))
    out = concat(out, "|")
    out = concat(out, to_string(self.max_supply))
    out = concat(out, "|")
    out = concat(out, to_string(self.royalty_bps))
    out = concat(out, "|")
    out = concat(out, to_string(self.owner))
    out = concat(out, "|")
    out = concat(out, to_string(self.revealed))
    return out
  }

  view fn get_phase_info(): string {
    let out = to_string(self.gtd_price)
    out = concat(out, "|")
    out = concat(out, to_string(self.gtd_start_epoch))
    out = concat(out, "|")
    out = concat(out, to_string(self.gtd_end_epoch))
    out = concat(out, "|")
    out = concat(out, to_string(self.gtd_cap))
    out = concat(out, "|")
    out = concat(out, to_string(self.gtd_minted))
    out = concat(out, "|")
    out = concat(out, to_string(self.fcfs_price))
    out = concat(out, "|")
    out = concat(out, to_string(self.fcfs_start_epoch))
    out = concat(out, "|")
    out = concat(out, to_string(self.fcfs_end_epoch))
    out = concat(out, "|")
    out = concat(out, to_string(self.fcfs_cap))
    out = concat(out, "|")
    out = concat(out, to_string(self.fcfs_minted))
    out = concat(out, "|")
    out = concat(out, to_string(self.public_price))
    out = concat(out, "|")
    out = concat(out, to_string(self.public_start_epoch))
    out = concat(out, "|")
    out = concat(out, to_string(self.public_end_epoch))
    out = concat(out, "|")
    out = concat(out, to_string(self.public_cap))
    out = concat(out, "|")
    out = concat(out, to_string(self.public_minted))
    return out
  }
}