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
14 changes: 8 additions & 6 deletions packages/cashscript/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,12 @@ import {
isHex,
bigIntToCompactUint,
NonFungibleTokenCapability,
bigIntToVmNumber,
SigningSerializationFlag,
} from '@bitauth/libauth';
import {
encodeInt,
hash160,
hash256,
sha256,
Op,
Script,
scriptToBytecode,
Expand Down Expand Up @@ -419,6 +417,10 @@ export function getNetworkPrefix(network: string): 'bitcoincash' | 'bchtest' | '

const randomInt = (): bigint => BigInt(Math.floor(Math.random() * 10000));

// Transaction ids, token categories and NFT commitments are random bytes, so tests that create many random UTXOs
// or tokens don't end up with colliding outpoints or categories
const randomBytesHex = (length: number): string => binToHex(crypto.getRandomValues(new Uint8Array(length)));

/**
* Generate a random `Utxo` for use with `MockNetworkProvider.addUtxo` in tests and examples.
* Fields can be overridden by passing a `defaults` object.
Expand All @@ -427,7 +429,7 @@ const randomInt = (): bigint => BigInt(Math.floor(Math.random() * 10000));
* @returns A synthetic UTXO with random `txid`, `vout`, and `satoshis`.
*/
export const randomUtxo = <T extends Partial<Utxo>>(defaults?: T): Utxo & T => ({
txid: binToHex(sha256(bigIntToVmNumber(randomInt()))),
txid: randomBytesHex(32),
vout: Math.floor(Math.random() * 10),
satoshis: 100_000n + randomInt(),
...defaults,
Expand All @@ -442,7 +444,7 @@ export const randomUtxo = <T extends Partial<Utxo>>(defaults?: T): Utxo & T => (
*/
export const randomToken = (defaults?: Partial<TokenDetails>): TokenDetails => ({
...{
category: binToHex(sha256(bigIntToVmNumber(randomInt()))),
category: randomBytesHex(32),
amount: 100_000n + randomInt(),
},
...defaults,
Expand All @@ -457,10 +459,10 @@ export const randomToken = (defaults?: Partial<TokenDetails>): TokenDetails => (
*/
export const randomNFT = (defaults?: Partial<TokenDetails>): TokenDetails => ({
...{
category: binToHex(sha256(bigIntToVmNumber(randomInt()))),
category: randomBytesHex(32),
amount: 0n,
nft: {
commitment: binToHex(sha256(bigIntToVmNumber(randomInt()))).slice(0, 8),
commitment: randomBytesHex(4),
capability: NonFungibleTokenCapability.none,
},
},
Expand Down
41 changes: 41 additions & 0 deletions packages/cashscript/test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
scriptToAddress,
createUnlockingBytecode,
getInputSize,
randomUtxo,
randomToken,
randomNFT,
} from '../src/utils.js';
import { Network } from '../src/interfaces.js';
import { alicePkh, alicePub } from './fixture/vars.js';
Expand Down Expand Up @@ -115,4 +118,42 @@ describe('utils', () => {
expect(regtestAddressP2sh20Tokens).toEqual(expectedRegtestAddressP2sh20Tokens);
});
});

describe('random test helpers', () => {
// With the old 10,000 possible values, 2,000 draws were all but certain to collide
const draws = 2_000;
const distinct = (values: string[]): number => new Set(values).size;

it('should give every random UTXO a distinct outpoint', () => {
const utxos = Array.from({ length: draws }, () => randomUtxo());

expect(distinct(utxos.map((utxo) => `${utxo.txid}:${utxo.vout}`))).toBe(draws);
utxos.forEach((utxo) => expect(utxo.txid).toMatch(/^[0-9a-f]{64}$/));
});

it('should give every random token a distinct category', () => {
const categories = Array.from({ length: draws }, () => randomToken().category);

expect(distinct(categories)).toBe(draws);
categories.forEach((category) => expect(category).toMatch(/^[0-9a-f]{64}$/));
});

it('should give random NFTs distinct categories and 4-byte commitments', () => {
const nfts = Array.from({ length: draws }, () => randomNFT());
const commitments = nfts.map((nft) => nft.nft!.commitment);

expect(distinct(nfts.map((nft) => nft.category))).toBe(draws);
commitments.forEach((commitment) => expect(commitment).toMatch(/^[0-9a-f]{8}$/));
// 4 random bytes can still collide by chance (~0.05% for 2,000 draws), so allow a few repeats
expect(distinct(commitments)).toBeGreaterThan(draws - 5);
});

it('should still let defaults override the random fields', () => {
const txid = 'ab'.repeat(32);

expect(randomUtxo({ txid, vout: 3 })).toMatchObject({ txid, vout: 3 });
expect(randomToken({ category: txid }).category).toBe(txid);
expect(randomNFT({ category: txid }).category).toBe(txid);
});
});
});
Loading