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
    • 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
  • 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 with Sign Protocol
  • On Optimism with Sign Protocol
  • On-chain (not Optimism)
  • Sui SBTs
  • Decrypt
Export as PDF
  1. For Developers

Clean Hands Attestations

How to read Clean Hands attestations

PreviousOff-Chain ProofsNextRun an Observer

Last updated 23 days ago

Human ID issues its Clean Hands attestation to users who prove that they are not on any sanctions lists (see all the lists here: ).

Off-chain with Sign Protocol

Use Sign Protocol's scan API to see whether a user has a valid Clean Hands attestation.

// Set user address
const address = '0x123'

const resp = await fetch(`https://mainnet-rpc.sign.global/api/scan/addresses/${address}/attestations`)
const data = await resp.json()
const cleanHandsAttestations = data.data.rows.filter((att) => (
  att.fullSchemaId == 'onchain_evm_10_0x8' &&
  att.attester == '0xB1f50c6C34C72346b1229e5C80587D0D659556Fd' &&
  att.isReceiver == true && 
  !att.revoked &&
  att.validUntil > (new Date().getTime() / 1000)
))
const hasValidAttestation = cleanHandsAttestations.length > 0

On Optimism with Sign Protocol

Tutorial coming soon...

On-chain (not Optimism)

Use our off-chain attester, and verify its signature on-chain. Our attester returns a signature of the circuit ID, action ID, and user address, if the address has a clean hands attestation.

Our attester address is 0xa74772264f896843c6346ceA9B13e0128A1d3b5D.

Query for signature

// Set user address
const userAddress = '0x123'

const actionId = 123456789
const resp = await fetch(`https://api.holonym.io/attestation/sbts/clean-hands?action-id=${actionId}&address=${userAddress}`)
const { isUnique, signature, circuitId } = await resp.json();

Verify signature in Solidity

Warning: this Solidity code is untested.

pragma solidity ^0.8.4;

import "openzeppelin-contracts/contracts/utils/cryptography/ECDSA.sol";

contract SignatureVerification {
    using ECDSA for bytes32;

    address public humanIdAttester = 0xa74772264f896843c6346ceA9B13e0128A1d3b5D;

    function verifySignature(
        uint256 circuitId,
        uint256 actionId,
        address userAddress,
        bytes memory signature
    ) public pure returns (bool) {
        bytes32 digest = keccak256(abi.encodePacked(
            circuitId,
            actionId,
            userAddress
        ));

        bytes32 personalSignPreimage = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            digest
        ));

        (
            address recovered,
            ECDSA.RecoverError err,
            bytes32 _sig
        ) = ECDSA.tryRecover(personalSignPreimage, publicSignalsSig);

        return recovered == humanIdAttester;
    }
}

Verify signature in JavaScript

// Verify using ethers v5
const digest = ethers.utils.solidityKeccak256(
  ["uint256", "uint256", "address"],
  [circuitId, actionId, userAddress]
);
const personalSignPreimage = ethers.utils.solidityKeccak256(
  ["string", "bytes32"],
  ["\x19Ethereum Signed Message:\n32", digest]
);
const recovered = ethers.utils.recoverAddress(personalSignPreimage, signature)
console.log(recovered === '0xa74772264f896843c6346ceA9B13e0128A1d3b5D')

Sui SBTs

We also allow users to mint SBTs on Sui. The following code verifies that the address possesses a Clean Hands SBT on Sui.

import { SuiClient, getFullnodeUrl } from '@mysten/sui.js/client';
const PACKAGE_ID = '0x53ddebd997f0e57dc899d598f12001930e228dddadf268a41d4c9a7c1df47a97';
const SBT_TYPE = `${PACKAGE_ID}::sbt::SoulBoundToken`;
const client = new SuiClient({
    url: getFullnodeUrl('mainnet'),
});
async function hasSBT(address: string): Promise<boolean> {
    try {
        // Query all objects owned by the address
        const objects = await client.getOwnedObjects({
            owner: address,
            options: { showType: true }, // Include object type in the response
        });
        // Check if any object matches the SBT type
        const hasSbt = objects.data.some((obj) => {
            const type = obj.data?.type;
            return type === SBT_TYPE;
        });
        return hasSbt;
    } catch (error) {
        console.error('Error querying SBT ownership:', error);
        return false;
    }
}

Decrypt

See .

Decryption of Provably Encrypted Data
Lists checked for Proof of Clean Hands