Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/sdk/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
- **Please note**: some functionality related to this is currently either stubbed or routed into the old verifiable
presentation computation functions for now.

- types `PrivateVerificationAuditRecord` and `VerificationAuditRecord` for creating audit records.
- `PrivateVerificationAuditRecord.registerPublicRecord` is a GRPC helper function for registering a creating a public
record and registering it on chain.

## 11.0.0

### Fixed
Expand Down
88 changes: 88 additions & 0 deletions packages/sdk/src/wasm/VerifiablePresentationV1/audit/private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Buffer } from 'buffer/index.js';
import _JB from 'json-bigint';

import { ConcordiumGRPCClient } from '../../../grpc/index.js';
import { sha256 } from '../../../hash.js';
import { AccountSigner, signTransaction } from '../../../signHelpers.js';
import {
AccountTransaction,
AccountTransactionHeader,
AccountTransactionType,
NextAccountNonce,
RegisterDataPayload,
} from '../../../types.js';
import { AccountAddress, DataBlob, TransactionExpiry, TransactionHash } from '../../../types/index.js';
import { VerifiablePresentationRequestV1, VerifiablePresentationV1, VerificationAuditRecord } from '../index.js';

const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true });

class PrivateVerificationAuditRecord {
public readonly type = 'ConcordiumVerificationAuditRecord';
public readonly version = 1;

constructor(
public readonly id: string,
public request: VerifiablePresentationRequestV1.Type,
public presentation: VerifiablePresentationV1.Type
) {}

public toJSON(): JSON {
return { ...this, request: this.request.toJSON(), presentation: this.presentation.toJSON() };
}
}

export type Type = PrivateVerificationAuditRecord;

export type JSON = Pick<Type, 'id' | 'type' | 'version'> & {
request: VerifiablePresentationRequestV1.JSON;
presentation: VerifiablePresentationV1.JSON;
};

export function create(
id: string,
request: VerifiablePresentationRequestV1.Type,
presentation: VerifiablePresentationV1.Type
): PrivateVerificationAuditRecord {
return new PrivateVerificationAuditRecord(id, request, presentation);
}

export function fromJSON(json: JSON): PrivateVerificationAuditRecord {
return new PrivateVerificationAuditRecord(
json.id,
VerifiablePresentationRequestV1.fromJSON(json.request),
VerifiablePresentationV1.fromJSON(json.presentation)
);
}

export function toPublic(record: PrivateVerificationAuditRecord, info?: string): VerificationAuditRecord.Type {
const message = Buffer.from(JSONBig.stringify(record)); // TODO: replace this with proper hashing.. properly from @concordium/rust-bindings
const hash = Uint8Array.from(sha256([message]));
return VerificationAuditRecord.create(hash, info);
}

export async function registerPublicRecord(
privateRecord: PrivateVerificationAuditRecord,
grpc: ConcordiumGRPCClient,
sender: AccountAddress.Type,
signer: AccountSigner,
info?: string
): Promise<{ publicRecord: VerificationAuditRecord.Type; transactionHash: TransactionHash.Type }> {
const nextNonce: NextAccountNonce = await grpc.getNextAccountNonce(sender);
const header: AccountTransactionHeader = {
expiry: TransactionExpiry.futureMinutes(60),
nonce: nextNonce.nonce,
sender,
};

const publicRecord = toPublic(privateRecord, info);
const payload: RegisterDataPayload = { data: new DataBlob(VerificationAuditRecord.createAnchor(publicRecord)) };
const accountTransaction: AccountTransaction = {
header: header,
payload,
type: AccountTransactionType.RegisterData,
};
const signature = await signTransaction(accountTransaction, signer);
const transactionHash = await grpc.sendAccountTransaction(accountTransaction, signature);

return { publicRecord, transactionHash };
}
64 changes: 64 additions & 0 deletions packages/sdk/src/wasm/VerifiablePresentationV1/audit/public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Buffer } from 'buffer/index.js';

import { HexString } from '../../../types.js';
import { cborDecode, cborEncode } from '../../../types/cbor.js';

class VerificationAuditRecord {
constructor(
// TODO: possibly add a specialized type for sha256 hashes
public readonly hash: Uint8Array,
public readonly info?: string
) {}

public toJSON(): JSON {
let json: JSON = { hash: Buffer.from(this.hash).toString('hex') };
if (this.info !== undefined) json.info = this.info;
return json;
}
}

export type Type = VerificationAuditRecord;

export type JSON = {
hash: HexString;
info?: string;
};

export function fromJSON(json: JSON): VerificationAuditRecord {
return new VerificationAuditRecord(Uint8Array.from(Buffer.from(json.hash, 'hex')), json.info);
}

export function create(hash: Uint8Array, info?: string): VerificationAuditRecord {
return new VerificationAuditRecord(hash, info);
}

export type AnchorData = {
type: 'CCDVAA';
version: number;
hash: Uint8Array; // TODO: possibly add a specialized type for sha256 hashes
public?: Record<string, any>;
};

export function createAnchor(value: VerificationAuditRecord, publicInfo?: Record<string, any>): Uint8Array {
const data: AnchorData = {
type: 'CCDVAA',
version: 1,
hash: value.hash,
public: publicInfo,
};
return cborEncode(data);
}

export function decodeAnchor(cbor: Uint8Array): AnchorData {
const value = cborDecode(cbor);
if (typeof value !== 'object' || value === null) throw new Error('Expected a cbor encoded object');
// required fields
if (!('type' in value) || value.type !== 'CCDVAA') throw new Error('Expected "type" to be "CCDVAA"');
if (!('version' in value) || typeof value.version !== 'number')
throw new Error('Expected "version" to be a number');
if (!('hash' in value) || !(value.hash instanceof Uint8Array))
throw new Error('Expected "hash" to be a Uint8Array');
// optional fields
if ('public' in value && typeof value.public !== 'object') throw new Error('Expected "public" to be an object');
return value as AnchorData;
}
9 changes: 8 additions & 1 deletion packages/sdk/src/wasm/VerifiablePresentationV1/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as PrivateVerificationAuditRecord from './audit/private.js';
import * as VerificationAuditRecord from './audit/public.js';
import * as VerifiablePresentationV1 from './proof.js';
import * as VerifiablePresentationRequestV1 from './request.js';

export { VerifiablePresentationRequestV1, VerifiablePresentationV1 };
export {
VerifiablePresentationRequestV1,
VerifiablePresentationV1,
PrivateVerificationAuditRecord,
VerificationAuditRecord,
};

export * from './types.js';
12 changes: 1 addition & 11 deletions packages/sdk/test/ci/wasm/VerifiablePresentationV1.test.ts

Large diffs are not rendered by default.

117 changes: 117 additions & 0 deletions packages/sdk/test/ci/wasm/VerificationAuditRecord.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import _JB from 'json-bigint';

import {
PrivateVerificationAuditRecord,
VerifiablePresentationRequestV1,
VerifiablePresentationV1,
VerificationAuditRecord,
} from '../../../src/index.ts';

const JSONBig = _JB({ alwaysParseAsBig: true, useNativeBigInt: true });

const PRESENTATION_REQUEST = VerifiablePresentationRequestV1.fromJSON({
requestContext: {
type: 'ConcordiumContextInformationV1',
given: [
{ label: 'Nonce', context: '00010203' },
{ label: 'ConnectionID', context: '0102010201020102010201020102010201020102010201020102010201020102' },
{ label: 'ContextString', context: 'Wine payment' },
],
requested: ['BlockHash', 'ResourceID'],
},
credentialStatements: [
{
idQualifier: {
type: 'sci',
issuers: [
{ index: 2101n, subindex: 0n },
{ index: 1337n, subindex: 42n },
] as any,
},
statement: [
{ type: 'AttributeInRange', attributeTag: 'b', lower: 80n, upper: 1237n } as any,
{ type: 'AttributeInSet', attributeTag: 'c', set: ['aa', 'ff', 'zz'] },
],
},
{
idQualifier: { type: 'id', issuers: [0, 1, 2] },
statement: [{ type: 'RevealAttribute', attributeTag: 'firstName' }],
},
],
transactionRef: '0102030401020304010203040102030401020304010203040102030401020304',
});

const PRESENTATION = VerifiablePresentationV1.fromJSON({
presentationContext: {
type: 'ConcordiumContextInformationV1',
given: [
{ label: 'Nonce', context: '00010203' },
{ label: 'ConnectionID', context: '0102010201020102010201020102010201020102010201020102010201020102' },
{ label: 'ContextString', context: 'Wine payment' },
],
requested: [
{ label: 'BlockHash', context: '0101010101010101010101010101010101010101010101010101010101010101' },
{ label: 'ResourceID', context: 'https://compliant.shop' },
],
},
verifiableCredential: [
{
type: ['VerifiableCredential', 'ConcordiumVerifiableCredentialV1', 'ConcordiumIDBasedCredential'],
proof: {
type: 'ConcordiumZKProofV4',
createdAt: '2025-10-17T13:14:14.292Z',
proofValue:
'01020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102010201020102',
},
issuer: 'ccd:testnet:idp:0',
credentialSubject: {
statement: [
{ attributeTag: 'dob', lower: '81', type: 'AttributeInRange', upper: '1231' },
{ attributeTag: 'firstName', type: 'RevealAttribute' },
],
id: '123456123456123456123456123456123456123456123456',
},
},
],
proof: { created: '2025-10-17T13:14:14.290Z', proofValue: [], type: 'ConcordiumWeakLinkingProofV1' },
});

const PRIVATE_RECORD = PrivateVerificationAuditRecord.create('VERY unique ID', PRESENTATION_REQUEST, PRESENTATION);
const PUBLIC_RECORD = PrivateVerificationAuditRecord.toPublic(PRIVATE_RECORD, 'some public info?');

describe('PrivateVerificationAuditRecord', () => {
it('completes JSON roundtrip', () => {
const json = JSONBig.stringify(PRIVATE_RECORD);
const roundtrip = PrivateVerificationAuditRecord.fromJSON(JSONBig.parse(json));
expect(roundtrip).toEqual(PRIVATE_RECORD);
});

it('creates expected public record', () => {
const publicAuditRecord = PrivateVerificationAuditRecord.toPublic(PRIVATE_RECORD, 'some public info?');
const expected: VerificationAuditRecord.Type = VerificationAuditRecord.fromJSON({
hash: 'fcce3a7222e09bc86f0b4e0186501ff360c5a0abce88b8d1df2aaf7aa3ef8d78',
info: 'some public info?',
});
expect(publicAuditRecord).toEqual(expected);
});
});

describe('VerificationAuditRecord', () => {
it('completes JSON roundtrip', () => {
const json = JSONBig.stringify(PUBLIC_RECORD);
const roundtrip = VerificationAuditRecord.fromJSON(JSONBig.parse(json));
expect(roundtrip).toEqual(PUBLIC_RECORD);
});

it('computes the anchor as expected', () => {
const anchor = VerificationAuditRecord.createAnchor(PUBLIC_RECORD, { pub: 'anchor info' });
const decoded = VerificationAuditRecord.decodeAnchor(anchor);
const expected: VerificationAuditRecord.AnchorData = {
type: 'CCDVAA',
version: 1,
hash: PUBLIC_RECORD.hash,
public: { pub: 'anchor info' },
};
expect(decoded).toEqual(expected);
});
});
10 changes: 10 additions & 0 deletions packages/sdk/test/ci/wasm/constants.ts

Large diffs are not rendered by default.