Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
8 changes: 5 additions & 3 deletions packages/js-evo-sdk/src/documents/facade.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { asJsonString } from '../util.js';
import { asJsonString, generateEntropy } from '../util.js';
import type { EvoSDK } from '../sdk.js';

export class DocumentsFacade {
Expand Down Expand Up @@ -72,10 +72,12 @@ export class DocumentsFacade {
type: string;
ownerId: string;
data: unknown;
entropyHex: string;
entropyHex?: string; // Now optional - will auto-generate if not provided
privateKeyWif: string;
}): Promise<any> {
const { contractId, type, ownerId, data, entropyHex, privateKeyWif } = args;
const { contractId, type, ownerId, data, privateKeyWif } = args;
// Auto-generate entropy if not provided
const entropyHex = args.entropyHex ?? generateEntropy();
const w = await this.sdk.getWasmSdkConnected();
return w.documentCreate(
contractId,
Expand Down
31 changes: 31 additions & 0 deletions packages/js-evo-sdk/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,34 @@ export function asJsonString(value: unknown): string | undefined {
if (typeof value === 'string') return value;
return JSON.stringify(value);
}

/**
* Generate 32 bytes of cryptographically secure random entropy as a hex string.
* Works in both Node.js and browser environments.
*
* @returns A 64-character hex string representing 32 bytes of entropy
* @throws Error if no secure random source is available
*/
export function generateEntropy(): string {
// Node.js environment
if (typeof globalThis !== 'undefined' && globalThis.crypto && 'randomBytes' in globalThis.crypto) {
// @ts-ignore - Node.js crypto.randomBytes exists but may not be in types
return globalThis.crypto.randomBytes(32).toString('hex');
}

// Browser environment or Node.js with Web Crypto API
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
const buffer = new Uint8Array(32);
globalThis.crypto.getRandomValues(buffer);
return Array.from(buffer).map((b) => b.toString(16).padStart(2, '0')).join('');
}

// Fallback for older environments
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
const buffer = new Uint8Array(32);
window.crypto.getRandomValues(buffer);
return Array.from(buffer).map((b) => b.toString(16).padStart(2, '0')).join('');
}

throw new Error('No secure random source available. This environment does not support crypto.randomBytes or crypto.getRandomValues.');
}
32 changes: 31 additions & 1 deletion packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ describe('DocumentsFacade', () => {
expect(wasmSdk.getDocumentWithProofInfo).to.be.calledOnceWithExactly('c', 't', 'id');
});

it('create() calls wasmSdk.documentCreate with JSON data', async () => {
it('create() calls wasmSdk.documentCreate with JSON data and provided entropy', async () => {
const data = { foo: 'bar' };
await client.documents.create({
contractId: 'c',
Expand All @@ -66,6 +66,36 @@ describe('DocumentsFacade', () => {
expect(wasmSdk.documentCreate).to.be.calledOnceWithExactly('c', 't', 'o', JSON.stringify(data), 'ee', 'wif');
});

it('create() auto-generates entropy when not provided', async () => {
const data = { foo: 'bar' };
await client.documents.create({
contractId: 'c',
type: 't',
ownerId: 'o',
data,
// No entropyHex provided - should auto-generate
privateKeyWif: 'wif',
});

// Check that documentCreate was called
expect(wasmSdk.documentCreate).to.be.calledOnce();
const [
contractId, type, ownerId, jsonData, entropy, wif,
] = wasmSdk.documentCreate.firstCall.args;

// Verify all params except entropy
expect(contractId).to.equal('c');
expect(type).to.equal('t');
expect(ownerId).to.equal('o');
expect(jsonData).to.equal(JSON.stringify(data));
expect(wif).to.equal('wif');

// Verify that entropy was auto-generated (should be 64 hex chars = 32 bytes)
expect(entropy).to.be.a('string');
expect(entropy).to.match(/^[0-9a-f]{64}$/i);
expect(entropy.length).to.equal(64);
});

it('replace() calls wasmSdk.documentReplace with BigInt revision', async () => {
await client.documents.replace({
contractId: 'c',
Expand Down
77 changes: 77 additions & 0 deletions packages/js-evo-sdk/tests/unit/util.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { asJsonString, generateEntropy } from '../../dist/util.js';

describe('Util Functions', () => {
describe('asJsonString', () => {
it('returns undefined for null', () => {
expect(asJsonString(null)).to.be.undefined();
});

it('returns undefined for undefined', () => {
expect(asJsonString(undefined)).to.be.undefined();
});

it('returns string as-is', () => {
expect(asJsonString('hello')).to.equal('hello');
});

it('converts objects to JSON string', () => {
const obj = { foo: 'bar', num: 42 };
expect(asJsonString(obj)).to.equal(JSON.stringify(obj));
});

it('converts arrays to JSON string', () => {
const arr = [1, 2, 'three'];
expect(asJsonString(arr)).to.equal(JSON.stringify(arr));
});
});

describe('generateEntropy', () => {
it('generates a 64-character hex string', () => {
const entropy = generateEntropy();
expect(entropy).to.be.a('string');
expect(entropy.length).to.equal(64);
});

it('generates valid hexadecimal', () => {
const entropy = generateEntropy();
expect(entropy).to.match(/^[0-9a-f]{64}$/i);
});

it('generates different values each time', () => {
const entropy1 = generateEntropy();
const entropy2 = generateEntropy();
const entropy3 = generateEntropy();

// Should be different (extremely unlikely to be the same)
expect(entropy1).to.not.equal(entropy2);
expect(entropy2).to.not.equal(entropy3);
expect(entropy1).to.not.equal(entropy3);
});

it('returns exactly 32 bytes when decoded', () => {
const entropy = generateEntropy();
// Convert hex string to bytes
const bytes = [];
for (let i = 0; i < entropy.length; i += 2) {
bytes.push(parseInt(entropy.substring(i, 2), 16));
}
expect(bytes.length).to.equal(32);
});

it('generates values with good distribution', () => {
// Generate multiple samples and check that we get a variety of hex digits
const samples = [];
for (let i = 0; i < 10; i += 1) {
samples.push(generateEntropy());
}

// Check that we see various hex digits (not all zeros or all ones)
const allChars = samples.join('');
const uniqueChars = new Set(allChars).size;

// We should see most of the 16 possible hex digits (0-9, a-f)
// With 640 characters (10 * 64), we expect to see all 16
expect(uniqueChars).to.be.at.least(10);
});
});
});
Loading