From Contracts to Map

·

3 min read

This feature will be implemented in the next Rhône upgrade.

Map simplifies smart contract code for accessing and storing data. ALPH.bet utilizes this feature to manage data like player and round information more efficiently. This article will explain the changes made to adapt the smart contract to use Mapping.

Read our articles about the dapp we developed: part 1, part 2, and part 3.

Overview of Changes

Each time someone places a bet, a contract is deployed with details like the bet amount and specifics. Several functions utilize subcontracts to check if someone has already bet in a round or to withdraw the bet once it's completed.

Previous Implementation Without Mapping

The following code demonstrates how these functions were handled without Mapping:

Checking if someone already played:

assert!(!contractExists!(subContractId!(bidderContractId)), ErrorCodes.AlreadyPlayed)

Source

Deploying a smart contract when someone bets:

let selfContractId = selfContractId!()
let (encodedImmFields, encodedMutFields) = PunterChoice.encodeFields!(selfContractId, caller, epoch, side, bidAmount, claimedByAnyoneTimestamp)
let _ = copyCreateSubContract!{caller -> ALPH: 1 alph}(bidderContractId, punterTemplateId, encodedImmFields, encodedMutFields)

Source

Withdrawing:

let betInfoContractId = subContractId!(SubContractTypes.PunterChoice ++ toByteVec!(addressToClaim) ++ epochNumber)
let roundContractId = subContractId!(SubContractTypes.RoundMultipleChoice ++ epochNumber) 
assert!(contractExists!(roundContractId), ErrorCodes.RoundNotExists)
assert!(contractExists!(betInfoContractId), ErrorCodes.PunterChoiceNotExists)

if (contractExists!(roundContractId) && contractExists!(betInfoContractId)){
    let betInfoCaller = getBetInfoByEpoch(addressToClaim, epochNumber)

    let addressPunterChoice = betInfoCaller.getAddress()
    let canBeClaimedAt = betInfoCaller.getClaimedByAnyone()

    if(addressToClaim != from){
       assert!(timestampNow > canBeClaimedAt, ErrorCodes.CannotBeClaimedYet)
    }

    let amountPunterChoice = betInfoCaller.getAmountBid()
    let upBid = betInfoCaller.getBid()

    let round = getRoundByEpochByteVec(epochNumber) 
    round.userClaimRewards(addressToClaim, amountPunterChoice, upBid)
    betInfoCaller.destroy(from)
}

Source

Managing subcontracts can become complex with numerous contracts, which is where mapping becomes useful. This feature reduces complexity and provides a good abstraction.

Key-value pairs are fundamental in computing, linking an identifier (key) to a piece of data (value). For example, the key might be CITY, and the value might be BERLIN. Mapping is a set of key-value pairs.

Alephium on Twitter

New Implementation with Mapping

For ALPH.bet, the major change was replacing contract management of players with a simple struct:

struct PunterChoice {
   predictionContractId: ByteVec,
   punterAddress: Address,
   epoch: U256,
   side: U256,
   amountBid: U256,
   claimedByAnyoneAt: U256
}

And adding this map at the beginning of the contract:

mapping[ByteVec, PunterChoice] punters

This simplifies checking if someone has already played:

assert!(!punters.contains!(bidderKey), ErrorCodes.AlreadyPlayed)

Previously, accessing player information involved computing the path and checking for subcontract existence:

fn getBetInfoByEpoch(from: Address, epochToGet: ByteVec) -> PunterChoice {
   let bidderContractId = subContractId!(SubContractTypes.PunterChoice ++ toByteVec!(from) ++ epochToGet)
   assert!(contractExists!(bidderContractId), ErrorCodes.PunterChoiceNotExists)

   return PunterChoice(bidderContractId)
}

This has been replaced by one line, with the subcontract ID becoming the map key:

let betInfoContractKey = SubContractTypes.PunterChoice ++ toByteVec!(addressToClaim) ++ epochNumber
let betInfoCaller = punters[betInfoContractKey]

So when the refactor is done, the withdrawing function will look like this. The example below demonstrate how easy it is to access to player information and delete (=get back locked ALPH) the entry.

let roundContractId = subContractId!(SubContractTypes.RoundMultipleChoice ++ epochNumber)
let betInfoContractKey = SubContractTypes.PunterChoice ++ toByteVec!(addressToClaim) ++ epochNumber
assert!(contractExists!(roundContractId), ErrorCodes.RoundNotExists)
assert!(punters.contains!(betInfoContractKey), ErrorCodes.PunterChoiceNotExists)

if (contractExists!(roundContractId) && contractExists!(betInfoContractKey)){
   let betInfoCaller = punters[betInfoContractKey]         
   let addressPunterChoice = betInfoCaller.punterAddress
   let canBeClaimedAt = betInfoCaller.claimedByAnyoneAt

   if(addressToClaim != from){
      assert!(timestampNow > canBeClaimedAt, ErrorCodes.CannotBeClaimedYet)
   }

   let amountPunterChoice = betInfoCaller.amountBid
   let upBid = betInfoCaller.side

   let round = getRoundByEpochByteVec(epochNumber)

   round.userClaimRewards(addressToClaim, amountPunterChoice, upBid)
   punters.remove!(from, betInfoContractKey)
}

Conclusion

This new feature significantly aids in writing cleaner and more optimized code. The abstraction enhances the developer experience by simplifying the logic behind managing and deploying subcontracts.

Although the smart contract isn't much shorter, it is cleaner and greatly reduces the complexity of using subcontracts with Ralph.



This version maintains the technical detail while improving readability and emphasizing the benefits of the changes.