Skip to content
Open
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
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