Human ID
  • 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 1 month 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