Calling TezID

In the previous post here we introduced TezID and why we built it.

In this post we will outline how to use TezID.

We will use an example of a ICO contract that required participants to have 2 valid TezID proofs in order to qualify for registration. We will use SmartPy code in our examples.

Let's dive right into code!

## Types
#

TGetProofsRequestPayload = sp.TRecord(
    address=sp.TAddress, 
    callback_address=sp.TAddress, 
    callback_entrypoint=sp.TString
)
TGetProofsResponsePayload = sp.TRecord(
  address = sp.TAddress,
  proofs = sp.TMap(sp.TString, sp.TRecord(
    register_date = sp.TTimestamp, 
    verified = sp.TBool
  ))
)

## Contract
#

class ICO(sp.Contract):

  def __init__(self, tezid, requiredProofs):
    self.init(
      tezid = tezid,
      requiredProofs = requiredProofs,
      participants = {}
    )

  @sp.entry_point
  def signup(self):
    c = sp.contract(TGetProofsRequestPayload, self.data.tezid, entry_point="getProofs").open_some()
    sp.transfer(sp.record(address=sp.sender, callback_address=sp.self_address, callback_entrypoint="register"), sp.mutez(0), c)

  @sp.entry_point
  def register(self, ptr):
    sp.if sp.sender != self.data.tezid:
      sp.failwith('Only TezID can register')
    sp.set_type(ptr, TGetProofsResponsePayload)
    validProofs = sp.local("validProofs", [])
    sp.for requiredProof in self.data.requiredProofs:
      sp.if ptr.proofs.contains(requiredProof):
        sp.if ptr.proofs[requiredProof].verified:
          validProofs.value.push(requiredProof)
    sp.if sp.len(self.data.requiredProofs) == sp.len(validProofs.value):
      self.data.participants[ptr.address] = {}

Ok, so here we have some datatypes and a Tezos Smart Contract called ICO.

The datatypes TGetProofsRequestPayload and TGetProofsResponsePayload represent the structure of the data payload when calling and receiving a callback from TezID.

The ICO contract has the __init__ function to set some initial storage data. In addition is has two entrypoints; signup and register.

@__init__

Init is called when we create the contract. It sets some intial data for the contract;

* tezid ~ this is the address of the TezID contract
* requiredProofs ~ a list of required TezID proofs
* participants ~ the registered participants of the ICO

@signup

Signup can be called by anyone that attempts to sign up for the ICO. Signup will trigger a call to TezID's getProofs endpoint in order to retrieve the registered proofs for the calling address, and states that it would like it's own register endpoint to be called with this information (this is what we call a callback pattern, and it's how Tezos contracts can interact - atleast for now).

@register

Register is where TezID will call back with the registered proofs for some address. Note that it is set up to only allow TezID to call this endpoint. It received some proofs from TezID, compares to the required proofs set in requiredProofs, checks their validity and adds the address to the participants map if it all checks out.

And that is it 🎉

We now have created a contract that requires participants to have some proofs registered on TezID and we have more confidence that the registered participants are real people.

A SmartPy testcase

The test code for this ICO contract is quite long since it require all sorts of setup, but here it is if you are interested;

@sp.add_test(name = "Call TezID from other contract")
def test():
  admin = sp.test_account("admin")
  user = sp.test_account("User")
  user2 = sp.test_account("User2")
  user3 = sp.test_account("User3")
  cost = sp.tez(5)
  proof1 = sp.record(
    type = 'email'
  )
  proof2 = sp.record(
    type = 'phone'
  )
  proofVer1 = sp.record(
    tzaddr = user.address,
    type = 'email'
  )
  proofVer2 = sp.record(
    tzaddr = user.address,
    type = 'phone'
  )

  scenario = sp.test_scenario()
  c1 = TezID(admin.address, cost)
  scenario += c1
  c2 = ICO(c1.address, ["email","phone"])
  scenario += c2
  
  ## A user with the correct valid proofs can register as participant
  #
  scenario += c1.registerAddress().run(sender = user, amount = sp.tez(5))
  scenario += c1.registerProof(proof1).run(sender = user, amount = sp.tez(5))
  scenario += c1.registerProof(proof2).run(sender = user, amount = sp.tez(5))
  scenario += c1.verifyProof(proofVer1).run(sender = admin)
  scenario += c1.verifyProof(proofVer2).run(sender = admin)
  scenario += c2.signup().run(sender = user)
  scenario.verify(c2.data.participants.contains(user.address))
  
  ## A user without the correct valid proofs cannot register as participant
  #
  scenario += c1.registerAddress().run(sender = user2, amount = sp.tez(5))
  scenario += c1.registerProof(proof1).run(sender = user2, amount = sp.tez(5))
  scenario += c1.registerProof(proof2).run(sender = user2, amount = sp.tez(5))
  scenario += c1.verifyProof(proofVer1).run(sender = admin)
  scenario += c2.signup().run(sender = user2)
  scenario.verify(c2.data.participants.contains(user2.address) == False)
  
  ## A user not registered on TezID cannot register as participant
  #
  scenario += c2.signup().run(sender = user3)
  scenario.verify(c2.data.participants.contains(user3.address) == False)
  
  ## Only TezID can call register endpoiint
  #
  emailProof = sp.record(
      register_date = sp.timestamp(0),
      verified = True
  )
  phoneProof = sp.record(
      register_date = sp.timestamp(0),
      verified = True
  )
  proofs = {}
  proofs['email'] = emailProof
  proofs['phone'] = phoneProof
  pr = sp.record(address = user3.address, proofs = proofs)
  scenario += c2.register(pr).run(sender = user3, valid=False)

enjoy.