Skip to content

Commit

Permalink
feat(core): add 'bytesToHex', 'HexBlob' and 'castHexBlob' utils
Browse files Browse the repository at this point in the history
add 'fromHexBlob' to some existing OpaqueString types, simplify their tests
  • Loading branch information
mkazlauskas committed Feb 23, 2022
1 parent d67debb commit dc33f15
Show file tree
Hide file tree
Showing 11 changed files with 119 additions and 55 deletions.
4 changes: 2 additions & 2 deletions packages/core/src/Asset/util/assetId.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AssetId, AssetName, PolicyId } from '../../Cardano';
import { CSL } from '../../CSL';
import { CSL, util } from '../../';

export const policyIdFromAssetId = (assetId: AssetId): PolicyId => PolicyId(assetId.slice(0, 56));
export const assetNameFromAssetId = (assetId: AssetId): AssetName => AssetName(assetId.slice(56));
Expand All @@ -8,7 +8,7 @@ export const assetNameFromAssetId = (assetId: AssetId): AssetName => AssetName(a
* @returns {string} concatenated hex-encoded policy id and asset name
*/
export const createAssetId = (scriptHash: CSL.ScriptHash, assetName: CSL.AssetName): AssetId =>
AssetId(Buffer.from(scriptHash.to_bytes()).toString('hex') + Buffer.from(assetName.name()).toString('hex'));
AssetId(util.bytesToHex(scriptHash.to_bytes()) + util.bytesToHex(assetName.name()).toString());

export const parseAssetId = (assetId: AssetId) => {
const policyId = policyIdFromAssetId(assetId);
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/CSL/cslToCore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Asset, CSL, Cardano } from '..';
import { Asset, CSL, Cardano, util } from '..';
import { Transaction } from '@emurgo/cardano-serialization-lib-nodejs';

export const tx = (_input: Transaction): Cardano.TxAlonzo => {
Expand Down Expand Up @@ -33,14 +33,14 @@ export const value = (cslValue: CSL.Value): Cardano.Value => {
export const txIn = (input: CSL.TransactionInput, address: Cardano.Address): Cardano.TxIn => ({
address,
index: input.index(),
txId: Cardano.TransactionId(Buffer.from(input.transaction_id().to_bytes()).toString('hex'))
txId: Cardano.TransactionId.fromHexBlob(util.bytesToHex(input.transaction_id().to_bytes()))
});

export const txOut = (output: CSL.TransactionOutput): Cardano.TxOut => {
const dataHashBytes = output.data_hash()?.to_bytes();
return {
address: Cardano.Address(output.address().to_bech32()),
datum: dataHashBytes ? Cardano.Hash32ByteBase16(Buffer.from(dataHashBytes).toString('hex')) : undefined,
datum: dataHashBytes ? Cardano.Hash32ByteBase16.fromHexBlob(util.bytesToHex(dataHashBytes)) : undefined,
value: value(output.amount())
};
};
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/Cardano/types/Key.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { Hash32ByteBase16, OpaqueString, typedHex } from '../util';
import { Hash32ByteBase16, HexBlob, OpaqueString, castHexBlob, typedHex } from '../util';

/**
* BIP32 public key as hex string
*/
export type Bip32PublicKey = OpaqueString<'Bip32PublicKey'>;
export const Bip32PublicKey = (key: string): Bip32PublicKey => typedHex(key, 128);
Bip32PublicKey.fromHexBlob = (value: HexBlob) => castHexBlob<Bip32PublicKey>(value, 128);

/**
* BIP32 private key as hex string
*/
export type Bip32PrivateKey = OpaqueString<'Bip32PrivateKey'>;
export const Bip32PrivateKey = (key: string): Bip32PrivateKey => typedHex(key, 192);
Bip32PrivateKey.fromHexBlob = (value: HexBlob) => castHexBlob<Bip32PrivateKey>(value, 192);

/**
* Ed25519 public key as hex string
Expand All @@ -22,6 +24,7 @@ export type Ed25519PublicKey = OpaqueString<'Ed25519PublicKey'>;
* @throws InvalidStringError
*/
export const Ed25519PublicKey = (value: string): Ed25519PublicKey => typedHex(value, 64);
Ed25519PublicKey.fromHexBlob = (value: HexBlob) => castHexBlob<Ed25519PublicKey>(value, 64);

/**
* 32 byte ED25519 key hash as hex string
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/Cardano/types/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as Cardano from '.';
import { AuxiliaryData } from './AuxiliaryData';
import { BlockBodyAlonzo } from '@cardano-ogmios/schema';
import { Ed25519PublicKey } from './Key';
import { Hash32ByteBase16, OpaqueString, typedHex } from '../util';
import { Hash32ByteBase16, HexBlob, OpaqueString, typedHex } from '../util';
import { PartialBlockHeader } from './Block';

/**
Expand All @@ -16,6 +16,7 @@ export type TransactionId = Hash32ByteBase16<'TransactionId'>;
* @throws InvalidStringError
*/
export const TransactionId = (value: string): TransactionId => Hash32ByteBase16<'TransactionId'>(value);
TransactionId.fromHexBlob = (value: HexBlob) => Hash32ByteBase16.fromHexBlob<TransactionId>(value);

/**
* Ed25519 signature as hex string
Expand Down
24 changes: 21 additions & 3 deletions packages/core/src/Cardano/util/primitives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,19 @@ export const typedBech32 = <T>(
return target as unknown as T;
};

const assertLength = (expectedLength: number | undefined, target: string) => {
if (expectedLength && target.length !== expectedLength) {
throw new InvalidStringError(`expected length '${expectedLength}', got ${target.length}`);
}
};

/**
* @param {string} target hex string to validate
* @param {string} expectedLength expected string length, >0
* @throws {InvalidStringError}
*/
export const assertIsHexString = (target: string, expectedLength?: number): void => {
if (expectedLength && target.length !== expectedLength) {
throw new InvalidStringError(`expected length '${expectedLength}', got ${target.length}`);
}
assertLength(expectedLength, target);
// eslint-disable-next-line wrap-regex
if (!/^[\da-f]+$/i.test(target)) {
throw new InvalidStringError('expected hex string');
Expand All @@ -79,6 +83,19 @@ export const typedHex = <T>(value: string, length?: number): T => {
return value as any as T;
};

export type HexBlob = OpaqueString<'HexBlob'>;
export const HexBlob = (target: string): HexBlob => typedHex(target);
/**
* Cast HexBlob it into another OpaqueString type.
*
* @param {HexBlob} target hex string to convert
* @param {number} expectedLength optionally validate the length
*/
export const castHexBlob = <T>(target: HexBlob, expectedLength?: number) => {
assertLength(expectedLength, target.toString());
return target as unknown as T;
};

/**
* 32 byte hash as hex string
*/
Expand All @@ -90,6 +107,7 @@ export type Hash32ByteBase16<T extends string = 'Hash32ByteBase16'> = OpaqueStri
*/
export const Hash32ByteBase16 = <T extends string = 'Hash32ByteBase16'>(value: string): Hash32ByteBase16<T> =>
typedHex<Hash32ByteBase16<T>>(value, 64);
Hash32ByteBase16.fromHexBlob = <T>(value: HexBlob) => castHexBlob<T>(value, 64);

/**
* 28 byte hash as hex string
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/util/misc/bytesToHex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { HexBlob } from '../../Cardano/util';

export const bytesToHex = (bytes: Uint8Array): HexBlob => HexBlob(Buffer.from(bytes).toString('hex'));
1 change: 1 addition & 0 deletions packages/core/src/util/misc/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './replaceNullsToUndefineds';
export * from './isNotNil';
export * from './bytesToHex';
50 changes: 23 additions & 27 deletions packages/core/test/Cardano/types/Key.test.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,51 @@
/* eslint-disable max-len */
import { Cardano } from '../../../src';

jest.mock('../../../src/Cardano/util/primitives', () => {
const actual = jest.requireActual('../../../src/Cardano/util/primitives');
return {
Hash32ByteBase16: jest.fn().mockImplementation((...args) => actual.Hash32ByteBase16(...args)),
typedHex: jest.fn().mockImplementation((...args) => actual.typedHex(...args))
};
});

describe('Cardano/types/Key', () => {
it('Ed25519PublicKey() accepts a valid public key hex string and is implemented using util.typedHex', () => {
it('Ed25519PublicKey() accepts a valid public key hex string', () => {
expect(() =>
Cardano.Ed25519PublicKey('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39')
).not.toThrow();
expect(Cardano.util.typedHex).toBeCalledWith(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39',
64
);
expect(() =>
Cardano.Ed25519PublicKey.fromHexBlob(
Cardano.util.HexBlob('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39')
)
).not.toThrow();
});

it('Ed25519KeyHash() accepts a key hash hex string and is implemented using util.Hash32ByteBase16', () => {
it('Ed25519KeyHash() accepts a key hash hex string', () => {
expect(() =>
Cardano.Ed25519KeyHash('6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39')
).not.toThrow();
expect(Cardano.util.Hash32ByteBase16).toBeCalledWith(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39'
);
});

it('Bip32PublicKey() accepts a valid public key hex string and is implemented using util.typedHex', () => {
it('Bip32PublicKey() accepts a valid public key hex string', () => {
expect(() =>
Cardano.Bip32PublicKey(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d396199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39'
)
).not.toThrow();
expect(Cardano.util.typedHex).toBeCalledWith(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d396199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39',
128
);
expect(() =>
Cardano.Bip32PublicKey.fromHexBlob(
Cardano.util.HexBlob(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d396199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39'
)
)
).not.toThrow();
});

it('Bip32PrivateKey() accepts a valid public key hex string and is implemented using util.typedHex', () => {
it('Bip32PrivateKey() accepts a valid public key hex string', () => {
expect(() =>
Cardano.Bip32PrivateKey(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d36199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d3996199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39'
)
).not.toThrow();
expect(Cardano.util.typedHex).toBeCalledWith(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d36199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d3996199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39',
192
);
expect(() =>
Cardano.Bip32PrivateKey.fromHexBlob(
Cardano.util.HexBlob(
'6199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d36199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d3996199186adb51974690d7247d2646097d2c62763b767b528816fb7ed3f9f55d39'
)
)
).not.toThrow();
});
});
24 changes: 7 additions & 17 deletions packages/core/test/Cardano/types/Transaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
import { Ed25519Signature, TransactionId, util } from '../../../src/Cardano';

jest.mock('../../../src/Cardano/util/primitives', () => {
const actual = jest.requireActual('../../../src/Cardano/util/primitives');
return {
Hash32ByteBase16: jest.fn().mockImplementation((...args) => actual.Hash32ByteBase16(...args)),
typedHex: jest.fn().mockImplementation((...args) => actual.typedHex(...args))
};
});
import { Ed25519Signature, TransactionId } from '../../../src/Cardano';
import { HexBlob } from '../../../src/Cardano/util';

describe('Cardano/types/Transaction', () => {
it('TransactionId accepts a valid transaction hash hex string and is implemented with util.Hash32ByteBase16', () => {
it('TransactionId accepts a valid transaction hash hex string', () => {
expect(() => TransactionId('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d')).not.toThrow();
expect(util.Hash32ByteBase16).toBeCalledWith('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d');
expect(() =>
TransactionId.fromHexBlob(HexBlob('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d'))
).not.toThrow();
});

it('Ed25519Signature() accepts a valid signature hex string and is implemented using util.typedHex', () => {
it('Ed25519Signature() accepts a valid signature hex string', () => {
expect(() =>
Ed25519Signature(
// eslint-disable-next-line max-len
'709f937c4ce152c81f8406c03279ff5a8556a12a8657e40a578eaaa6223d2e6a2fece39733429e3ec73a6c798561b5c2d47d82224d656b1d964cfe8b5fdffe09'
)
).not.toThrow();
expect(util.typedHex).toBeCalledWith(
// eslint-disable-next-line max-len
'709f937c4ce152c81f8406c03279ff5a8556a12a8657e40a578eaaa6223d2e6a2fece39733429e3ec73a6c798561b5c2d47d82224d656b1d964cfe8b5fdffe09',
128
);
});
});
49 changes: 48 additions & 1 deletion packages/core/test/Cardano/util/primitives.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Hash28ByteBase16, Hash32ByteBase16, typedBech32, typedHex } from '../../../src/Cardano/util';
/* eslint-disable sonarjs/no-duplicate-string */
import {
Hash28ByteBase16,
Hash32ByteBase16,
HexBlob,
castHexBlob,
typedBech32,
typedHex
} from '../../../src/Cardano/util';
import { InvalidStringError } from '../../../src/errors';

describe('Cardano.util/primitives', () => {
Expand Down Expand Up @@ -63,21 +71,60 @@ describe('Cardano.util/primitives', () => {
});
});

describe('HexBlob', () => {
it('throws when asserting an empty string', () => {
expect(() => HexBlob('')).toThrowError(InvalidStringError);
});

it('does not throw when asserting a valid hex string', () => {
expect(() => HexBlob('ABCDEF')).not.toThrowError();
expect(() => HexBlob('1234567890abcdef')).not.toThrowError();
});

it('throws when string has an non base16 character', () => {
expect(() => HexBlob(' 1234567890abcdef')).toThrowError(InvalidStringError);
expect(() => HexBlob('1234567890abcdefg')).toThrowError(InvalidStringError);
});
});

describe('castHexBlob', () => {
it('returns the same string', () => {
expect(castHexBlob(HexBlob('abc123'))).toEqual('abc123');
});

it('does not throw when string length matches expectedLength', () => {
expect(() => castHexBlob(HexBlob('ABCDEF'), 6)).not.toThrowError();
});

it('throws when string length does not match expectedLength', () => {
expect(() => castHexBlob(HexBlob('ABCDEF'), 5)).toThrowError(InvalidStringError);
});
});

describe('Hash32ByteBase16', () => {
it('expects a hex string with length of 64', () => {
expect(() => Hash32ByteBase16('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d')).not.toThrow();
expect(() =>
Hash32ByteBase16.fromHexBlob(HexBlob('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d'))
).not.toThrow();
});

it('throws with non-hex string', () => {
expect(() => Hash32ByteBase16('ge33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d')).toThrowError(
InvalidStringError
);
expect(() =>
Hash32ByteBase16.fromHexBlob(HexBlob('ge33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d'))
).toThrowError(InvalidStringError);
});

it('throws with hex string of different length', () => {
expect(() => Hash32ByteBase16('e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d')).toThrowError(
InvalidStringError
);
expect(() =>
Hash32ByteBase16.fromHexBlob(HexBlob('e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d'))
).toThrowError(InvalidStringError);
});
});

Expand Down
5 changes: 5 additions & 0 deletions packages/core/test/util/bytesToHex.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { util } from '../../src/util';

test('bytesToHex', () => {
expect(util.bytesToHex(Buffer.from('abc'))).toBe('616263');
});

0 comments on commit dc33f15

Please sign in to comment.