mina-attestations
Version:
Private Attestations on Mina
232 lines (231 loc) • 8.95 kB
JavaScript
export { PrettyPrinter };
/**
* Methods to print Mina Attestation data types
* in human readable format.
*/
const PrettyPrinter = {
printPresentationRequest,
printVerifierIdentity,
simplifyCredentialData,
};
function printPresentationRequest(request) {
let formatted = [
`Type: ${request.type}`,
'',
formatInputsHumanReadable(request.spec.inputs),
'',
`Requirements:\n${formatLogicNode(request.spec.assert, 0)}`,
'',
`Output:\n${formatLogicNode(request.spec.outputClaim, 0)}`,
formatClaimsHumanReadable(request.claims),
request.inputContext
? `\nContext:\n- Type: ${request.inputContext.type}\n- Action: ${request.inputContext.action}\n- Server Nonce: ${request.inputContext.serverNonce.value}`
: 'WARNING: This request is not bound to any context',
].join('\n');
return formatted;
}
function printVerifierIdentity(request, origin) {
if (request.type === 'no-context') {
return '\nWARNING: No verifier identity provided\n';
}
if (request.type === 'https') {
return `\nVerifier Identity: ${origin}\n`;
}
// for zkapp requests, verifier identity is contained in the presentation request
if (request.inputContext?.type !== 'zk-app') {
return '\nWARNING: Invalid request!\n';
}
let { verifierIdentity } = request.inputContext;
let verifierUrl = `minascan.io/${verifierIdentity.network}/account/${verifierIdentity.publicKey}`;
return `\nVerifier Identity: ${JSON.stringify(verifierIdentity, null, 2)}
See verifying zkApp on Minascan: https://${verifierUrl}\n`;
}
function simplifyCredentialData(storedCredential) {
const data = getCredentialData(storedCredential.credential);
let simplified = {};
for (let [key, value] of Object.entries(data)) {
if (typeof value === 'object' && value !== null) {
if ('bytes' in value) {
simplified[key] = value.bytes
.map((b) => b.value)
.join('');
}
else if ('value' in value) {
simplified[key] = value.value;
}
else {
simplified[key] = value;
}
}
else {
simplified[key] = value;
}
}
return simplified;
}
function getCredentialData(credential) {
if ('value' in credential) {
// TODO get rid of type coercions
return credential.value.data;
}
return credential.data;
}
function extractCredentialFields(data) {
if (!data)
return [];
if (data._type === 'Struct' && data.properties) {
return Object.keys(data.properties);
}
if (data._type === 'DynamicRecord' && data.knownShape) {
return Object.keys(data.knownShape);
}
return Object.keys(data);
}
function buildPropertyPath(node) {
let parts = [];
let currentNode = node;
while (currentNode?.type === 'property') {
parts.unshift(currentNode.key);
currentNode = currentNode.inner;
}
return parts.join('.');
}
function formatLogicNode(node, level = 0) {
let indent = ' '.repeat(level);
switch (node.type) {
case 'and':
if (node.inputs.length === 0) {
return 'true';
}
return `${indent}All of these conditions must be true:\n${node.inputs
.map((n) => `${indent}- ${formatLogicNode(n, level + 1)}`)
.join('\n')}`;
case 'or':
return `${indent}Either:\n${indent}- ${formatLogicNode(node.left, level + 1)}\n${indent}Or:\n${indent}- ${formatLogicNode(node.right, level + 1)}`;
case 'equals':
return `${formatLogicNode(node.left)} = ${formatLogicNode(node.right)}`;
case 'equalsOneOf': {
let input = formatLogicNode(node.input, level);
let options = Array.isArray(node.options)
? node.options.map((o) => formatLogicNode(o, level)).join(', ')
: formatLogicNode(node.options, level);
return `${options} contains ${input}`;
}
case 'lessThan':
return `${formatLogicNode(node.left)} < ${formatLogicNode(node.right)}`;
case 'lessThanEq':
return `${formatLogicNode(node.left)} ≤ ${formatLogicNode(node.right)}`;
case 'property': {
// If this is the root property, just return the path
if (node.inner?.type === 'root') {
return node.key;
}
// For nested properties, build the complete path
return buildPropertyPath(node);
}
case 'root':
return '';
case 'hash':
return `hash(${node.inputs
.map((n) => formatLogicNode(n, level))
.join(', ')})`;
case 'issuer':
return `issuer(${node.credentialKey})`;
case 'not':
if (node.inner.type === 'equals') {
return `${formatLogicNode(node.inner.left)} ≠ ${formatLogicNode(node.inner.right)}`;
}
return `not(${formatLogicNode(node.inner, level)})`;
case 'add':
return `(${formatLogicNode(node.left)} + ${formatLogicNode(node.right)})`;
case 'sub':
return `(${formatLogicNode(node.left)} - ${formatLogicNode(node.right)})`;
case 'mul':
return `(${formatLogicNode(node.left)} x ${formatLogicNode(node.right)})`;
case 'div':
return `(${formatLogicNode(node.left)} ÷ ${formatLogicNode(node.right)})`;
case 'record': {
if (Object.keys(node.data).length === 0) {
return '{}';
}
return Object.entries(node.data)
.map(([key, value]) => `${key}: ${formatLogicNode(value, level)}`)
.join(`\n${indent}`);
}
case 'constant': {
if (node.data._type === 'Undefined') {
return 'undefined';
}
return node.data.value?.toString() ?? 'null';
}
case 'ifThenElse':
return `${indent}If this condition is true:\n${indent}- ${formatLogicNode(node.condition, level + 1)}\n${indent}Then:\n${indent}- ${formatLogicNode(node.thenNode, level + 1)}\n${indent}Otherwise:\n${indent}- ${formatLogicNode(node.elseNode, level + 1)}`;
case 'credential': {
return node.credentialKey;
}
case 'owner': {
return 'OWNER';
}
case 'issuerPublicKey': {
return `issuerPublicKey(${node.credentialKey})`;
}
case 'publicInput': {
return `publicInput(${node.credentialKey})`;
}
case 'verificationKeyHash': {
return `verificationKeyHash(${node.credentialKey})`;
}
default:
throw Error(`Unknown node type: ${node.type}`);
}
}
// TODO here we assume that it makes sense to simple converting general serialized provable values to strings
// but they can be objects etc
function formatInputsHumanReadable(inputs) {
let sections = [];
// Handle credentials
let credentials = Object.entries(inputs).filter((input) => input[1].type === 'credential');
if (credentials.length > 0) {
sections.push('Required credentials:');
for (let [key, input] of credentials) {
let fields = extractCredentialFields(input.data);
let wrappedFields = fields.reduce((acc, field, i) => {
if (i === fields.length - 1)
return acc + field;
return `${acc + field}, `;
}, '');
sections.push(`- ${key} (type: ${input.credentialType}):\n Contains: ${wrappedFields}`);
}
}
// Handle claims
let claims = Object.entries(inputs).filter(([_, input]) => input.type === 'claim');
if (claims.length > 0) {
sections.push('\nClaims:');
for (let [key, input] of claims) {
sections.push(`- ${key}: ${input.data._type}`);
}
}
// Handle constants
let constants = Object.entries(inputs).filter((input) => input[1].type === 'constant');
if (constants.length > 0) {
sections.push('\nConstants:');
for (let [key, input] of constants) {
sections.push(`- ${key}: ${input.data._type} = ${input.value}`);
}
}
return sections.join('\n');
}
function formatClaimsHumanReadable(claims) {
let sections = ['\nClaimed values:'];
for (let [key, claim] of Object.entries(claims)) {
if (claim._type === 'DynamicArray' && claim.value) {
let values = claim.value.map((v) => v.value).join(', ');
sections.push(`- ${key}:\n ${values}`);
}
else {
sections.push(`- ${key}: ${claim.value}`);
}
}
return sections.join('\n');
}
//# sourceMappingURL=pretty-printer.js.map