Skip to content

Commit

Permalink
feat: add encodeAbiClarityValue method for better encoding
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed May 3, 2024
1 parent 6f51d9f commit 2885813
Show file tree
Hide file tree
Showing 2 changed files with 130 additions and 40 deletions.
99 changes: 59 additions & 40 deletions packages/transactions/src/contract-abi.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { cloneDeep } from './utils';
import { hexToBytes, utf8ToBytes } from '@stacks/common';
import {
ClarityType,
ClarityValue,
uintCV,
intCV,
contractPrincipalCV,
standardPrincipalCV,
noneCV,
bufferCV,
bufferCVFromString,
contractPrincipalCV,
falseCV,
trueCV,
ClarityType,
getCVTypeString,
bufferCVFromString,
intCV,
noneCV,
someCV,
standardPrincipalCV,
trueCV,
uintCV,
} from './clarity';
import { ContractCallPayload } from './payload';
import { NotImplementedError } from './errors';
import { stringAsciiCV, stringUtf8CV } from './clarity/types/stringCV';
import { utf8ToBytes } from '@stacks/common';
import { NotImplementedError } from './errors';
import { ContractCallPayload } from './payload';
import { cloneDeep } from './utils';

// From https://github.com/blockstack/stacks-blockchain-sidecar/blob/master/src/event-stream/contract-abi.ts

Expand Down Expand Up @@ -138,58 +139,76 @@ export function getTypeUnion(val: ClarityAbiType): ClarityAbiTypeUnion {
}
}

function encodeClarityValue(type: ClarityAbiType, val: string): ClarityValue;
function encodeClarityValue(type: ClarityAbiTypeUnion, val: string): ClarityValue;
function encodeClarityValue(
input: ClarityAbiTypeUnion | ClarityAbiType,
val: string
/**
* Convert a string to a Clarity value based on the ABI type.
*
* Currently does NOT support some nested Clarity ABI types:
* - ClarityAbiTypeResponse
* - ClarityAbiTypeTuple
* - ClarityAbiTypeList
*/
export function encodeAbiClarityValue(
value: string,
type: ClarityAbiType | ClarityAbiTypeUnion
): ClarityValue {
let union: ClarityAbiTypeUnion;
if ((input as ClarityAbiTypeUnion).id !== undefined) {
union = input as ClarityAbiTypeUnion;
} else {
union = getTypeUnion(input as ClarityAbiType);
}
const union = (type as ClarityAbiTypeUnion).id
? (type as ClarityAbiTypeUnion)
: getTypeUnion(type as ClarityAbiType);
switch (union.id) {
case ClarityAbiTypeId.ClarityAbiTypeUInt128:
return uintCV(val);
return uintCV(value);
case ClarityAbiTypeId.ClarityAbiTypeInt128:
return intCV(val);
return intCV(value);
case ClarityAbiTypeId.ClarityAbiTypeBool:
if (val === 'false' || val === '0') return falseCV();
else if (val === 'true' || val === '1') return trueCV();
else throw new Error(`Unexpected Clarity bool value: ${JSON.stringify(val)}`);
if (value === 'false' || value === '0') return falseCV();
else if (value === 'true' || value === '1') return trueCV();
else throw new Error(`Unexpected Clarity bool value: ${JSON.stringify(value)}`);
case ClarityAbiTypeId.ClarityAbiTypePrincipal:
if (val.includes('.')) {
const [addr, name] = val.split('.');
if (value.includes('.')) {
const [addr, name] = value.split('.');
return contractPrincipalCV(addr, name);
} else {
return standardPrincipalCV(val);
return standardPrincipalCV(value);
}
case ClarityAbiTypeId.ClarityAbiTypeTraitReference:
const [addr, name] = val.split('.');
const [addr, name] = value.split('.');
return contractPrincipalCV(addr, name);
case ClarityAbiTypeId.ClarityAbiTypeNone:
return noneCV();
case ClarityAbiTypeId.ClarityAbiTypeBuffer:
return bufferCV(utf8ToBytes(val));
return bufferCV(hexToBytes(value));
case ClarityAbiTypeId.ClarityAbiTypeStringAscii:
return stringAsciiCV(val);
return stringAsciiCV(value);
case ClarityAbiTypeId.ClarityAbiTypeStringUtf8:
return stringUtf8CV(val);
case ClarityAbiTypeId.ClarityAbiTypeResponse:
throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
return stringUtf8CV(value);
case ClarityAbiTypeId.ClarityAbiTypeOptional:
throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
return someCV(encodeAbiClarityValue(value, union.type.optional));
case ClarityAbiTypeId.ClarityAbiTypeResponse:
case ClarityAbiTypeId.ClarityAbiTypeTuple:
throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
case ClarityAbiTypeId.ClarityAbiTypeList:
throw new NotImplementedError(`Unsupported encoding for Clarity type: ${union.id}`);
default:
throw new Error(`Unexpected Clarity type ID: ${JSON.stringify(union)}`);
}
}
export { encodeClarityValue };

/** @deprecated due to a breaking bug for the buffer encoding case, this was fixed and renamed to {@link clarityAbiStringToCV} */
export function encodeClarityValue(type: ClarityAbiType, value: string): ClarityValue;
export function encodeClarityValue(type: ClarityAbiTypeUnion, value: string): ClarityValue;
export function encodeClarityValue(
type: ClarityAbiTypeUnion | ClarityAbiType,
value: string
): ClarityValue {
const union = (type as ClarityAbiTypeUnion).id
? (type as ClarityAbiTypeUnion)
: getTypeUnion(type as ClarityAbiType);

if (union.id === ClarityAbiTypeId.ClarityAbiTypeBuffer) {
return bufferCV(utf8ToBytes(value)); // legacy behavior
}

return encodeAbiClarityValue(value, union);
}

export function getTypeString(val: ClarityAbiType): string {
if (isClarityAbiPrimitive(val)) {
Expand Down
71 changes: 71 additions & 0 deletions packages/transactions/tests/contract-abi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { utf8ToBytes } from '@stacks/common';
import { Cl, encodeAbiClarityValue, encodeClarityValue } from '../src';

const TEST_CASES = [
{
type: { optional: 'principal' },
value: 'ST000000000000000000002AMW42H',
expected: Cl.some(Cl.address('ST000000000000000000002AMW42H')),
},
{
type: { optional: 'uint128' },
value: '1000',
expected: Cl.some(Cl.uint(1000n)),
},
{
type: 'trait_reference',
value: 'ST000000000000000000002AMW42H.trait',
expected: Cl.address('ST000000000000000000002AMW42H.trait'),
},
{
type: 'bool',
value: 'true',
expected: Cl.bool(true),
},
{
type: 'bool',
value: 'false',
expected: Cl.bool(false),
},
{
type: 'int128',
value: '-42',
expected: Cl.int(-42n),
},
{
type: 'uint128',
value: '17',
expected: Cl.uint(17n),
},
{
type: { buffer: { length: 10 } },
value: 'beef',
expected: Cl.buffer(utf8ToBytes('beef')), // legacy behavior
},
{
type: 'principal',
value: 'ST1J28031BYDX19TYXSNDG9Q4HDB2TBDAM921Y7MS',
expected: Cl.principal('ST1J28031BYDX19TYXSNDG9Q4HDB2TBDAM921Y7MS'),
},
{
type: 'principal',
value: 'ST1J28031BYDX19TYXSNDG9Q4HDB2TBDAM921Y7MS.contract-name',
expected: Cl.contractPrincipal('ST1J28031BYDX19TYXSNDG9Q4HDB2TBDAM921Y7MS', 'contract-name'),
},
] as const;

test.each(TEST_CASES)(encodeClarityValue.name, ({ type, value, expected }) => {
const result = encodeClarityValue(type, value);
expect(result).toEqual(expected);
});

test(encodeAbiClarityValue.name, () => {
// buffer is expected to be hex
const result = encodeAbiClarityValue('beef', { buffer: { length: 10 } });
expect(result).toEqual(Cl.bufferFromHex('beef'));

TEST_CASES.filter((tc: any) => !tc.type.buffer).forEach(({ type, value, expected }) => {
const result = encodeAbiClarityValue(value, type);
expect(result).toEqual(expected);
});
});

0 comments on commit 2885813

Please sign in to comment.