Skip to content

Commit

Permalink
refactor(experimental): add coercion utility functions (#1619)
Browse files Browse the repository at this point in the history
  • Loading branch information
buffalojoec authored Sep 30, 2023
1 parent c0c57ff commit da22638
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 0 deletions.
17 changes: 17 additions & 0 deletions packages/addresses/src/__tests__/coercions-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { address, Base58EncodedAddress } from '../base58';

describe('coercions', () => {
describe('address', () => {
it('can coerce to `Base58EncodedAddress`', () => {
// See scripts/fixtures/GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G.json
const raw =
'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G' as Base58EncodedAddress<'GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G'>;
const coerced = address('GQE2yjns7SKKuMc89tveBDpzYHwXfeuB2PGAbGaPWc6G');
expect(coerced).toBe(raw);
});
it('throws on invalid `Base58EncodedAddress`', () => {
const thisThrows = () => address('3333333333333333');
expect(thisThrows).toThrow('`3333333333333333` is not a base-58 encoded address');
});
});
});
3 changes: 3 additions & 0 deletions packages/addresses/src/__typetests__/coercions-typetests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { address, Base58EncodedAddress } from '../base58';

address('555555555555555555555555') satisfies Base58EncodedAddress<'555555555555555555555555'>;
28 changes: 28 additions & 0 deletions packages/addresses/src/base58.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,27 @@ export type Base58EncodedAddress<TAddress extends string = string> = TAddress &
readonly __brand: unique symbol;
};

export function isBase58EncodedAddress(
putativeBase58EncodedAddress: string
): putativeBase58EncodedAddress is Base58EncodedAddress<typeof putativeBase58EncodedAddress> {
// Fast-path; see if the input string is of an acceptable length.
if (
// Lowest address (32 bytes of zeroes)
putativeBase58EncodedAddress.length < 32 ||
// Highest address (32 bytes of 255)
putativeBase58EncodedAddress.length > 44
) {
return false;
}
// Slow-path; actually attempt to decode the input string.
const bytes = base58.serialize(putativeBase58EncodedAddress);
const numBytes = bytes.byteLength;
if (numBytes !== 32) {
return false;
}
return true;
}

export function assertIsBase58EncodedAddress(
putativeBase58EncodedAddress: string
): asserts putativeBase58EncodedAddress is Base58EncodedAddress<typeof putativeBase58EncodedAddress> {
Expand All @@ -30,6 +51,13 @@ export function assertIsBase58EncodedAddress(
}
}

export function address<TAddress extends string = string>(
putativeBase58EncodedAddress: TAddress
): Base58EncodedAddress<TAddress> {
assertIsBase58EncodedAddress(putativeBase58EncodedAddress);
return putativeBase58EncodedAddress as Base58EncodedAddress<TAddress>;
}

export function getBase58EncodedAddressCodec(
config?: Readonly<{
description: string;
Expand Down
67 changes: 67 additions & 0 deletions packages/rpc-core/src/__tests__/coercions-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { lamports, LamportsUnsafeBeyond2Pow53Minus1 } from '../lamports';
import { StringifiedBigInt, stringifiedBigInt } from '../stringified-bigint';
import { StringifiedNumber, stringifiedNumber } from '../stringified-number';
import { TransactionSignature, transactionSignature } from '../transaction-signature';
import { UnixTimestamp, unixTimestamp } from '../unix-timestamp';

describe('coercions', () => {
describe('lamports', () => {
it('can coerce to `LamportsUnsafeBeyond2Pow53Minus1`', () => {
const raw = 1234n as LamportsUnsafeBeyond2Pow53Minus1;
const coerced = lamports(1234n);
expect(coerced).toBe(raw);
});
it('throws on invalid `LamportsUnsafeBeyond2Pow53Minus1`', () => {
const thisThrows = () => lamports(-5n);
expect(thisThrows).toThrow('Input for 64-bit unsigned integer cannot be negative');
});
});
describe('stringifiedBigInt', () => {
it('can coerce to `StringifiedBigInt`', () => {
const raw = '1234' as StringifiedBigInt;
const coerced = stringifiedBigInt('1234');
expect(coerced).toBe(raw);
});
it('throws on invalid `StringifiedBigInt`', () => {
const thisThrows = () => stringifiedBigInt('test');
expect(thisThrows).toThrow('`test` cannot be parsed as a BigInt');
});
});
describe('stringifiedNumber', () => {
it('can coerce to `StringifiedNumber`', () => {
const raw = '1234' as StringifiedNumber;
const coerced = stringifiedNumber('1234');
expect(coerced).toBe(raw);
});
it('throws on invalid `StringifiedNumber`', () => {
const thisThrows = () => stringifiedNumber('test');
expect(thisThrows).toThrow('`test` cannot be parsed as a Number');
});
});
describe('transactionSignature', () => {
it('can coerce to `TransactionSignature`', () => {
// Randomly generated
const raw =
'3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoPaL' as TransactionSignature;
const coerced = transactionSignature(
'3bwsNoq6EP89sShUAKBeB26aCC3KLGNajRm5wqwr6zRPP3gErZH7erSg3332SVY7Ru6cME43qT35Z7JKpZqCoPaL'
);
expect(coerced).toBe(raw);
});
it('throws on invalid `TransactionSignature`', () => {
const thisThrows = () => transactionSignature('test');
expect(thisThrows).toThrow('`test` is not a transaction signature');
});
});
describe('unixTimestamp', () => {
it('can coerce to `UnixTimestamp`', () => {
const raw = 1234 as UnixTimestamp;
const coerced = unixTimestamp(1234);
expect(coerced).toBe(raw);
});
it('throws on invalid `UnixTimestamp`', () => {
const thisThrows = () => unixTimestamp(8.75e15);
expect(thisThrows).toThrow('`8750000000000000` is not a timestamp');
});
});
});
11 changes: 11 additions & 0 deletions packages/rpc-core/src/__typetests__/coercions-typetests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { lamports, LamportsUnsafeBeyond2Pow53Minus1 } from '../lamports';
import { StringifiedBigInt, stringifiedBigInt } from '../stringified-bigint';
import { StringifiedNumber, stringifiedNumber } from '../stringified-number';
import { TransactionSignature, transactionSignature } from '../transaction-signature';
import { UnixTimestamp, unixTimestamp } from '../unix-timestamp';

lamports(50_000_000_000_000n) satisfies LamportsUnsafeBeyond2Pow53Minus1;
stringifiedBigInt('50_000_000_000_000') satisfies StringifiedBigInt;
stringifiedNumber('50_000_000_000_000') satisfies StringifiedNumber;
transactionSignature('x') satisfies TransactionSignature;
unixTimestamp(0) satisfies UnixTimestamp;
9 changes: 9 additions & 0 deletions packages/rpc-core/src/lamports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export type LamportsUnsafeBeyond2Pow53Minus1 = bigint & { readonly __brand: uniq
// Largest possible value to be represented by a u64
const maxU64Value = 18446744073709551615n; // 2n ** 64n - 1n

export function isLamports(putativeLamports: bigint): putativeLamports is LamportsUnsafeBeyond2Pow53Minus1 {
return putativeLamports >= 0 && putativeLamports <= maxU64Value;
}

export function assertIsLamports(
putativeLamports: bigint
): asserts putativeLamports is LamportsUnsafeBeyond2Pow53Minus1 {
Expand All @@ -19,3 +23,8 @@ export function assertIsLamports(
throw new Error('Input number is too large to be represented as a 64-bit unsigned integer');
}
}

export function lamports(putativeLamports: bigint): LamportsUnsafeBeyond2Pow53Minus1 {
assertIsLamports(putativeLamports);
return putativeLamports;
}
14 changes: 14 additions & 0 deletions packages/rpc-core/src/stringified-bigint.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
export type StringifiedBigInt = string & { readonly __brand: unique symbol };

export function isStringifiedBigInt(putativeBigInt: string): putativeBigInt is StringifiedBigInt {
try {
BigInt(putativeBigInt);
return true;
} catch (_) {
return false;
}
}

export function assertIsStringifiedBigInt(putativeBigInt: string): asserts putativeBigInt is StringifiedBigInt {
try {
BigInt(putativeBigInt);
Expand All @@ -9,3 +18,8 @@ export function assertIsStringifiedBigInt(putativeBigInt: string): asserts putat
});
}
}

export function stringifiedBigInt(putativeBigInt: string): StringifiedBigInt {
assertIsStringifiedBigInt(putativeBigInt);
return putativeBigInt;
}
9 changes: 9 additions & 0 deletions packages/rpc-core/src/stringified-number.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
export type StringifiedNumber = string & { readonly __brand: unique symbol };

export function isStringifiedNumber(putativeNumber: string): putativeNumber is StringifiedNumber {
return !Number.isNaN(Number(putativeNumber));
}

export function assertIsStringifiedNumber(putativeNumber: string): asserts putativeNumber is StringifiedNumber {
if (Number.isNaN(Number(putativeNumber))) {
throw new Error(`\`${putativeNumber}\` cannot be parsed as a Number`);
}
}

export function stringifiedNumber(putativeNumber: string): StringifiedNumber {
assertIsStringifiedNumber(putativeNumber);
return putativeNumber;
}
26 changes: 26 additions & 0 deletions packages/rpc-core/src/transaction-signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@ import { base58 } from '@metaplex-foundation/umi-serializers';

export type TransactionSignature = string & { readonly __brand: unique symbol };

export function isTransactionSignature(
putativeTransactionSignature: string
): putativeTransactionSignature is TransactionSignature {
// Fast-path; see if the input string is of an acceptable length.
if (
// Lowest value (64 bytes of zeroes)
putativeTransactionSignature.length < 64 ||
// Highest value (64 bytes of 255)
putativeTransactionSignature.length > 88
) {
return false;
}
// Slow-path; actually attempt to decode the input string.
const bytes = base58.serialize(putativeTransactionSignature);
const numBytes = bytes.byteLength;
if (numBytes !== 64) {
return false;
}
return true;
}

export function assertIsTransactionSignature(
putativeTransactionSignature: string
): asserts putativeTransactionSignature is TransactionSignature {
Expand All @@ -27,3 +48,8 @@ export function assertIsTransactionSignature(
});
}
}

export function transactionSignature(putativeTransactionSignature: string): TransactionSignature {
assertIsTransactionSignature(putativeTransactionSignature);
return putativeTransactionSignature;
}
13 changes: 13 additions & 0 deletions packages/rpc-core/src/unix-timestamp.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
export type UnixTimestamp = number & { readonly __brand: unique symbol };

export function isUnixTimestamp(putativeTimestamp: number): putativeTimestamp is UnixTimestamp {
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date
if (putativeTimestamp > 8.64e15 || putativeTimestamp < -8.64e15) {
return false;
}
return true;
}

export function assertIsUnixTimestamp(putativeTimestamp: number): asserts putativeTimestamp is UnixTimestamp {
// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#the_epoch_timestamps_and_invalid_date
try {
Expand All @@ -12,3 +20,8 @@ export function assertIsUnixTimestamp(putativeTimestamp: number): asserts putati
});
}
}

export function unixTimestamp(putativeTimestamp: number): UnixTimestamp {
assertIsUnixTimestamp(putativeTimestamp);
return putativeTimestamp;
}

0 comments on commit da22638

Please sign in to comment.