Human ID (by human.tech)
  • Introduction
    • Private Credentials
    • The Issue of Regulatory Compliance & Non-Consensual Data Sharing
    • Need Support?
  • For Users
    • FAQs
    • Verifying Identity with Human ID
    • Verifying ePassport
    • Using Human ID with NEAR
    • Using Human ID with Stellar
    • Getting Refunded
  • For Developers
    • Integrating Human ID
    • Custom Sybil Resistance
    • API Reference
    • Dry Runs
    • Sign Protocol Attestations
    • Verax Attestations
    • Off-Chain Proofs
    • Clean Hands Attestations
  • Stellar
  • For Node Operators
    • Run an Observer
  • Architecture
    • Overview
    • Flow of Data
    • Flow of Data: KYC Proof of Personhood
    • Where Data Is(n't) Stored
    • VOLE-based ZK
    • On-Chain Proofs
    • Clean Hands Architecture
  • How it Works
    • Modularity of the Stack
    • Issuer
    • Credentials
    • Hub
Powered by GitBook
On this page
  • Off-chain
  • On-chain (Soroban)
Export as PDF

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.

soroban contract fetch --id CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5 -o ./human_id_sbt_contract.wasm --network mainnet --network-passphrase "Public Global Stellar Network ; September 2015" --rpc-url https://mainnet.sorobanrpc.com

2. Incorporate into your Soroban contract.

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

#![no_std]
use soroban_sdk::{contract, contractimpl, Env, Address, U256};

mod human_id_sbt_contract {
    soroban_sdk::contractimport!(
        file = "../../human_id_sbt_contract.wasm"
    );
}

#[contract]
pub struct MyContract;

#[contractimpl]
impl MyContract {
    /// * `recipient` - The address of the SBT recipient.
    /// * `circuit_id` - This determines the type of SBT to look up. For example, KYC, ePassport, and phone number
    ///   SBTs each have a different circuit ID.
    pub fn get_sbt(env: Env, recipient: Address, circuit_id: U256) -> human_id_sbt_contract::SBT {
        let human_id_sbt_contract_addr = Address::from_str(&env, "CCNTHEVSWNDOQAMXXHFOLQIXWUINUPTJIM6AXFSKODNVXWA4N7XV3AI5");
        let human_id_sbt_client = human_id_sbt_contract::Client::new(&env, &human_id_sbt_contract_addr);

        human_id_sbt_client.get_sbt(&recipient, &circuit_id)
    }
}
PreviousClean Hands AttestationsNextRun an Observer

Last updated 9 days ago

Check out the full .

For the list of circuit IDs, see .

Human ID SBT contract here
here