import { expect, test } from 'vitest'; import { bigIntToVmNumber, binToHex, createCompilerBch, createVirtualMachineBch, encodeDataPush, generatePrivateKey, hash256, secp256k1, utf8ToBin, } from '@bitauth/libauth'; import { XOTemplateBaseTypes, XOTemplatePrimitiveTypes } from '@xo-cash/types'; import { isCashAssemblyExpression, extractCashAssemblyEvaluations, extractVariablesFromEvaluations, decodeCompiledCashAssemblyEvaluation, compileCashAssemblyString, generateCashAssemblyBytecode, compileCashAssemblyEvaluations, resolvePrimitiveMethodBytes, } from '../source/cash-assembly/index.ts'; import { CashAssemblyRequiredVariableMissingError, CashAssemblyCompilationFailedError, CashAssemblyVariableTypeMismatchError, CashAssemblyPrimitiveMethodMissingError, CashAssemblyPrimitiveVariableMissingError, CashAssemblyVmNumberDecodeError, CashAssemblyNumberNotSafeIntegerError, } from '../source/cash-assembly/errors.ts'; /** * Tests that isCashAssemblyExpression recognizes a string made up entirely of one evaluation. */ const testIsCashAssemblyExpressionMatchesFullExpression = (): void => { expect(isCashAssemblyExpression('$()')).toBe(true); }; /** * Tests that isCashAssemblyExpression rejects a string that contains an evaluation plus other text. */ const testIsCashAssemblyExpressionRejectsSurroundingText = (): void => { expect(isCashAssemblyExpression('Received $()')).toBe(false); }; /** * Tests that isCashAssemblyExpression rejects plain text with no evaluation at all. */ const testIsCashAssemblyExpressionRejectsPlainText = (): void => { expect(isCashAssemblyExpression('Received funds')).toBe(false); }; /** * Tests that isCashAssemblyExpression rejects an empty evaluation, since it references no variables. */ const testIsCashAssemblyExpressionRejectsEmptyEvaluation = (): void => { expect(isCashAssemblyExpression('$()')).toBe(false); }; /** * Tests that isCashAssemblyExpression rejects non-string input rather than coercing it. */ const testIsCashAssemblyExpressionRejectsNonStringInput = (): void => { // A number can never be a CashAssembly expression, regardless of its value expect(isCashAssemblyExpression(5000)).toBe(false); }; /** * Tests that extractCashAssemblyEvaluations finds a single evaluation embedded in a larger string. */ const testExtractCashAssemblyEvaluationsFindsSingleEvaluation = (): void => { const satoshisDescription = 'Received $() satoshis from sender.'; // Only the evaluation substring is returned, not the surrounding text expect(extractCashAssemblyEvaluations(satoshisDescription)).toStrictEqual([ '$()' ]); }; /** * Tests that extractCashAssemblyEvaluations returns an empty array when the text has no evaluations. */ const testExtractCashAssemblyEvaluationsReturnsEmptyArrayWhenNoneFound = (): void => { expect(extractCashAssemblyEvaluations('Received funds')).toStrictEqual([]); }; /** * Tests that extractVariablesFromEvaluations returns each referenced variable name once, * even when it appears in more than one evaluation. */ const testExtractVariablesFromEvaluationsDeduplicatesAcrossEvaluations = (): void => { const evaluations = [ '$( OP_DIV)', '$( OP_MOD)' ]; // requestedTokenAmount and decimalsFactor each appear in both evaluations but are listed once expect(extractVariablesFromEvaluations(evaluations)).toStrictEqual([ 'requestedTokenAmount', 'decimalsFactor' ]); }; /** * Tests that extractVariablesFromEvaluations excludes numeric and quoted-string literal tokens, * returning only the true variable reference. */ const testExtractVariablesFromEvaluationsExcludesLiteralTokens = (): void => { const evaluation = '$( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <"immutable"> OP_ENDIF)'; // <0x02>, <"minting">, and <"immutable"> are literals, not variables, so only tokenCapability is returned expect(extractVariablesFromEvaluations([ evaluation ])).toStrictEqual([ 'tokenCapability' ]); }; /** * Tests that decodeCompiledCashAssemblyEvaluation converts VM number bytes to their decimal string * when decode mode is bigint. */ const testDecodeCompiledCashAssemblyEvaluationDecodesBigint = (): void => { const compiledResult = bigIntToVmNumber(1234n); expect(decodeCompiledCashAssemblyEvaluation(compiledResult, 'bigint')).toBe('1234'); }; /** * Tests that decodeCompiledCashAssemblyEvaluation throws when bigint mode receives bytes that are not a VM number. */ const testDecodeCompiledCashAssemblyEvaluationThrowsWhenBigintIsNotAVmNumber = (): void => { const decodeNonMinimalZero = (): string => decodeCompiledCashAssemblyEvaluation(new Uint8Array([ 0x00 ]), 'bigint'); const expectedMessage = 'CashAssembly evaluation could not be decoded as a VM number: Failed to decode VM Number: the number is not minimally-encoded.'; expect(decodeNonMinimalZero).toThrow(CashAssemblyVmNumberDecodeError); expect(decodeNonMinimalZero).toThrow(expectedMessage); }; /** * Tests that decodeCompiledCashAssemblyEvaluation converts bytes to a hex string when decode mode is hex. */ const testDecodeCompiledCashAssemblyEvaluationDecodesHex = (): void => { const compiledResult = new Uint8Array([ 0xab, 0xcd ]); expect(decodeCompiledCashAssemblyEvaluation(compiledResult, 'hex')).toBe('abcd'); }; /** * Tests that decodeCompiledCashAssemblyEvaluation reports an empty byte array as 'false' and any * non-empty byte array as 'true' when decode mode is boolean. */ const testDecodeCompiledCashAssemblyEvaluationDecodesBoolean = (): void => { // An empty byte array is falsy on the BCH VM expect(decodeCompiledCashAssemblyEvaluation(new Uint8Array(0), 'boolean')).toBe('false'); // Any non-empty byte array is truthy on the BCH VM expect(decodeCompiledCashAssemblyEvaluation(new Uint8Array([ 1 ]), 'boolean')).toBe('true'); }; /** * Tests that decodeCompiledCashAssemblyEvaluation defaults to utf8 decoding when no mode is given. */ const testDecodeCompiledCashAssemblyEvaluationDefaultsToUtf8 = (): void => { const compiledResult = utf8ToBin('hello'); expect(decodeCompiledCashAssemblyEvaluation(compiledResult)).toBe('hello'); }; /** * Tests that decodeCompiledCashAssemblyEvaluation returns the comma-separated decimal byte values * when decode mode is uint8array. */ const testDecodeCompiledCashAssemblyEvaluationDecodesUint8Array = (): void => { const compiledResult = new Uint8Array([ 1, 2, 255 ]); expect(decodeCompiledCashAssemblyEvaluation(compiledResult, 'uint8array')).toBe('1,2,255'); }; /** * Tests that compileCashAssemblyString compiles a single evaluation and decodes it as a bigint. */ const testCompileCashAssemblyStringDecodesSingleEvaluationAsBigint = (): void => { const compiledSatoshisText = compileCashAssemblyString({ cashAssemblyText: '$()', variables: { requestedSatoshis: 5000n }, evaluationDecodeMode: 'bigint', }); expect(compiledSatoshisText).toBe('5000'); }; /** * Tests that compileCashAssemblyString compiles multiple evaluations embedded in the same string. * */ const testCompileCashAssemblyStringCompilesMultipleEvaluationsInOneString = (): void => { const tokenDescription = 'Transferred $( OP_DIV).$( OP_MOD) tokens.'; expect(extractCashAssemblyEvaluations(tokenDescription)).toStrictEqual([ '$( OP_DIV)', '$( OP_MOD)', ]); const compiledTokenText = compileCashAssemblyString({ cashAssemblyText: tokenDescription, variables: { requestedTokenAmount: 1050n, decimalsFactor: 100n }, evaluationDecodeMode: 'bigint', }); expect(compiledTokenText).toBe('Transferred 10.50 tokens.'); }; /** * Tests that a variable referenced twice in one evaluation uses the same supplied value both times. */ const testCompileCashAssemblyStringReusesTheSameVariableTwiceInOneEvaluation = (): void => { const evaluation = '$( OP_ADD)'; // The name appears twice in the script body but is required only once in the variables map. expect(extractVariablesFromEvaluations([ evaluation ])).toStrictEqual([ 'amount' ]); const compiledText = compileCashAssemblyString({ cashAssemblyText: evaluation, variables: { amount: 21n }, evaluationDecodeMode: 'bigint', }); expect(compiledText).toBe('42'); }; /** * Tests that compileCashAssemblyString respects OP_IF/OP_ELSE branching driven by a variable. */ const testCompileCashAssemblyStringEvaluatesConditionalLiterals = (): void => { const conditionalExpression = '$( <0x02> OP_EQUAL OP_IF <"minting"> OP_ELSE <"immutable"> OP_ENDIF)'; // tokenCapability equal to 2 takes the OP_IF branch const mintingResult = compileCashAssemblyString({ cashAssemblyText: conditionalExpression, variables: { tokenCapability: 2n }, evaluationDecodeMode: 'utf8', }); expect(mintingResult).toBe('minting'); // tokenCapability not equal to 2 takes the OP_ELSE branch const immutableResult = compileCashAssemblyString({ cashAssemblyText: conditionalExpression, variables: { tokenCapability: 0n }, evaluationDecodeMode: 'utf8', }); expect(immutableResult).toBe('immutable'); }; /** * Tests that compileCashAssemblyString defaults to utf8 decoding when evaluationDecodeMode is omitted. */ const testCompileCashAssemblyStringDefaultsToUtf8DecodeMode = (): void => { const compiledLabel = compileCashAssemblyString({ cashAssemblyText: '$(