Passport Issuer
  • INTRODUCTION
    • Intro to Quadrata Passport
    • Intro to Passport Issuers
  • INTEGRATION (DEVELOPERS)
    • Issuer Permission
    • Attesting attributes
      • 1. API Request
      • 2. API Response
      • 3. Minting Passport
    • Querying Attributes
    • Burning Passports
    • On-going Monitoring
  • Additional Information
    • Passport Attributes
    • Smart Contracts
    • Issuer Toolkit Library
Powered by GitBook
On this page
  1. INTEGRATION (DEVELOPERS)

On-going Monitoring

PreviousBurning PassportsNextPassport Attributes

Last updated 2 years ago

For a subset of attributes (ex: AML), the issuer is responsible to perform and update the attribute values directly in the passport.

Only specific issuers are required to perform on-going monitoring.

Example (change of AML score)

import { ethers } from 'ethers';
import { BytesLike } from '@ethersproject/bytes';

const newAMLScore = hexZeroPad("0x05", 32); // new AML score
const accountToUpdate = "0x......."; // Wallet of the Passport holder to update
const issuer = new Wallet(ISSUER_PRIVATE_KEY); // Loading issuer account from their private key
const attrTypes = [utils.id("AML")];  // List of attributes types
const attrKeys = computeAttrKeys(accountAddress, attrTypes); // List of attributes Identifiers
const attrValues = [newAMLScore]; // List of attributes values
const verifiedAt = Math.floor(new Date().getTime() / 1000) - 60; // we substract 60 seconds to avoid errors during minting passport with long block time
const issuedAt = Math.floor(new Date().getTime() / 1000) - 60; // we substract 60 seconds to avoid errors during minting passport with long block time
const passportAddress = "0x2e779749c40CC4Ba1cAB4c57eF84d90755CC017d";
const chainId = 1; // 1 for Ethereum Mainnet
const fee = utils.parseEther("0"); // No fee to paid as this is an update performed by the issuer themselves
const did = ethers.constants.HashZero; // Leave empty
const signature = await signAttributes(
  accountAddress,
  issuer,
  attrKeys,
  attrValues,
  verifiedAt,
  issuedAt, 
  fee,
  passportAddress,
  chainId
)

const QUAD_PASSPORT_ABI = ''; // Can be retrieve at https://etherscan.io/address/0xebf9b4db6050517e402c578237326b793bf348ff#code

const passportContract = new ethers.Contract(
    passportAddress,
    QUAD_PASSPORT_ABI,
);

const tx = await passportContract.setAttributesIssuer(
    accountAddress,
    [
      attrKeys,
      attrValues,
      attrTypes,
      ethers.constants.HashZero, // Leave bytes32(0)
      0, // Leave 0
      verifiedAt,
      issuedAt,
      fee,
    ],
    sigIssuer,
    {
      value: fee,
    }
  )
)

await tx.wait();

export const signAttributes = async (
  accountAddress: string,
  issuer: typeof Signer,
  attrKeys: BytesLike[],
  attrValues: BytesLike[],
  verifiedAt: number,
  issuedAt: number,
  fee: any,
  passportAddress: string,
  chainId: number,
  did: BytesLike = utils.constants.HashZero,
): Promise<typeof DataHexString> => {
  const attrKeys: string[] = [];
  const attrValues: string[] = [];

  const hash = utils.keccak256(
    utils.defaultAbiCoder.encode(
      [
        "address",
        "bytes32[]",
        "bytes32[]",
        "bytes32",
        "uint256",
        "uint256",
        "uint256",
        "uint256",
        "address",
      ],
      [
        accountAddress,
        attrKeys,
        attrValues,
        did,
        verifiedAt,
        issuedAt,
        fee,
        chainId,
        passportAddress,
      ]
    )
  );

  const sig = await issuer.signMessage(utils.arrayify(hash));

  return sig;
};
`
W3_TIMEOUT = 60
ABI_PATH = "" # DIRECTORY CONTAINING ABI FILE
PASSPORT_ABI_FILE_NAME = "" # ABI FILE NAME
NETWORK_URI = "" # INFURA_RPC_NODE
PASSPORT_CONTRACT_ADDRESS = "" # PASSPORT ADDRESS
CHAIN_ID = 1 # CHAIN ID WHERE ATTESTATION WILL BE POSTED
GAS_LIMIT = 300000
ISSUER_PRIVATE_KEY = "" # ISSUER__PRIVATE_KEY
HEX_PADDING = 66

@dataclass(frozen=True)
class UpdateCreditScoreInputs:
    target_address: str
    token_id: int
    new_value: str
    verified_at: int
    quad_did: str
    issued_at: int = blockchain_timestamp_seconds()
    # Update fee should be 0 so issuer does not have to pay for update
    update_fee: int = 0

    def attr_keys(self) -> List[bytes]:
        # Need to modify CREDIT_SCORE to attribute type 
        return [Web3.solidityKeccak(['bytes32', 'bytes32'], [self.target_address, keccak('CREDIT_SCORE')])]

    def attr_values(self) -> List[bytes]:
        return [self.hexlified_value()]

    def attr_types(self) -> List[str]:
        return [keccak('CREDIT_SCORE')]

    def hexlified_value(self) -> bytes:
        return bytes.fromhex(f'{self.new_value:#0{HEX_PADDING}x}'[2:])

    def attribute_setter_config(self) -> tuple:
        return (
            [key.hex() for key in self.attr_keys()],
            [f'0x{val.hex()}' for val in self.attr_values()],
            self.attr_types(),
            self.quad_did,
            self.token_id,
            self.verified_at,
            self.issued_at,
            self.update_fee,
        )

    def update_message(self, chain_id) -> str:
        abiEncoded = eth_abi.encode_abi(
            [
                'bytes32',
                'bytes32[]',
                'bytes32[]',
                'bytes32',
                'uint256',
                'uint256',
                'uint256',
                'uint256',
                'bytes32',
            ],
            [
                self.target_address,
                self.attr_keys(),
                self.attr_values(),
                bytes.fromhex(self.quad_did[2:]),
                self.verified_at,
                self.issued_at,
                self.update_fee,
                chain_id,
                bytes.fromhex(Web3.toChecksumAddress(PASSPORT_CONTRACT_ADDRESS)[2:]).rjust(
                    32, b'\0'
                ),
            ],
        )
        return Web3.solidityKeccak(['bytes32'], [('0x' + abiEncoded.hex())]).hex()

def keccak(value: str) -> str:
    if not value:
        raise ValueError('Attempted to keccak hash an empty string')
    return Web3.solidityKeccak(['bytes32'], [value.encode()]).hex()


def _load_passport_abi(self) -> str:
    with open(os.path.join(ABI_PATH, PASSPORT_ABI_FILE_NAME)) as f:
        return json.load(f)

update_inputs = UpdateCreditScoreInputs(
    target_address = '', # target wallet address for credit score update
    token_id = 1, # configurable passport skin
    new_value = '',# new value
    verified_at = int(datetime.now(timezone.utc).timestamp()) - 60,
    quad_did = web3.constants.HASH_ZERO,
    issued_at = int(datetime.now(timezone.utc).timestamp()) - 60,
    update_fee = 0 # Update fee should be 0 so issuer does not have to pay for update
)

w3 = Web3(Web3.HTTPProvider(NETWORK_URI, request_kwargs={'timeout': W3_TIMEOUT}))
contract = w3.eth.contract(
    address=Web3.toChecksumAddress(PASSPORT_CONTRACT_ADDRESS),
    abi=self._load_passport_abi(),
)

# Assumes no pending transaction
nonce = w3.eth.getTransactionCount(
    Web3.toChecksumAddress(
        w3.eth.account.privateKeyToAccount(ISSUER_PRIVATE_KEY).address))


transaction = contract.functions.setAttributesIssuer(
    Web3.toChecksumAddress(update_inputs.target_address),
    update_inputs.attribute_setter_config(),
    update_inputs.update_message(CHAIN_ID),
).buildTransaction(
    {
        'gasPrice': w3.eth.gas_price,
        'gas': GAS_LIMIT, 
        'nonce': nonce,
        'chainId': CHAIN_ID,
    }
)
decoder = codecs.getdecoder('hex_codec')
signed_tx = self.w3.eth.account.signTransaction(
    transaction, private_key=decoder(str.encode(ISSUER_PRIVATE_KEY))[0]
)
tx_hash = self.w3.eth.sendRawTransaction(signed_tx.rawTransaction)
on-going monitoring