diff --git a/cspell.json b/cspell.json index 83d1e9a1e7..50001d2ecd 100644 --- a/cspell.json +++ b/cspell.json @@ -53,6 +53,7 @@ "src/test/exit-codes/contracts/compute-phase-errors.tact", "src/test/e2e-emulated/map-property-tests/map-properties-key-value-types.ts", "src/test/e2e-emulated/map-property-tests/build", + "src/test/e2e-emulated/map-property-tests/fuzzing/minimal-fc-stdlib/stdlib.fc", "/docs", "src/benchmarks/contracts/func/notcoin/stdlib-custom.fc", "src/benchmarks/contracts/func/notcoin/gas.fc" diff --git a/package.json b/package.json index 30e6502430..95ce128a54 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "gen:contracts:test:map": "ts-node ./src/test/e2e-emulated/map-property-tests/generate.ts", "gen:contracts:all": "yarn gen:contracts:examples && yarn gen:contracts:test && yarn gen:contracts:benchmarks && yarn gen:contracts:test:map", "gen": "yarn gen:grammar && yarn gen:stdlib && yarn gen:func-js && yarn gen:contracts:all", + "fuzz:map:functions": "ts-node ./src/test/e2e-emulated/map-property-tests/fuzzing/fuzz-map-functions.ts", "clean": "rm -fr dist", "cleanall": "rm -fr dist node_modules", "copy:stdlib": "ts-node src/stdlib/copy.build.ts", diff --git a/src/test/e2e-emulated/map-property-tests/fuzzing/fuzz-map-functions.ts b/src/test/e2e-emulated/map-property-tests/fuzzing/fuzz-map-functions.ts new file mode 100644 index 0000000000..d19c291e7b --- /dev/null +++ b/src/test/e2e-emulated/map-property-tests/fuzzing/fuzz-map-functions.ts @@ -0,0 +1,915 @@ +import { Blockchain } from "@ton/sandbox"; +import type { SandboxContract, TreasuryContract } from "@ton/sandbox"; +import type * as Ast from "@/ast/ast"; +import { getAstFactory, type FactoryAst } from "@/ast/ast-helpers"; +import { getMakeAst } from "@/ast/generated/make-factory"; +import { + buildModule, + compareDicts, + createDict, + filterGlobalDeclarations, + generateKeyValuePairs, + getContractStateInit, + getExists, + getGetValue, + getKeyTypeHandler, + getValueTypeHandler, + getWholeMap, + keyTypes, + loadCustomStdlibFc, + parseStandardLibrary, + ProxyContract, + valueTypes, +} from "@/test/e2e-emulated/map-property-tests/fuzzing/util"; +import type { + keyType, + KeyTypeHandler, + keyValueTypes, + valueType, + ValueTypeHandler, +} from "@/test/e2e-emulated/map-property-tests/fuzzing/util"; +import { expect } from "expect"; +import { beginCell, Cell, toNano } from "@ton/core"; +import type { DictionaryKeyTypes } from "@ton/core"; +import { findTransaction } from "@ton/test-utils"; +import * as fc from "fast-check"; + +function splitTypeAndSerialization( + type: keyValueTypes, +): [string, string | undefined] { + const components = type.split(" as "); + const typePart = components[0]; + if (typeof typePart === "undefined") { + throw new Error(`Expected a type in ${type}`); + } + return [typePart, components[1]]; +} + +type ContractWrapper = { + moduleItems: Ast.ModuleItem[]; + keyType: keyType; + valueType: valueType; + contractName: string; + messageOpCodes: Map; +}; + +type CompiledContractWrapper = { + boc: Buffer; + keyType: keyType; + valueType: valueType; + contractName: string; + messageOpCodes: Map; +}; + +type ModuleWrapper = { + module: Ast.Module; + contractNames: Set; +}; + +function getModuleItemsFactory(astF: FactoryAst) { + let opCodeCounter = 50n; + const mF = getMakeAst(astF); + const mapFieldName = "mapUnderTest"; + + function getFreshOpCode(): bigint { + return opCodeCounter++; + } + + function createContract( + keyT: keyType, + valT: valueType, + ): { + items: Ast.ModuleItem[]; + contractName: string; + messageOpCodes: Map; + } { + const moduleItems: Ast.ModuleItem[] = []; + const messageOpCodes: Map = new Map(); + + const namePostfix = + keyT.replaceAll(" ", "_") + valT.replaceAll(" ", "_"); + const contractName = "C_" + namePostfix; + + const [kT, kS] = splitTypeAndSerialization(keyT); + const [vT, vS] = splitTypeAndSerialization(valT); + const kTNode = mF.makeDummyTypeId(kT); + const vTNode = mF.makeDummyTypeId(vT); + const kSNode = + typeof kS !== "undefined" ? mF.makeDummyId(kS) : undefined; + const vSNode = + typeof vS !== "undefined" ? mF.makeDummyId(vS) : undefined; + + // Message declaration for SetKeyValue + { + const keyField = mF.makeDummyFieldDecl( + mF.makeDummyId("key"), + kTNode, + undefined, + kSNode, + ); + const valueField = mF.makeDummyFieldDecl( + mF.makeDummyId("value"), + vTNode, + undefined, + vSNode, + ); + + const opCode = getFreshOpCode(); + moduleItems.push( + mF.makeDummyMessageDecl( + mF.makeDummyId("SetKeyValue_" + namePostfix), + mF.makeDummyNumber(10, opCode), + [keyField, valueField], + ), + ); + + messageOpCodes.set("SetKeyValue", opCode); + } + + // Message declaration for DeleteKey + { + const keyField = mF.makeDummyFieldDecl( + mF.makeDummyId("key"), + kTNode, + undefined, + kSNode, + ); + + const opCode = getFreshOpCode(); + moduleItems.push( + mF.makeDummyMessageDecl( + mF.makeDummyId("DeleteKey_" + namePostfix), + mF.makeDummyNumber(10, opCode), + [keyField], + ), + ); + + messageOpCodes.set("DeleteKey", opCode); + } + + // Contract declaration + { + const decls: Ast.ContractDeclaration[] = []; + + // Map field + decls.push( + mF.makeDummyFieldDecl( + mF.makeDummyId(mapFieldName), + mF.makeDummyMapType(kTNode, kSNode, vTNode, vSNode), + undefined, + undefined, + ), + ); + + // init function + { + const body = mF.makeDummyStatementAssign( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + mF.makeDummyStaticCall(mF.makeDummyId("emptyMap"), []), + ); + decls.push(mF.makeDummyContractInit([], [body])); + } + + // wholeMap getter + { + const body = mF.makeDummyStatementReturn( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + ); + decls.push( + mF.makeDummyFunctionDef( + [mF.makeDummyFunctionAttributeGet(undefined)], + mF.makeDummyId("wholeMap"), + mF.makeDummyMapType(kTNode, kSNode, vTNode, vSNode), + [], + [body], + ), + ); + } + + // getValue getter + { + const body = mF.makeDummyStatementReturn( + mF.makeDummyMethodCall( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + mF.makeDummyId("get"), + [mF.makeDummyId("key")], + ), + ); + decls.push( + mF.makeDummyFunctionDef( + [mF.makeDummyFunctionAttributeGet(undefined)], + mF.makeDummyId("getValue"), + mF.makeDummyOptionalType(vTNode), + [ + mF.makeDummyTypedParameter( + mF.makeDummyId("key"), + kTNode, + ), + ], + [body], + ), + ); + } + + // exists getter + { + const body = mF.makeDummyStatementReturn( + mF.makeDummyMethodCall( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + mF.makeDummyId("exists"), + [mF.makeDummyId("key")], + ), + ); + decls.push( + mF.makeDummyFunctionDef( + [mF.makeDummyFunctionAttributeGet(undefined)], + mF.makeDummyId("exists"), + mF.makeDummyTypeId("Bool"), + [ + mF.makeDummyTypedParameter( + mF.makeDummyId("key"), + kTNode, + ), + ], + [body], + ), + ); + } + + // Empty receiver + decls.push( + mF.makeDummyReceiver( + mF.makeDummyReceiverInternal(mF.makeReceiverFallback()), + [], + ), + ); + + // ClearRequest receiver + { + const body = mF.makeDummyStatementAssign( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + mF.makeDummyStaticCall(mF.makeDummyId("emptyMap"), []), + ); + decls.push( + mF.makeDummyReceiver( + mF.makeDummyReceiverInternal( + mF.makeReceiverSimple( + mF.makeDummyTypedParameter( + mF.makeDummyId("_"), + mF.makeDummyTypeId("ClearRequest"), + ), + ), + ), + [body], + ), + ); + } + + // SetKeyValue receiver + { + const body = mF.makeDummyStatementExpression( + mF.makeDummyMethodCall( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + mF.makeDummyId("set"), + [ + mF.makeDummyFieldAccess( + mF.makeDummyId("data"), + mF.makeDummyId("key"), + ), + mF.makeDummyFieldAccess( + mF.makeDummyId("data"), + mF.makeDummyId("value"), + ), + ], + ), + ); + decls.push( + mF.makeDummyReceiver( + mF.makeDummyReceiverInternal( + mF.makeReceiverSimple( + mF.makeDummyTypedParameter( + mF.makeDummyId("data"), + mF.makeDummyTypeId( + "SetKeyValue_" + namePostfix, + ), + ), + ), + ), + [body], + ), + ); + } + + // DeleteKey receiver + { + const body = mF.makeDummyStatementExpression( + mF.makeDummyMethodCall( + mF.makeDummyFieldAccess( + mF.makeDummyId("self"), + mF.makeDummyId(mapFieldName), + ), + mF.makeDummyId("del"), + [ + mF.makeDummyFieldAccess( + mF.makeDummyId("data"), + mF.makeDummyId("key"), + ), + ], + ), + ); + decls.push( + mF.makeDummyReceiver( + mF.makeDummyReceiverInternal( + mF.makeReceiverSimple( + mF.makeDummyTypedParameter( + mF.makeDummyId("data"), + mF.makeDummyTypeId( + "DeleteKey_" + namePostfix, + ), + ), + ), + ), + [body], + ), + ); + } + + moduleItems.push( + mF.makeDummyContract( + mF.makeDummyId(contractName), + [], + [], + undefined, + decls, + ), + ); + } + + return { items: moduleItems, contractName, messageOpCodes }; + } + + function createCommonModuleItems(): { + items: Ast.ModuleItem[]; + messageOpCodes: Map; + } { + const moduleItems: Ast.ModuleItem[] = []; + const messageOpCodes: Map = new Map(); + + // Message declaration for ClearRequest + const opCode = getFreshOpCode(); + moduleItems.push( + mF.makeDummyMessageDecl( + mF.makeDummyId("ClearRequest"), + mF.makeDummyNumber(10, opCode), + [], + ), + ); + messageOpCodes.set("ClearRequest", opCode); + + return { items: moduleItems, messageOpCodes }; + } + + function createCompilationModules( + wrappedContracts: ContractWrapper[], + commonItems: Ast.ModuleItem[], + compilationBatchSize: number, + ): ModuleWrapper[] { + const modules: ModuleWrapper[] = []; + + let moduleItemAccumulator: Ast.ModuleItem[] = []; + let contractNamesAccumulator: Set = new Set(); + + let counter = 0; + + for (const wrappedContract of wrappedContracts) { + moduleItemAccumulator.push(...wrappedContract.moduleItems); + const contractName = wrappedContract.contractName; + contractNamesAccumulator.add(contractName); + counter++; + + if (counter >= compilationBatchSize) { + modules.push({ + module: mF.makeModule( + [], + [...moduleItemAccumulator, ...commonItems], + ), + contractNames: contractNamesAccumulator, + }); + counter = 0; + moduleItemAccumulator = []; + contractNamesAccumulator = new Set(); + } + } + + // if there are elements in the accumulators, it means that the last group + // did not fill completely, we need to create a module with the leftovers + if (moduleItemAccumulator.length > 0) { + modules.push({ + module: mF.makeModule( + [], + [...moduleItemAccumulator, ...commonItems], + ), + contractNames: contractNamesAccumulator, + }); + } + + return modules; + } + + return { + createContract, + createCommonModuleItems, + createCompilationModules, + }; +} + +async function main() { + const batchSize = 15; + const contracts: ContractWrapper[] = []; + const astF = getAstFactory(); + const mF = getModuleItemsFactory(astF); + + // Create common module items + const commonItems = mF.createCommonModuleItems(); + + // Generate specific contract for each type combination + for (const keyType of keyTypes) { + for (const valueType of valueTypes) { + const contractData = mF.createContract(keyType, valueType); + contracts.push({ + moduleItems: contractData.items, + keyType, + valueType, + contractName: contractData.contractName, + messageOpCodes: contractData.messageOpCodes, + }); + } + } + + // Now group the contracts into batches. Each batch will be a single module + // for compilation. Attach the common module items into each batch. + const modulesForCompilation = mF.createCompilationModules( + contracts, + commonItems.items, + batchSize, + ); + + console.log( + `There are ${contracts.length} contracts, grouped into ${modulesForCompilation.length} compilation batches`, + ); + + // Parse the stdlib and filter it with the minimal definitions we need + const stdlibModule = filterGlobalDeclarations( + parseStandardLibrary(astF), + getMakeAst(astF), + new Set([ + "Int", + "Bool", + "Address", + "Cell", + "Context", + "Slice", + //"Builder", + //"String", + "StateInit", + "SendParameters", + "BaseTrait", + "SendDefaultMode", + "SendRemainingValue", + "SendIgnoreErrors", + "SendRemainingBalance", + "ReserveExact", + "sender", + "context", + "myBalance", + "nativeReserve", + //"contractAddress", + //"contractAddressExt", + //"storeUint", + //"storeInt", + //"contractHash", + //"newAddress", + //"beginCell", + //"endCell", + "send", + //"asSlice", + //"asAddressUnsafe", + //"beginParse", + ]), + ); + + const customStdlibFc = loadCustomStdlibFc(); + + // Create the custom stdlib, with the loaded custom FunC stdlib + const customStdlib = { + modules: [stdlibModule], + stdlib_fc: customStdlibFc.stdlib_fc, + stdlib_ex_fc: customStdlibFc.stdlib_ex_fc, + }; + + const contractCodesAccumulator: Map = new Map(); + + for (const moduleWrap of modulesForCompilation) { + console.log( + `Compiling batch with contract names [${[...moduleWrap.contractNames].join(",")}]...`, + ); + + const contractCodes = await buildModule( + astF, + moduleWrap.module, + customStdlib, + true, + ); + + for (const [key, value] of contractCodes) { + contractCodesAccumulator.set(key, value); + } + } + + const finalCompiledContracts = contracts.map((contract) => { + const boc = contractCodesAccumulator.get(contract.contractName); + if (typeof boc === "undefined") { + throw new Error( + `Expected contract ${contract.contractName} to have a compiled code`, + ); + } + // Attach the message opcodes created by the common items + const finalMessageOpcodes = new Map(commonItems.messageOpCodes); + for (const [key, val] of contract.messageOpCodes) { + finalMessageOpcodes.set(key, val); + } + return { + boc, + contractName: contract.contractName, + keyType: contract.keyType, + valueType: contract.valueType, + messageOpCodes: finalMessageOpcodes, + }; + }); + + await testCompiledContracts(finalCompiledContracts); +} + +async function testCompiledContracts(contracts: CompiledContractWrapper[]) { + const blockchain = await Blockchain.create(); + const treasury = await blockchain.treasury("treasury"); + + for (const contract of contracts) { + await testCompiledContract(contract, blockchain, treasury); + } +} + +async function testCompiledContract( + contract: CompiledContractWrapper, + blockchain: Blockchain, + treasury: SandboxContract, +) { + console.log(`Testing contract ${contract.contractName}...`); + + // Some utility functions + async function sendMessage(message: Cell) { + return await contractToTest.send( + treasury.getSender(), + { value: toNano("1") }, + message, + ); + } + + function obtainOpCode(messageName: string): bigint { + const opCode = contract.messageOpCodes.get(messageName); + if (typeof opCode === "undefined") { + throw new Error( + `${messageName} does not have a registered op code`, + ); + } + return opCode; + } + + function prepareSetKeyValueMessage( + key: K, + value: V, + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, + ): Cell { + const builder = beginCell(); + builder.storeUint(obtainOpCode("SetKeyValue"), 32); + kHandler.storeInCellBuilder(key, builder); + vHandler.storeInCellBuilder(value, builder); + return builder.endCell(); + } + + function prepareDeleteKeyMessage( + key: K, + kHandler: KeyTypeHandler, + ): Cell { + const builder = beginCell(); + builder.storeUint(obtainOpCode("DeleteKey"), 32); + kHandler.storeInCellBuilder(key, builder); + return builder.endCell(); + } + + function prepareClearRequestMessage(): Cell { + const builder = beginCell(); + builder.storeUint(obtainOpCode("ClearRequest"), 32); + return builder.endCell(); + } + + async function initializeContract( + keyValuePairs: [K, V][], + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, + ) { + for (const [key, value] of keyValuePairs) { + const messageCell = prepareSetKeyValueMessage( + key, + value, + kHandler, + vHandler, + ); + await sendMessage(messageCell); + } + } + + function withClear(property: fc.IAsyncPropertyWithHooks) { + return property.afterEach(async () => { + //Clear map data for next tests + await sendMessage(prepareClearRequestMessage()); + }); + } + + const contractToTest = blockchain.openContract( + new ProxyContract(getContractStateInit(contract.boc)), + ); + + // Deploy the contract with an empty message + const { transactions } = await sendMessage(new Cell()); + expect( + findTransaction(transactions, { + from: treasury.address, + to: contractToTest.address, + oldStatus: "uninitialized", + endStatus: "active", + exitCode: 0, + actionResultCode: 0, + }), + ).toBeDefined(); + + // These handlers are "sealed" because their types are existential types. + const sealedKHandler = getKeyTypeHandler(contract.keyType); + const sealedVHandler = getValueTypeHandler(contract.valueType); + + // To unpack a variable of existential type, we simply apply it on a function + // where the argument to the function is the unpacked value. + await sealedKHandler(async (kHandler) => { + await sealedVHandler(async (vHandler) => { + // Check that the contract has an initial empty map + expect( + (await getWholeMap(contractToTest, kHandler, vHandler)).size, + ).toBe(0); + + console.log("Checking 'set' function..."); + await checkSetFunction(kHandler, vHandler); + + console.log("Checking 'get' function..."); + await checkGetFunction(kHandler, vHandler); + + console.log("Checking 'del' function..."); + await checkDelFunction(kHandler, vHandler); + + console.log("Checking 'exists' function..."); + await checkExistsFunction(kHandler, vHandler); + }); + }); + + // Test: adds new element in tact's 'map' exactly like in ton's 'Dictionary' + async function checkSetFunction( + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, + ) { + await fc.assert( + withClear( + fc.asyncProperty( + generateKeyValuePairs( + kHandler.getGenerator, + vHandler.getGenerator, + ), + kHandler.getGenerator(), + vHandler.getGenerator(), + async (keyValuePairs, testKey, testValue) => { + await initializeContract( + keyValuePairs, + kHandler, + vHandler, + ); + + // This is an external dict that emulates what the contract is doing + const externalDict = createDict( + keyValuePairs, + kHandler, + vHandler, + ); + + const initialMap = await getWholeMap( + contractToTest, + kHandler, + vHandler, + ); + + initialMap.set(testKey, testValue); + externalDict.set(testKey, testValue); + + await sendMessage( + prepareSetKeyValueMessage( + testKey, + testValue, + kHandler, + vHandler, + ), + ); + + const finalMap = await getWholeMap( + contractToTest, + kHandler, + vHandler, + ); + + return ( + compareDicts(initialMap, finalMap, vHandler) && + compareDicts(finalMap, externalDict, vHandler) + ); + }, + ), + ), + ); + } + + // Test: Gets element from tact 'map' exactly like from ton's 'Dictionary'", + async function checkGetFunction( + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, + ) { + await fc.assert( + withClear( + fc.asyncProperty( + generateKeyValuePairs( + kHandler.getGenerator, + vHandler.getGenerator, + ), + kHandler.getGenerator(), + async (keyValuePairs, testKey) => { + await initializeContract( + keyValuePairs, + kHandler, + vHandler, + ); + + // This is an external dict that emulates what the contract is doing + const externalDict = createDict( + keyValuePairs, + kHandler, + vHandler, + ); + + const map = await getWholeMap( + contractToTest, + kHandler, + vHandler, + ); + + const val = await getGetValue( + contractToTest, + testKey, + kHandler, + vHandler, + ); + + const mapVal = map.get(testKey); + const externalVal = externalDict.get(testKey); + + expect(vHandler.equals(val, mapVal)).toBe(true); + + expect(vHandler.equals(val, externalVal)).toBe(true); + }, + ), + ), + ); + } + + // Test: Deletes key from tact's 'map' exactly like from ton's 'Dictionary' + async function checkDelFunction( + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, + ) { + await fc.assert( + withClear( + fc.asyncProperty( + generateKeyValuePairs( + kHandler.getGenerator, + vHandler.getGenerator, + ), + kHandler.getGenerator(), + async (keyValuePairs, testKey) => { + await initializeContract( + keyValuePairs, + kHandler, + vHandler, + ); + + // This is an external dict that emulates what the contract is doing + const externalDict = createDict( + keyValuePairs, + kHandler, + vHandler, + ); + + const initialMap = await getWholeMap( + contractToTest, + kHandler, + vHandler, + ); + + initialMap.delete(testKey); + externalDict.delete(testKey); + + await sendMessage( + prepareDeleteKeyMessage(testKey, kHandler), + ); + + const finalMap = await getWholeMap( + contractToTest, + kHandler, + vHandler, + ); + + return ( + compareDicts(initialMap, finalMap, vHandler) && + compareDicts(finalMap, externalDict, vHandler) + ); + }, + ), + ), + ); + } + + // Test: Check if element exists in tact's 'map' exactly like in ton's 'Dictionary' + async function checkExistsFunction( + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, + ) { + await fc.assert( + withClear( + fc.asyncProperty( + generateKeyValuePairs( + kHandler.getGenerator, + vHandler.getGenerator, + ), + kHandler.getGenerator(), + async (keyValuePairs, testKey) => { + await initializeContract( + keyValuePairs, + kHandler, + vHandler, + ); + + // This is an external dict that emulates what the contract is doing + const externalDict = createDict( + keyValuePairs, + kHandler, + vHandler, + ); + + const map = await getWholeMap( + contractToTest, + kHandler, + vHandler, + ); + + expect( + await getExists(contractToTest, testKey, kHandler), + ).toBe(map.has(testKey) && externalDict.has(testKey)); + }, + ), + ), + ); + } +} + +void main(); diff --git a/src/test/e2e-emulated/map-property-tests/fuzzing/minimal-fc-stdlib/stdlib.fc b/src/test/e2e-emulated/map-property-tests/fuzzing/minimal-fc-stdlib/stdlib.fc new file mode 100644 index 0000000000..95acd5b2c5 --- /dev/null +++ b/src/test/e2e-emulated/map-property-tests/fuzzing/minimal-fc-stdlib/stdlib.fc @@ -0,0 +1,61 @@ +cell get_data() asm "c4 PUSH"; + +slice begin_parse(cell c) asm "CTOS"; + +slice skip_bits(slice s, int len) asm "SDSKIPFIRST"; +(slice, ()) ~skip_bits(slice s, int len) asm "SDSKIPFIRST"; + +int slice_bits(slice s) asm "SBITS"; + +(slice, slice) load_msg_addr(slice s) asm(-> 1 0) "LDMSGADDR"; + +forall X -> X null() asm "PUSHNULL"; + +(slice, cell) load_ref(slice s) asm(-> 1 0) "LDREF"; + +builder begin_cell() asm "NEWC"; + +() set_data(cell c) impure asm "c4 POP"; + +cell end_cell(builder b) asm "ENDC"; + +builder store_dict(builder b, cell c) asm(c b) "STDICT"; + +(slice, cell) load_dict(slice s) asm(-> 1 0) "LDDICT"; + +(cell, int) idict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIDEL"; +(slice, int) idict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGET" "NULLSWAPIFNOT"; + +cell idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; +(cell, ()) ~idict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTISET"; + +cell idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; +(cell, ()) ~idict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTISETREF"; + +(cell, int) idict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTIGETREF" "NULLSWAPIFNOT"; + +cell idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +(cell, ()) ~idict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTISETB"; +cell dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; +(cell, ()) ~dict_set_builder(cell dict, int key_len, slice index, builder value) asm(value index dict key_len) "DICTSETB"; + +(slice, int) load_coins(slice s) asm(-> 1 0) "LDVARUINT16"; +builder store_coins(builder b, int x) asm "STVARUINT16"; + +(slice, int) load_varuint16(slice s) asm(-> 1 0) "LDVARUINT16"; + +builder store_slice(builder b, slice s) asm "STSLICER"; + +(slice, int) udict_get?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGET" "NULLSWAPIFNOT"; +(cell, int) udict_delete?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUDEL"; + +cell udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; +(cell, ()) ~udict_set_builder(cell dict, int key_len, int index, builder value) asm(value index dict key_len) "DICTUSETB"; + +cell udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; +(cell, ()) ~udict_set(cell dict, int key_len, int index, slice value) asm(value index dict key_len) "DICTUSET"; + +(cell, int) udict_get_ref?(cell dict, int key_len, int index) asm(index dict key_len) "DICTUGETREF" "NULLSWAPIFNOT"; + +cell udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; +(cell, ()) ~udict_set_ref(cell dict, int key_len, int index, cell value) asm(value index dict key_len) "DICTUSETREF"; \ No newline at end of file diff --git a/src/test/e2e-emulated/map-property-tests/fuzzing/util.ts b/src/test/e2e-emulated/map-property-tests/fuzzing/util.ts new file mode 100644 index 0000000000..5c3c0d5302 --- /dev/null +++ b/src/test/e2e-emulated/map-property-tests/fuzzing/util.ts @@ -0,0 +1,936 @@ +import type * as Ast from "@/ast/ast"; +import { idText, type FactoryAst } from "@/ast/ast-helpers"; +import { featureEnable } from "@/config/features"; +import { CompilerContext } from "@/context/context"; +import { Logger } from "@/context/logger"; +import { funcCompile } from "@/func/funcCompile"; +import { getParser } from "@/grammar"; +import { compile } from "@/pipeline/compile"; +import { precompile } from "@/pipeline/precompile"; +import { topSortContracts } from "@/pipeline/utils"; +import files from "@/stdlib/stdlib"; +import * as fs from "fs"; +import { posixNormalize } from "@/utils/filePath"; +import { createVirtualFileSystem } from "@/vfs/createVirtualFileSystem"; +import type { + Address, + Builder, + Contract, + ContractProvider, + DictionaryKey, + DictionaryKeyTypes, + DictionaryValue, + Sender, + StateInit, + TupleItem, + TupleReader, +} from "@ton/core"; +import { Cell, Dictionary } from "@ton/core"; +import { beginCell, contractAddress, TupleBuilder } from "@ton/core"; +import { resolveImports } from "@/imports/resolveImports"; +import { getRawAST, openContext, parseModules } from "@/context/store"; +import path from "path"; +import { getAllTypes } from "@/types/resolveDescriptors"; +import type { MakeAstFactory } from "@/ast/generated/make-factory"; +import * as fc from "fast-check"; +import { TreasuryContract } from "@ton/sandbox"; +import type { SandboxContract } from "@ton/sandbox"; +import { sha256_sync } from "@ton/crypto"; + +export const keyTypes = [ + "Int", + "Address", + "Int as uint8", + "Int as uint16", + "Int as uint32", + "Int as uint64", + "Int as uint128", + "Int as uint256", + "Int as int8", + "Int as int16", + "Int as int32", + "Int as int64", + "Int as int128", + "Int as int256", + "Int as int257", +] as const; + +export const valueTypes = [ + "Int", + "Bool", + "Address", + "Cell", + "Int as uint8", + "Int as uint16", + "Int as uint32", + "Int as uint64", + "Int as uint128", + "Int as uint256", + "Int as int8", + "Int as int16", + "Int as int32", + "Int as int64", + "Int as int128", + "Int as int256", + "Int as int257", + "Int as coins", +] as const; + +export type keyType = (typeof keyTypes)[number]; +export type valueType = (typeof valueTypes)[number]; +export type keyValueTypes = keyType | valueType; + +export function parseStandardLibrary(astF: FactoryAst): CompilerContext { + let ctx = new CompilerContext(); + const parser = getParser(astF); + const fileSystem = { + [`contracts/empty.tact`]: "", + }; + const project = createVirtualFileSystem("/", fileSystem, false); + const stdlib = createVirtualFileSystem("@stdlib", files); + + const imported = resolveImports({ + entrypoint: "contracts/empty.tact", + project, + stdlib, + parser, + }); + + // Add information about all the source code entries to the context + ctx = openContext( + ctx, + imported.tact, + imported.func, + parseModules(imported.tact, getParser(astF)), + ); + + return ctx; +} + +export function filterGlobalDeclarations( + ctx: CompilerContext, + mF: MakeAstFactory, + names: Set, +): Ast.Module { + const result: Ast.ModuleItem[] = []; + + const rawAst = getRawAST(ctx); + + for (const c of rawAst.constants) { + if (names.has(idText(c.name))) { + result.push(c); + } + } + + for (const f of rawAst.functions) { + if (names.has(idText(f.name))) { + result.push(f); + } + } + + for (const t of rawAst.types) { + if (names.has(idText(t.name))) { + result.push(t); + } + } + + return mF.makeModule([], result); +} + +export function loadCustomStdlibFc(): { + stdlib_fc: string; + stdlib_ex_fc: string; +} { + const stdlib_fc = fs + .readFileSync(path.join(__dirname, "minimal-fc-stdlib", "stdlib.fc")) + .toString("base64"); + return { + stdlib_fc: stdlib_fc, + stdlib_ex_fc: "", + }; +} + +type CustomStdlib = { + // Parsed modules of Tact stdlib + modules: Ast.Module[]; + // Contents of the stdlib.fc file + stdlib_fc: string; + // Contents of the stdlib_ex.fc file + stdlib_ex_fc: string; +}; + +// If flag useCustomStdlib is false, it will parse the entire stdlib. Otherwise, +// it will use the provided data in CustomStdlib. +export async function buildModule( + astF: FactoryAst, + module: Ast.Module, + customStdlib: CustomStdlib, + useCustomStdlib: boolean, +): Promise> { + let ctx = new CompilerContext(); + const parser = getParser(astF); + // We need an entrypoint for precompile, even if it is empty + const fileSystem = { + [`contracts/empty.tact`]: "", + }; + const minimalStdlib = { + // Needed by precompile, but we set its contents to be empty + ["std/stdlib.tact"]: "", + // These two func files are needed during tvm compilation + ["std/stdlib_ex.fc"]: customStdlib.stdlib_ex_fc, + ["std/stdlib.fc"]: customStdlib.stdlib_fc, + }; + + const project = createVirtualFileSystem("/", fileSystem, false); + // If the provided stdlib modules are empty, prepare the full stdlib. + // Otherwise, just include the minimal stdlib + const stdlib = useCustomStdlib + ? createVirtualFileSystem("@stdlib", minimalStdlib) + : createVirtualFileSystem("@stdlib", files); + + const config = { + name: "test", + path: "contracts/empty.tact", + output: ".", + }; + const contractCodes = new Map(); + + if (useCustomStdlib) { + ctx = precompile(ctx, project, stdlib, config.path, parser, astF, [ + module, + ...customStdlib.modules, + ]); + } else { + ctx = precompile(ctx, project, stdlib, config.path, parser, astF, [ + module, + ]); + } + + const built: Record< + string, + | { + codeBoc: Buffer; + abi: string; + } + | undefined + > = {}; + + const allContracts = getAllTypes(ctx).filter((v) => v.kind === "contract"); + + // Sort contracts in topological order + // If a cycle is found, return undefined + const sortedContracts = topSortContracts(allContracts); + if (sortedContracts !== undefined) { + ctx = featureEnable(ctx, "optimizedChildCode"); + } + for (const contract of sortedContracts ?? allContracts) { + const contractName = contract.name; + + // Compiling contract to func + const res = await compile( + ctx, + contractName, + `${config.name}_${contractName}`, + built, + ); + const codeFc = res.output.files.map((v) => ({ + path: posixNormalize(project.resolve(config.output, v.name)), + content: v.code, + })); + const codeEntrypoint = res.output.entrypoint; + + // Compiling contract to TVM + const stdlibPath = stdlib.resolve("std/stdlib.fc"); + const stdlibCode = stdlib.readFile(stdlibPath).toString(); + const stdlibExPath = stdlib.resolve("std/stdlib_ex.fc"); + const stdlibExCode = stdlib.readFile(stdlibExPath).toString(); + + const c = await funcCompile({ + entries: [ + stdlibPath, + stdlibExPath, + posixNormalize(project.resolve(config.output, codeEntrypoint)), + ], + sources: [ + { + path: stdlibPath, + content: stdlibCode, + }, + { + path: stdlibExPath, + content: stdlibExCode, + }, + ...codeFc, + ], + logger: new Logger(), + }); + + if (!c.ok) { + throw new Error(c.log); + } + + // Add to built map + built[contractName] = { + codeBoc: c.output, + abi: "", + }; + + contractCodes.set(contractName, c.output); + } + + return contractCodes; +} + +export class ProxyContract implements Contract { + address: Address; + init: StateInit; + + constructor(stateInit: StateInit) { + this.address = contractAddress(0, stateInit); + this.init = stateInit; + } + + async send( + provider: ContractProvider, + via: Sender, + args: { value: bigint; bounce?: boolean | null | undefined }, + body: Cell, + ) { + await provider.internal(via, { ...args, body: body }); + } + + async getWholeMap(provider: ContractProvider, params: TupleItem[]) { + return (await provider.get("wholeMap", params)).stack; + } + + async getGetValue(provider: ContractProvider, params: TupleItem[]) { + return (await provider.get("getValue", params)).stack; + } + + async getExists(provider: ContractProvider, params: TupleItem[]) { + return (await provider.get("exists", params)).stack; + } +} + +// We need to do this indirection (i.e., not placing the entire code of this +// function inside the "getWholeMap" in the ProxyContract class, and similarly for the rest +// of the contract getters) because SandboxContract for some reason messes up the generics. +export async function getWholeMap( + contract: SandboxContract, + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, +) { + const builder = new TupleBuilder(); + const reader = await contract.getWholeMap(builder.build()); + return Dictionary.loadDirect( + kHandler.getDictionaryKeyType(), + vHandler.getDictionaryValueType(), + reader.readCellOpt(), + ); +} + +export async function getGetValue( + contract: SandboxContract, + key: K, + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, +) { + const builder = new TupleBuilder(); + kHandler.storeInTupleBuilder(key, builder); + const source = await contract.getGetValue(builder.build()); + const result = vHandler.readOptionalFromTupleReader(source); + return result; +} + +export async function getExists( + contract: SandboxContract, + key: K, + kHandler: KeyTypeHandler, +) { + const builder = new TupleBuilder(); + kHandler.storeInTupleBuilder(key, builder); + const source = await contract.getExists(builder.build()); + const result = source.readBoolean(); + return result; +} + +export function getContractStateInit(contractCode: Buffer): StateInit { + const data = beginCell().storeUint(0, 1).endCell(); + const code = Cell.fromBoc(contractCode)[0]; + if (typeof code === "undefined") { + throw new Error("Code cell expected"); + } + return { code, data }; +} + +export function createDict( + initialKeyValuePairs: [K, V][], + kHandler: KeyTypeHandler, + vHandler: ValueTypeHandler, +) { + const dict = Dictionary.empty( + kHandler.getDictionaryKeyType(), + vHandler.getDictionaryValueType(), + ); + for (const [k, v] of initialKeyValuePairs) { + dict.set(k, v); + } + return dict; +} + +// The interface responsible for handling methods specific for types acting +// as keys in dictionaries +export interface KeyTypeHandler { + getDictionaryKeyType(): DictionaryKey; + getGenerator(): fc.Arbitrary; + storeInCellBuilder(v: K, builder: Builder): void; + storeInTupleBuilder(v: K, builder: TupleBuilder): void; +} + +// The interface responsible for handling methods specific for types acting +// as values in dictionaries +export interface ValueTypeHandler { + getDictionaryValueType(): DictionaryValue; + getGenerator(): fc.Arbitrary; + storeInCellBuilder(v: V, builder: Builder): void; + readOptionalFromTupleReader(reader: TupleReader): V | undefined; + equals(o1: V | undefined, o2: V | undefined): boolean; +} + +// This type represents the existential type (sigma type): +// exists T. KeyTypeHandler +type ExistsKeyTypeHandler = ( + cont: (handler: KeyTypeHandler) => R, +) => R; + +// This type represents the existential type (sigma type): +// exists T. ValueTypeHandler +type ExistsValueTypeHandler = ( + cont: (handler: ValueTypeHandler) => R, +) => R; + +// The constructor for the existential type for KeyTypeHandler +function constructKeyTypeExistential( + handler: KeyTypeHandler, +): ExistsKeyTypeHandler { + return ( + cont: (h: KeyTypeHandler) => R, + ) => cont(handler); +} + +// The constructor for the existential type for ValueTypeHandler +function constructValueTypeExistential( + handler: ValueTypeHandler, +): ExistsValueTypeHandler { + return (cont: (h: ValueTypeHandler) => R) => cont(handler); +} + +export function getKeyTypeHandler(kT: keyType): ExistsKeyTypeHandler { + const handler = getTypeHandlers(kT)[0]; + if (typeof handler === "undefined") { + throw new Error(`${kT} does not have a key type handler`); + } + return handler; +} + +export function getValueTypeHandler(vT: valueType): ExistsValueTypeHandler { + return getTypeHandlers(vT)[1]; +} + +// This function returns both the key type handler and value type handler for each possible +// dictionary type. Some dictionary types do not have a key type handler; for example, "Bool" +// does not have a key type handler because it cannot act as keys in a dictionary. +// For such types, the function returns "undefined" in the first position of the tuple. +function getTypeHandlers( + kvT: keyValueTypes, +): [ExistsKeyTypeHandler | undefined, ExistsValueTypeHandler] { + switch (kvT) { + case "Int": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigInt(257), + getGenerator: () => _generateIntBitLength(257, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 257), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.BigInt(257), + getGenerator: () => _generateIntBitLength(257, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 257), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Address": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Address(), + getGenerator: () => _generateAddress(), + storeInCellBuilder: (v: Address, builder: Builder) => + builder.storeAddress(v), + storeInTupleBuilder: ( + v: Address, + builder: TupleBuilder, + ) => { + builder.writeAddress(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Address(), + getGenerator: () => _generateAddress(), + storeInCellBuilder: (v: Address, builder: Builder) => + builder.storeAddress(v), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readAddressOpt() ?? undefined, + equals: ( + o1: Address | undefined, + o2: Address | undefined, + ) => { + if ( + typeof o1 !== "undefined" && + typeof o2 !== "undefined" + ) { + return o1.equals(o2); + } else { + return o1 === o2; + } + }, + }), + ]; + } + case "Bool": { + return [ + undefined, + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Bool(), + getGenerator: () => fc.boolean(), + storeInCellBuilder: (v: boolean, builder: Builder) => + builder.storeBit(v), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBooleanOpt() ?? undefined, + equals: ( + o1: boolean | undefined, + o2: boolean | undefined, + ) => o1 === o2, + }), + ]; + } + case "Cell": { + return [ + undefined, + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Cell(), + getGenerator: () => _generateCell(), + storeInCellBuilder: (v: Cell, builder: Builder) => + builder.storeRef(v), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readCellOpt() ?? undefined, + equals: (o1: Cell | undefined, o2: Cell | undefined) => { + if ( + typeof o1 !== "undefined" && + typeof o2 !== "undefined" + ) { + return o1.equals(o2); + } else { + return o1 === o2; + } + }, + }), + ]; + } + case "Int as int8": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Int(8), + getGenerator: () => + _generateIntBitLength(8, true).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeInt(v, 8), + storeInTupleBuilder: (v: number, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Int(8), + getGenerator: () => + _generateIntBitLength(8, true).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeInt(v, 8), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readNumberOpt() ?? undefined, + equals: (o1: number | undefined, o2: number | undefined) => + o1 === o2, + }), + ]; + } + case "Int as int16": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Int(16), + getGenerator: () => + _generateIntBitLength(16, true).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeInt(v, 16), + storeInTupleBuilder: (v: number, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Int(16), + getGenerator: () => + _generateIntBitLength(16, true).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeInt(v, 16), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readNumberOpt() ?? undefined, + equals: (o1: number | undefined, o2: number | undefined) => + o1 === o2, + }), + ]; + } + case "Int as int32": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Int(32), + getGenerator: () => + _generateIntBitLength(32, true).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeInt(v, 32), + storeInTupleBuilder: (v: number, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Int(32), + getGenerator: () => + _generateIntBitLength(32, true).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeInt(v, 32), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readNumberOpt() ?? undefined, + equals: (o1: number | undefined, o2: number | undefined) => + o1 === o2, + }), + ]; + } + case "Int as int64": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigInt(64), + getGenerator: () => _generateIntBitLength(64, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 64), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.BigInt(64), + getGenerator: () => _generateIntBitLength(64, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 64), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as int128": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigInt(128), + getGenerator: () => _generateIntBitLength(128, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 128), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.BigInt(128), + getGenerator: () => _generateIntBitLength(128, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 128), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as int256": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigInt(256), + getGenerator: () => _generateIntBitLength(256, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 256), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.BigInt(256), + getGenerator: () => _generateIntBitLength(256, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 256), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as int257": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigInt(257), + getGenerator: () => _generateIntBitLength(257, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 257), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.BigInt(257), + getGenerator: () => _generateIntBitLength(257, true), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeInt(v, 257), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as uint8": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Uint(8), + getGenerator: () => + _generateIntBitLength(8, false).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeUint(v, 8), + storeInTupleBuilder: (v: number, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Uint(8), + getGenerator: () => + _generateIntBitLength(8, false).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeUint(v, 8), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readNumberOpt() ?? undefined, + equals: (o1: number | undefined, o2: number | undefined) => + o1 === o2, + }), + ]; + } + case "Int as uint16": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Uint(16), + getGenerator: () => + _generateIntBitLength(16, false).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeUint(v, 16), + storeInTupleBuilder: (v: number, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Uint(16), + getGenerator: () => + _generateIntBitLength(16, false).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeUint(v, 16), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readNumberOpt() ?? undefined, + equals: (o1: number | undefined, o2: number | undefined) => + o1 === o2, + }), + ]; + } + case "Int as uint32": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.Uint(32), + getGenerator: () => + _generateIntBitLength(32, false).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeUint(v, 32), + storeInTupleBuilder: (v: number, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.Uint(32), + getGenerator: () => + _generateIntBitLength(32, false).map((n) => Number(n)), + storeInCellBuilder: (v: number, builder: Builder) => + builder.storeUint(v, 32), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readNumberOpt() ?? undefined, + equals: (o1: number | undefined, o2: number | undefined) => + o1 === o2, + }), + ]; + } + case "Int as uint64": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigUint(64), + getGenerator: () => _generateIntBitLength(64, false), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeUint(v, 64), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => Dictionary.Values.BigUint(64), + getGenerator: () => _generateIntBitLength(64, false), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeUint(v, 64), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as uint128": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigUint(128), + getGenerator: () => _generateIntBitLength(128, false), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeUint(v, 128), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => + Dictionary.Values.BigUint(128), + getGenerator: () => _generateIntBitLength(128, false), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeUint(v, 128), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as uint256": { + return [ + constructKeyTypeExistential({ + getDictionaryKeyType: () => Dictionary.Keys.BigUint(256), + getGenerator: () => _generateIntBitLength(256, false), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeUint(v, 256), + storeInTupleBuilder: (v: bigint, builder: TupleBuilder) => { + builder.writeNumber(v); + }, + }), + constructValueTypeExistential({ + getDictionaryValueType: () => + Dictionary.Values.BigUint(256), + getGenerator: () => _generateIntBitLength(256, false), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeUint(v, 256), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + case "Int as coins": { + return [ + undefined, + constructValueTypeExistential({ + getDictionaryValueType: () => + Dictionary.Values.BigVarUint(4), + getGenerator: () => _generateCoins(), + storeInCellBuilder: (v: bigint, builder: Builder) => + builder.storeCoins(v), + readOptionalFromTupleReader: (reader: TupleReader) => + reader.readBigNumberOpt() ?? undefined, + equals: (o1: bigint | undefined, o2: bigint | undefined) => + o1 === o2, + }), + ]; + } + } +} + +function testSubwalletId(seed: string): bigint { + return BigInt("0x" + sha256_sync("TEST_SEED" + seed).toString("hex")); +} + +function _generateAddress(): fc.Arbitrary
{ + return fc.string().map((str) => { + const subwalletId = testSubwalletId(str); + const wallet = TreasuryContract.create(0, subwalletId); + return wallet.address; + }); +} + +function _generateCell(): fc.Arbitrary { + return fc.int8Array().map((buf) => { + return beginCell().storeBuffer(Buffer.from(buf.buffer)).endCell(); + }); +} + +function _generateIntBitLength(bitLength: number, signed: boolean) { + const maxUnsigned = (1n << BigInt(bitLength)) - 1n; + + if (signed) { + const minSigned = -maxUnsigned / 2n - 1n; + const maxSigned = maxUnsigned / 2n; + return fc.bigInt(minSigned, maxSigned); + } else { + return fc.bigInt(0n, maxUnsigned); + } +} + +function _generateCoins() { + return fc + .integer({ min: 0, max: 120 }) + .chain((bitLength) => _generateIntBitLength(bitLength, false)); +} + +export function generateKeyValuePairs( + keyGenerator: () => fc.Arbitrary, + valueGenerator: () => fc.Arbitrary, +) { + return fc.array(fc.tuple(keyGenerator(), valueGenerator())); +} + +export function compareDicts( + dict1: Dictionary, + dict2: Dictionary, + vHandler: ValueTypeHandler, +) { + return ( + dict1 + .keys() + .every((key) => vHandler.equals(dict1.get(key), dict2.get(key))) && + dict2 + .keys() + .every((key) => vHandler.equals(dict2.get(key), dict1.get(key))) + ); +}