From efcca2e1bc177b890c502ad69c66cba4083b3b82 Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 29 Oct 2025 15:27:45 -0400 Subject: [PATCH 1/2] feat(sdk): add automatic entropy generation for document creation - Make entropyHex parameter optional in documents.create() - Add generateEntropy() utility function that works in Node.js and browsers - Auto-generate entropy when not provided - Add comprehensive tests for entropy generation and auto-generation behavior --- packages/js-evo-sdk/src/documents/facade.ts | 8 +- packages/js-evo-sdk/src/util.ts | 31 ++++++++ .../tests/unit/facades/documents.spec.mjs | 30 +++++++- packages/js-evo-sdk/tests/unit/util.spec.mjs | 77 +++++++++++++++++++ 4 files changed, 142 insertions(+), 4 deletions(-) create mode 100644 packages/js-evo-sdk/tests/unit/util.spec.mjs diff --git a/packages/js-evo-sdk/src/documents/facade.ts b/packages/js-evo-sdk/src/documents/facade.ts index b5061e4c5b4..db0a5552632 100644 --- a/packages/js-evo-sdk/src/documents/facade.ts +++ b/packages/js-evo-sdk/src/documents/facade.ts @@ -1,4 +1,4 @@ -import { asJsonString } from '../util.js'; +import { asJsonString, generateEntropy } from '../util.js'; import type { EvoSDK } from '../sdk.js'; export class DocumentsFacade { @@ -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 { - 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, diff --git a/packages/js-evo-sdk/src/util.ts b/packages/js-evo-sdk/src/util.ts index 36d5f27d2fb..bfec38161c9 100644 --- a/packages/js-evo-sdk/src/util.ts +++ b/packages/js-evo-sdk/src/util.ts @@ -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.'); +} diff --git a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs index 1943f2d00ca..ee9d4bcbac7 100644 --- a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs @@ -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', @@ -66,6 +66,34 @@ 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', diff --git a/packages/js-evo-sdk/tests/unit/util.spec.mjs b/packages/js-evo-sdk/tests/unit/util.spec.mjs new file mode 100644 index 00000000000..c98665886bf --- /dev/null +++ b/packages/js-evo-sdk/tests/unit/util.spec.mjs @@ -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.substr(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++) { + 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); + }); + }); +}); \ No newline at end of file From cbb42ea4f15f68f4ccf8fbc514b31e5dfcfa719b Mon Sep 17 00:00:00 2001 From: thephez Date: Wed, 29 Oct 2025 16:22:19 -0400 Subject: [PATCH 2/2] chore: lint fixes --- packages/js-evo-sdk/src/util.ts | 4 ++-- .../js-evo-sdk/tests/unit/facades/documents.spec.mjs | 4 +++- packages/js-evo-sdk/tests/unit/util.spec.mjs | 10 +++++----- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/js-evo-sdk/src/util.ts b/packages/js-evo-sdk/src/util.ts index bfec38161c9..9bd7552afc8 100644 --- a/packages/js-evo-sdk/src/util.ts +++ b/packages/js-evo-sdk/src/util.ts @@ -22,14 +22,14 @@ export function generateEntropy(): string { 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(''); + 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(''); + 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.'); diff --git a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs index ee9d4bcbac7..236fff41a55 100644 --- a/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs @@ -79,7 +79,9 @@ describe('DocumentsFacade', () => { // Check that documentCreate was called expect(wasmSdk.documentCreate).to.be.calledOnce(); - const [contractId, type, ownerId, jsonData, entropy, wif] = wasmSdk.documentCreate.firstCall.args; + const [ + contractId, type, ownerId, jsonData, entropy, wif, + ] = wasmSdk.documentCreate.firstCall.args; // Verify all params except entropy expect(contractId).to.equal('c'); diff --git a/packages/js-evo-sdk/tests/unit/util.spec.mjs b/packages/js-evo-sdk/tests/unit/util.spec.mjs index c98665886bf..a366cee7701 100644 --- a/packages/js-evo-sdk/tests/unit/util.spec.mjs +++ b/packages/js-evo-sdk/tests/unit/util.spec.mjs @@ -3,11 +3,11 @@ import { asJsonString, generateEntropy } from '../../dist/util.js'; describe('Util Functions', () => { describe('asJsonString', () => { it('returns undefined for null', () => { - expect(asJsonString(null)).to.be.undefined; + expect(asJsonString(null)).to.be.undefined(); }); it('returns undefined for undefined', () => { - expect(asJsonString(undefined)).to.be.undefined; + expect(asJsonString(undefined)).to.be.undefined(); }); it('returns string as-is', () => { @@ -53,7 +53,7 @@ describe('Util Functions', () => { // Convert hex string to bytes const bytes = []; for (let i = 0; i < entropy.length; i += 2) { - bytes.push(parseInt(entropy.substr(i, 2), 16)); + bytes.push(parseInt(entropy.substring(i, 2), 16)); } expect(bytes.length).to.equal(32); }); @@ -61,7 +61,7 @@ describe('Util Functions', () => { 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++) { + for (let i = 0; i < 10; i += 1) { samples.push(generateEntropy()); } @@ -74,4 +74,4 @@ describe('Util Functions', () => { expect(uniqueChars).to.be.at.least(10); }); }); -}); \ No newline at end of file +});