Skip to content

Commit

Permalink
feat: allow nested concrete ClarityValue types to be specified
Browse files Browse the repository at this point in the history
  • Loading branch information
zone117x committed Jun 3, 2021
1 parent 7b6ceb3 commit da68307
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 43 deletions.
6 changes: 4 additions & 2 deletions packages/transactions/src/clarity/clarityValue.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Buffer } from '@stacks/common';
import {
BooleanCV,
OptionalCV,
BufferCV,
IntCV,
UIntCV,
Expand All @@ -13,6 +12,8 @@ import {
TupleCV,
StringAsciiCV,
StringUtf8CV,
NoneCV,
SomeCV,
} from '.';

import { principalToString } from './types/principalCV';
Expand Down Expand Up @@ -42,14 +43,15 @@ export enum ClarityType {

export type ClarityValue =
| BooleanCV
| OptionalCV
| BufferCV
| IntCV
| UIntCV
| StandardPrincipalCV
| ContractPrincipalCV
| ResponseErrorCV
| ResponseOkCV
| NoneCV
| SomeCV
| ListCV
| TupleCV
| StringAsciiCV
Expand Down
46 changes: 29 additions & 17 deletions packages/transactions/src/clarity/deserialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,57 +21,69 @@ import { deserializeAddress, deserializeLPString } from '../types';
import { DeserializationError } from '../errors';
import { stringAsciiCV, stringUtf8CV } from './types/stringCV';

export default function deserializeCV(buffer: BufferReader | Buffer): ClarityValue {
const bufferReader = Buffer.isBuffer(buffer) ? new BufferReader(buffer) : buffer;
export default function deserializeCV<T extends ClarityValue = ClarityValue>(
serializedClarityValue: BufferReader | Buffer | string
): T {
let bufferReader: BufferReader;
if (typeof serializedClarityValue === 'string') {
const hasHexPrefix = serializedClarityValue.slice(0, 2).toLowerCase() === '0x';
bufferReader = new BufferReader(
Buffer.from(hasHexPrefix ? serializedClarityValue.slice(2) : serializedClarityValue, 'hex')
);
} else if (Buffer.isBuffer(serializedClarityValue)) {
bufferReader = new BufferReader(serializedClarityValue);
} else {
bufferReader = serializedClarityValue;
}
const type = bufferReader.readUInt8Enum(ClarityType, n => {
throw new DeserializationError(`Cannot recognize Clarity Type: ${n}`);
});

switch (type) {
case ClarityType.Int:
return intCV(bufferReader.readBuffer(16));
return intCV(bufferReader.readBuffer(16)) as T;

case ClarityType.UInt:
return uintCV(bufferReader.readBuffer(16));
return uintCV(bufferReader.readBuffer(16)) as T;

case ClarityType.Buffer:
const bufferLength = bufferReader.readUInt32BE();
return bufferCV(bufferReader.readBuffer(bufferLength));
return bufferCV(bufferReader.readBuffer(bufferLength)) as T;

case ClarityType.BoolTrue:
return trueCV();
return trueCV() as T;

case ClarityType.BoolFalse:
return falseCV();
return falseCV() as T;

case ClarityType.PrincipalStandard:
const sAddress = deserializeAddress(bufferReader);
return standardPrincipalCVFromAddress(sAddress);
return standardPrincipalCVFromAddress(sAddress) as T;

case ClarityType.PrincipalContract:
const cAddress = deserializeAddress(bufferReader);
const contractName = deserializeLPString(bufferReader);
return contractPrincipalCVFromAddress(cAddress, contractName);
return contractPrincipalCVFromAddress(cAddress, contractName) as T;

case ClarityType.ResponseOk:
return responseOkCV(deserializeCV(bufferReader));
return responseOkCV(deserializeCV(bufferReader)) as T;

case ClarityType.ResponseErr:
return responseErrorCV(deserializeCV(bufferReader));
return responseErrorCV(deserializeCV(bufferReader)) as T;

case ClarityType.OptionalNone:
return noneCV();
return noneCV() as T;

case ClarityType.OptionalSome:
return someCV(deserializeCV(bufferReader));
return someCV(deserializeCV(bufferReader)) as T;

case ClarityType.List:
const listLength = bufferReader.readUInt32BE();
const listContents: ClarityValue[] = [];
for (let i = 0; i < listLength; i++) {
listContents.push(deserializeCV(bufferReader));
}
return listCV(listContents);
return listCV(listContents) as T;

case ClarityType.Tuple:
const tupleLength = bufferReader.readUInt32BE();
Expand All @@ -83,17 +95,17 @@ export default function deserializeCV(buffer: BufferReader | Buffer): ClarityVal
}
tupleContents[clarityName] = deserializeCV(bufferReader);
}
return tupleCV(tupleContents);
return tupleCV(tupleContents) as T;

case ClarityType.StringASCII:
const asciiStrLen = bufferReader.readUInt32BE();
const asciiStr = bufferReader.readBuffer(asciiStrLen).toString('ascii');
return stringAsciiCV(asciiStr);
return stringAsciiCV(asciiStr) as T;

case ClarityType.StringUTF8:
const utf8StrLen = bufferReader.readUInt32BE();
const utf8Str = bufferReader.readBuffer(utf8StrLen).toString('utf8');
return stringUtf8CV(utf8Str);
return stringUtf8CV(utf8Str) as T;

default:
throw new DeserializationError(
Expand Down
6 changes: 3 additions & 3 deletions packages/transactions/src/clarity/types/listCV.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { ClarityValue, ClarityType } from '../clarityValue';

interface ListCV {
interface ListCV<T extends ClarityValue = ClarityValue> {
type: ClarityType.List;
list: ClarityValue[];
list: T[];
}

function listCV<T extends ClarityValue>(values: T[]): ListCV {
function listCV<T extends ClarityValue = ClarityValue>(values: T[]): ListCV<T> {
return { type: ClarityType.List, list: values };
}

Expand Down
20 changes: 13 additions & 7 deletions packages/transactions/src/clarity/types/optionalCV.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
import { ClarityType, ClarityValue } from '../clarityValue';

type OptionalCV = NoneCV | SomeCV;
type OptionalCV<T extends ClarityValue = ClarityValue> = NoneCV | SomeCV<T>;

interface NoneCV {
readonly type: ClarityType.OptionalNone;
}

interface SomeCV {
interface SomeCV<T extends ClarityValue = ClarityValue> {
readonly type: ClarityType.OptionalSome;
readonly value: ClarityValue;
readonly value: T;
}

const noneCV = (): OptionalCV => ({ type: ClarityType.OptionalNone });
const someCV = (value: ClarityValue): OptionalCV => ({ type: ClarityType.OptionalSome, value });
const optionalCVOf = (value?: ClarityValue): OptionalCV => {
function noneCV(): NoneCV {
return { type: ClarityType.OptionalNone };
}

function someCV<T extends ClarityValue = ClarityValue>(value: T): OptionalCV<T> {
return { type: ClarityType.OptionalSome, value };
}

function optionalCVOf<T extends ClarityValue = ClarityValue>(value?: T): OptionalCV<T> {
if (value) {
return someCV(value);
} else {
return noneCV();
}
};
}

export { OptionalCV, NoneCV, SomeCV, noneCV, someCV, optionalCVOf };
12 changes: 6 additions & 6 deletions packages/transactions/src/clarity/types/responseCV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { ClarityType, ClarityValue } from '../clarityValue';

type ResponseCV = ResponseErrorCV | ResponseOkCV;

interface ResponseErrorCV {
interface ResponseErrorCV<T extends ClarityValue = ClarityValue> {
readonly type: ClarityType.ResponseErr;
readonly value: ClarityValue;
readonly value: T;
}

interface ResponseOkCV {
interface ResponseOkCV<T extends ClarityValue = ClarityValue> {
readonly type: ClarityType.ResponseOk;
readonly value: ClarityValue;
readonly value: T;
}

function responseErrorCV(value: ClarityValue): ResponseErrorCV {
function responseErrorCV<T extends ClarityValue = ClarityValue>(value: T): ResponseErrorCV<T> {
return { type: ClarityType.ResponseErr, value };
}

function responseOkCV(value: ClarityValue): ResponseOkCV {
function responseOkCV<T extends ClarityValue = ClarityValue>(value: T): ResponseOkCV<T> {
return { type: ClarityType.ResponseOk, value };
}

Expand Down
8 changes: 4 additions & 4 deletions packages/transactions/src/clarity/types/tupleCV.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ClarityType, ClarityValue } from '../clarityValue';
import { isClarityName } from '../../utils';

type TupleData = { [key: string]: ClarityValue };
type TupleData<T extends ClarityValue = ClarityValue> = { [key: string]: T };

interface TupleCV {
interface TupleCV<T extends TupleData = TupleData> {
type: ClarityType.Tuple;
data: TupleData;
data: T;
}

function tupleCV(data: TupleData): TupleCV {
function tupleCV<T extends ClarityValue = ClarityValue>(data: TupleData<T>): TupleCV<TupleData<T>> {
for (const key in data) {
if (!isClarityName(key)) {
throw new Error(`"${key}" is not a valid Clarity name`);
Expand Down
4 changes: 1 addition & 3 deletions packages/transactions/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@ export function cvToHex(cv: ClarityValue) {
* @param {string} hex - the hex encoded string with or without `0x` prefix
*/
export function hexToCV(hex: string) {
const hexWithoutPrefix = hex.startsWith('0x') ? hex.slice(2) : hex;
const bufferCV = Buffer.from(hexWithoutPrefix, 'hex');
return deserializeCV(bufferCV);
return deserializeCV(hex);
}
/**
* Read only function response object
Expand Down
124 changes: 123 additions & 1 deletion packages/transactions/tests/clarity.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { oneLineTrim } from 'common-tags';
import { deserializeAddress } from '../src/types';
import { addressToString, deserializeAddress } from '../src/types';
import {
ClarityValue,
serializeCV,
Expand All @@ -26,6 +26,10 @@ import {
StringAsciiCV,
stringUtf8CV,
StringUtf8CV,
BufferCV,
SomeCV,
ListCV,
StandardPrincipalCV,
} from '../src/clarity';
import { BufferReader } from '../src/bufferReader';
import { cvToString, cvToJSON } from '../src/clarity/clarityValue';
Expand All @@ -39,6 +43,124 @@ function serializeDeserialize<T extends ClarityValue>(value: T): ClarityValue {
}

describe('Clarity Types', () => {

test('Deserialize with type generics', () => {
const serializedClarityValue = '0x0c00000003096e616d6573706163650200000003666f6f0a70726f706572746965730c000000061963616e2d7570646174652d70726963652d66756e6374696f6e030b6c61756e636865642d61740a0100000000000000000000000000000006086c69666574696d65010000000000000000000000000000000c106e616d6573706163652d696d706f7274051a164247d6f2b425ac5771423ae6c80c754f7172b00e70726963652d66756e6374696f6e0c0000000504626173650100000000000000000000000000000001076275636b6574730b00000010010000000000000000000000000000000101000000000000000000000000000000010100000000000000000000000000000001010000000000000000000000000000000101000000000000000000000000000000010100000000000000000000000000000001010000000000000000000000000000000101000000000000000000000000000000010100000000000000000000000000000001010000000000000000000000000000000101000000000000000000000000000000010100000000000000000000000000000001010000000000000000000000000000000101000000000000000000000000000000010100000000000000000000000000000001010000000000000000000000000000000105636f6566660100000000000000000000000000000001116e6f2d766f77656c2d646973636f756e740100000000000000000000000000000001116e6f6e616c7068612d646973636f756e7401000000000000000000000000000000010b72657665616c65642d61740100000000000000000000000000000003067374617475730d000000057265616479';

// The old way of deserializing without type generics - verbose, hard to read, frustrating to define 🤢
function parseWithManualTypeAssertions() {
const deserializedCv = deserializeCV(serializedClarityValue);
const clVal = deserializedCv as TupleCV;
const namespaceCV = clVal.data['namespace'] as BufferCV;
const statusCV = clVal.data['status'] as StringAsciiCV;
const properties = clVal.data['properties'] as TupleCV;
const launchedAtCV = properties.data['launched-at'] as SomeCV;
const launchAtIntCV = launchedAtCV.value as UIntCV;
const lifetimeCV = properties.data['lifetime'] as IntCV;
const revealedAtCV = properties.data['revealed-at'] as IntCV;
const addressCV = properties.data[
'namespace-import'
] as StandardPrincipalCV;
const priceFunction = properties.data['price-function'] as TupleCV;
const baseCV = priceFunction.data['base'] as IntCV;
const coeffCV = priceFunction.data['coeff'] as IntCV;
const noVowelDiscountCV = priceFunction.data['no-vowel-discount'] as IntCV;
const nonalphaDiscountCV = priceFunction.data['nonalpha-discount'] as IntCV;
const bucketsCV = priceFunction.data['buckets'] as ListCV;
const buckets: number[] = [];
const listCV = bucketsCV.list;
for (let i = 0; i < listCV.length; i++) {
const cv = listCV[i] as UIntCV;
buckets.push(cv.value.toNumber());
}
return {
namespace: namespaceCV.buffer.toString(),
status: statusCV.data,
launchedAt: launchAtIntCV.value.toNumber(),
lifetime: lifetimeCV.value.toNumber(),
revealedAt: revealedAtCV.value.toNumber(),
address: addressToString(addressCV.address),
base: baseCV.value.toNumber(),
coeff: coeffCV.value.toNumber(),
noVowelDiscount: noVowelDiscountCV.value.toNumber(),
nonalphaDiscount: nonalphaDiscountCV.value.toNumber(),
buckets
};
}

// The new way of deserializing with type generics 🙂
function parseWithTypeDefinition() {
// (tuple
// (namespace (buff 3))
// (status (string-ascii 5))
// (properties (tuple
// (launched-at (optional uint))
// (namespace-import principal)
// (lifetime uint)
// (revealed-at uint)
// (price-function (tuple
// (base uint)
// (coeff uint)
// (no-vowel-discount uint)
// (nonalpha-discount uint)
// (buckets (list 16 uint))
//
// Easily map the Clarity type string above to the Typescript definition:
type BnsNamespaceCV = TupleCV<{
['namespace']: BufferCV;
['status']: StringAsciiCV;
['properties']: TupleCV<{
['launched-at']: SomeCV<UIntCV>;
['namespace-import']: StandardPrincipalCV;
['lifetime']: IntCV;
['revealed-at']: IntCV;
['price-function']: TupleCV<{
['base']: IntCV;
['coeff']: IntCV;
['no-vowel-discount']: IntCV;
['nonalpha-discount']: IntCV;
['buckets']: ListCV<UIntCV>;
}>;
}>;
}>;
const cv = deserializeCV<BnsNamespaceCV>(serializedClarityValue);
// easy, fully-typed access into the Clarity value properties
const namespaceProps = cv.data.properties.data;
const priceProps = namespaceProps['price-function'].data;
return {
namespace: cv.data.namespace.buffer.toString(),
status: cv.data.status.data,
launchedAt: namespaceProps['launched-at'].value.value.toNumber(),
lifetime: namespaceProps.lifetime.value.toNumber(),
revealedAt: namespaceProps['revealed-at'].value.toNumber(),
address: addressToString(namespaceProps['namespace-import'].address),
base: priceProps.base.value.toNumber(),
coeff: priceProps.coeff.value.toNumber(),
noVowelDiscount: priceProps['no-vowel-discount'].value.toNumber(),
nonalphaDiscount: priceProps['nonalpha-discount'].value.toNumber(),
buckets: priceProps.buckets.list.map(b => b.value.toNumber()),
};
}

const parsed1 = parseWithManualTypeAssertions();
const parsed2 = parseWithTypeDefinition()
const expected = {
namespace: 'foo',
status: 'ready',
launchedAt: 6,
lifetime: 12,
revealedAt: 3,
address: 'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
base: 1,
coeff: 1,
noVowelDiscount: 1,
nonalphaDiscount: 1,
buckets: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
};
expect(parsed1).toEqual(expected);
expect(parsed2).toEqual(expected);
});

describe('Serialize Then Deserialize', () => {
test('TrueCV', () => {
const t = trueCV();
Expand Down

1 comment on commit da68307

@vercel
Copy link

@vercel vercel bot commented on da68307 Jun 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.