Stellar

How to read Human ID SBTs on Stellar

Users can opt to receive SBTs on Stellar. See Using Human ID with Stellar for user-facing instructions.

Off-chain

Use the Stellar SDK to simulate a read transaction to get the user's SBT.

import {
  Horizon,
  rpc,
  TransactionBuilder,
  Networks,
  Contract,
  scValToNative,
  nativeToScVal,
} from '@stellar/stellar-sdk'

/// Types ///
type StellarSbt = {
  action_nullifier: bigint,
  circuit_id: bigint,
  expiry: bigint,
  id: bigint,
  minter: string, // Stellar address
  public_values: Array<bigint>,
  recipient: string, // Stellar address
  revoked: boolean
}
type StellarSbtStatus = 'valid' | 'expired' | 'revoked' | 'none'
type GetStellarSBTRetVal = {
  sbt?: StellarSbt
  status: StellarSbtStatus
}

/// Constants ///
const horizonServerUrl = 'https://horizon.stellar.org'
const sorobanRpcUrl = 'https://mainnet.sorobanrpc.com'
const stellarSBTContractAddress = 'CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5'

/// Get SBT ///
async function getStellarSBTByAddress(
  address: string,
  circuitId: string
): Promise<GetStellarSBTRetVal | null> {
  const sorobanServer = new rpc.Server(sorobanRpcUrl)
  const userAccount = await sorobanServer.getAccount(address)
  const contract = new Contract(stellarSBTContractAddress)

  const operation = contract.call(
    'get_sbt',
    nativeToScVal(address, { type: 'address' }),
    nativeToScVal(circuitId, { type: 'u256' })
  )

  const transaction = new TransactionBuilder(userAccount, {
    Networks.PUBLIC,
    fee: '100',
  })
    .addOperation(operation)
    .setTimeout(60)
    .build()

  const simulationResponse = await sorobanServer.simulateTransaction(transaction)
  const parsed = rpc.parseRawSimulation(simulationResponse)

  // Happy path. User has the desired SBT.
  if (rpc.Api.isSimulationSuccess(simulationResponse)) {
    const _parsed = parsed as rpc.Api.SimulateTransactionSuccessResponse

    if (!_parsed.result) {
      throw new Error('Unexpected: Could not get "result" field from parsed Stellar transaction simulation for SBT query')
    }

    const sbt = scValToNative(_parsed.result?.retval)

    return {
      sbt,
      status: 'valid'
    }
  }
  // Error cases. User does not have the SBT.
  else if (rpc.Api.isSimulationError(simulationResponse)) {
    const _parsed = parsed as rpc.Api.SimulateTransactionErrorResponse

    // Error code 1 is "SBT not found"
    if (_parsed.error.includes('HostError: Error(Contract, #1)')) {
      return { status: 'none' }
    }

    // Error code 5 is "SBT revoked"
    if (_parsed.error.includes('HostError: Error(Contract, #5)')) {
      return { status: 'revoked' }
    }

    // Error code 6 is "SBT expired"
    if (_parsed.error.includes('HostError: Error(Contract, #6)')) {
      return { status: 'expired' }
    }

    throw new Error(`Stellar transaction simulation for SBT query failed: ${_parsed.error}`)
  } else {
    throw new Error('Unexpected: Stellar transaction simulation for SBT query returned an unexpected response')
  }
}

On-chain (Soroban)

1. Fetch the wasm for the Human ID SBT contract.

2. Incorporate into your Soroban contract.

This example contract has a get_sbt function that simply wraps the function from the SBT contract.

Check out the full Human ID SBT contract here.

For the list of circuit IDs, see here.

Last updated