diff --git a/packages/transactions/src/contract-abi.ts b/packages/transactions/src/contract-abi.ts index 3bc014277..9fc2a6dd9 100644 --- a/packages/transactions/src/contract-abi.ts +++ b/packages/transactions/src/contract-abi.ts @@ -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 @@ -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)) { diff --git a/packages/transactions/tests/contract-abi.test.ts b/packages/transactions/tests/contract-abi.test.ts new file mode 100644 index 000000000..859bf4e20 --- /dev/null +++ b/packages/transactions/tests/contract-abi.test.ts @@ -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); + }); +});