Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add encodeAbiClarityValue method for better encoding #1680

Merged
merged 1 commit into from
May 3, 2024
Merged
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
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);
});
});
Loading