Skip to content

Commit d852462

Browse files
committed
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
1 parent f49390f commit d852462

File tree

4 files changed

+142
-4
lines changed

4 files changed

+142
-4
lines changed

packages/js-evo-sdk/src/documents/facade.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { asJsonString } from '../util.js';
1+
import { asJsonString, generateEntropy } from '../util.js';
22
import type { EvoSDK } from '../sdk.js';
33

44
export class DocumentsFacade {
@@ -72,10 +72,12 @@ export class DocumentsFacade {
7272
type: string;
7373
ownerId: string;
7474
data: unknown;
75-
entropyHex: string;
75+
entropyHex?: string; // Now optional - will auto-generate if not provided
7676
privateKeyWif: string;
7777
}): Promise<any> {
78-
const { contractId, type, ownerId, data, entropyHex, privateKeyWif } = args;
78+
const { contractId, type, ownerId, data, privateKeyWif } = args;
79+
// Auto-generate entropy if not provided
80+
const entropyHex = args.entropyHex ?? generateEntropy();
7981
const w = await this.sdk.getWasmSdkConnected();
8082
return w.documentCreate(
8183
contractId,

packages/js-evo-sdk/src/util.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,34 @@ export function asJsonString(value: unknown): string | undefined {
33
if (typeof value === 'string') return value;
44
return JSON.stringify(value);
55
}
6+
7+
/**
8+
* Generate 32 bytes of cryptographically secure random entropy as a hex string.
9+
* Works in both Node.js and browser environments.
10+
*
11+
* @returns A 64-character hex string representing 32 bytes of entropy
12+
* @throws Error if no secure random source is available
13+
*/
14+
export function generateEntropy(): string {
15+
// Node.js environment
16+
if (typeof globalThis !== 'undefined' && globalThis.crypto && 'randomBytes' in globalThis.crypto) {
17+
// @ts-ignore - Node.js crypto.randomBytes exists but may not be in types
18+
return globalThis.crypto.randomBytes(32).toString('hex');
19+
}
20+
21+
// Browser environment or Node.js with Web Crypto API
22+
if (typeof globalThis !== 'undefined' && globalThis.crypto && globalThis.crypto.getRandomValues) {
23+
const buffer = new Uint8Array(32);
24+
globalThis.crypto.getRandomValues(buffer);
25+
return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join('');
26+
}
27+
28+
// Fallback for older environments
29+
if (typeof window !== 'undefined' && window.crypto && window.crypto.getRandomValues) {
30+
const buffer = new Uint8Array(32);
31+
window.crypto.getRandomValues(buffer);
32+
return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join('');
33+
}
34+
35+
throw new Error('No secure random source available. This environment does not support crypto.randomBytes or crypto.getRandomValues.');
36+
}

packages/js-evo-sdk/tests/unit/facades/documents.spec.mjs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('DocumentsFacade', () => {
5353
expect(wasmSdk.getDocumentWithProofInfo).to.be.calledOnceWithExactly('c', 't', 'id');
5454
});
5555

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

69+
it('create() auto-generates entropy when not provided', async () => {
70+
const data = { foo: 'bar' };
71+
await client.documents.create({
72+
contractId: 'c',
73+
type: 't',
74+
ownerId: 'o',
75+
data,
76+
// No entropyHex provided - should auto-generate
77+
privateKeyWif: 'wif',
78+
});
79+
80+
// Check that documentCreate was called
81+
expect(wasmSdk.documentCreate).to.be.calledOnce();
82+
const [contractId, type, ownerId, jsonData, entropy, wif] = wasmSdk.documentCreate.firstCall.args;
83+
84+
// Verify all params except entropy
85+
expect(contractId).to.equal('c');
86+
expect(type).to.equal('t');
87+
expect(ownerId).to.equal('o');
88+
expect(jsonData).to.equal(JSON.stringify(data));
89+
expect(wif).to.equal('wif');
90+
91+
// Verify that entropy was auto-generated (should be 64 hex chars = 32 bytes)
92+
expect(entropy).to.be.a('string');
93+
expect(entropy).to.match(/^[0-9a-f]{64}$/i);
94+
expect(entropy.length).to.equal(64);
95+
});
96+
6997
it('replace() calls wasmSdk.documentReplace with BigInt revision', async () => {
7098
await client.documents.replace({
7199
contractId: 'c',
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { asJsonString, generateEntropy } from '../../dist/util.js';
2+
3+
describe('Util Functions', () => {
4+
describe('asJsonString', () => {
5+
it('returns undefined for null', () => {
6+
expect(asJsonString(null)).to.be.undefined;
7+
});
8+
9+
it('returns undefined for undefined', () => {
10+
expect(asJsonString(undefined)).to.be.undefined;
11+
});
12+
13+
it('returns string as-is', () => {
14+
expect(asJsonString('hello')).to.equal('hello');
15+
});
16+
17+
it('converts objects to JSON string', () => {
18+
const obj = { foo: 'bar', num: 42 };
19+
expect(asJsonString(obj)).to.equal(JSON.stringify(obj));
20+
});
21+
22+
it('converts arrays to JSON string', () => {
23+
const arr = [1, 2, 'three'];
24+
expect(asJsonString(arr)).to.equal(JSON.stringify(arr));
25+
});
26+
});
27+
28+
describe('generateEntropy', () => {
29+
it('generates a 64-character hex string', () => {
30+
const entropy = generateEntropy();
31+
expect(entropy).to.be.a('string');
32+
expect(entropy.length).to.equal(64);
33+
});
34+
35+
it('generates valid hexadecimal', () => {
36+
const entropy = generateEntropy();
37+
expect(entropy).to.match(/^[0-9a-f]{64}$/i);
38+
});
39+
40+
it('generates different values each time', () => {
41+
const entropy1 = generateEntropy();
42+
const entropy2 = generateEntropy();
43+
const entropy3 = generateEntropy();
44+
45+
// Should be different (extremely unlikely to be the same)
46+
expect(entropy1).to.not.equal(entropy2);
47+
expect(entropy2).to.not.equal(entropy3);
48+
expect(entropy1).to.not.equal(entropy3);
49+
});
50+
51+
it('returns exactly 32 bytes when decoded', () => {
52+
const entropy = generateEntropy();
53+
// Convert hex string to bytes
54+
const bytes = [];
55+
for (let i = 0; i < entropy.length; i += 2) {
56+
bytes.push(parseInt(entropy.substr(i, 2), 16));
57+
}
58+
expect(bytes.length).to.equal(32);
59+
});
60+
61+
it('generates values with good distribution', () => {
62+
// Generate multiple samples and check that we get a variety of hex digits
63+
const samples = [];
64+
for (let i = 0; i < 10; i++) {
65+
samples.push(generateEntropy());
66+
}
67+
68+
// Check that we see various hex digits (not all zeros or all ones)
69+
const allChars = samples.join('');
70+
const uniqueChars = new Set(allChars).size;
71+
72+
// We should see most of the 16 possible hex digits (0-9, a-f)
73+
// With 640 characters (10 * 64), we expect to see all 16
74+
expect(uniqueChars).to.be.at.least(10);
75+
});
76+
});
77+
});

0 commit comments

Comments
 (0)