diff --git a/artifacts/ts/Add.ts b/artifacts/ts/Add.ts index 71d7e412a..762d6b0a7 100644 --- a/artifacts/ts/Add.ts +++ b/artifacts/ts/Add.ts @@ -69,6 +69,10 @@ export namespace AddTypes { }>; result: CallContractResult<[bigint, bigint]>; }; + addPrivate: { + params: CallContractParams<{ array: [bigint, bigint] }>; + result: CallContractResult<[bigint, bigint]>; + }; createSubContract: { params: CallContractParams<{ a: bigint; @@ -122,6 +126,10 @@ export namespace AddTypes { }>; result: SignExecuteScriptTxResult; }; + addPrivate: { + params: SignExecuteContractMethodParams<{ array: [bigint, bigint] }>; + result: SignExecuteScriptTxResult; + }; createSubContract: { params: SignExecuteContractMethodParams<{ a: bigint; @@ -316,6 +324,11 @@ export class AddInstance extends ContractInstance { ): Promise> => { return callMethod(Add, this, "add2", params, getContractByCodeHash); }, + addPrivate: async ( + params: AddTypes.CallMethodParams<"addPrivate"> + ): Promise> => { + return callMethod(Add, this, "addPrivate", params, getContractByCodeHash); + }, createSubContract: async ( params: AddTypes.CallMethodParams<"createSubContract"> ): Promise> => { @@ -356,6 +369,11 @@ export class AddInstance extends ContractInstance { ): Promise> => { return signExecuteMethod(Add, this, "add2", params); }, + addPrivate: async ( + params: AddTypes.SignExecuteMethodParams<"addPrivate"> + ): Promise> => { + return signExecuteMethod(Add, this, "addPrivate", params); + }, createSubContract: async ( params: AddTypes.SignExecuteMethodParams<"createSubContract"> ): Promise> => { diff --git a/artifacts/ts/MetaData.ts b/artifacts/ts/MetaData.ts index aa4076eab..715a1d152 100644 --- a/artifacts/ts/MetaData.ts +++ b/artifacts/ts/MetaData.ts @@ -51,6 +51,14 @@ export namespace MetaDataTypes { params: Omit, "args">; result: CallContractResult; }; + bar: { + params: Omit, "args">; + result: CallContractResult; + }; + baz: { + params: Omit, "args">; + result: CallContractResult; + }; } export type CallMethodParams = CallMethodTable[T]["params"]; @@ -73,6 +81,14 @@ export namespace MetaDataTypes { params: Omit, "args">; result: SignExecuteScriptTxResult; }; + bar: { + params: Omit, "args">; + result: SignExecuteScriptTxResult; + }; + baz: { + params: Omit, "args">; + result: SignExecuteScriptTxResult; + }; } export type SignExecuteMethodParams = SignExecuteMethodTable[T]["params"]; @@ -164,6 +180,28 @@ export class MetaDataInstance extends ContractInstance { getContractByCodeHash ); }, + bar: async ( + params?: MetaDataTypes.CallMethodParams<"bar"> + ): Promise> => { + return callMethod( + MetaData, + this, + "bar", + params === undefined ? {} : params, + getContractByCodeHash + ); + }, + baz: async ( + params?: MetaDataTypes.CallMethodParams<"baz"> + ): Promise> => { + return callMethod( + MetaData, + this, + "baz", + params === undefined ? {} : params, + getContractByCodeHash + ); + }, }; transact = { @@ -172,5 +210,15 @@ export class MetaDataInstance extends ContractInstance { ): Promise> => { return signExecuteMethod(MetaData, this, "foo", params); }, + bar: async ( + params: MetaDataTypes.SignExecuteMethodParams<"bar"> + ): Promise> => { + return signExecuteMethod(MetaData, this, "bar", params); + }, + baz: async ( + params: MetaDataTypes.SignExecuteMethodParams<"baz"> + ): Promise> => { + return signExecuteMethod(MetaData, this, "baz", params); + }, }; } diff --git a/artifacts/ts/contracts.ts b/artifacts/ts/contracts.ts index 1a5d25d07..8847838e9 100644 --- a/artifacts/ts/contracts.ts +++ b/artifacts/ts/contracts.ts @@ -64,10 +64,7 @@ export function getContractByCodeHash(codeHash: string): Contract { WrongNFTTest, ]; } - const c = contracts.find( - (c) => - c.contract.codeHash === codeHash || c.contract.codeHashDebug === codeHash - ); + const c = contracts.find((c) => c.contract.hasCodeHash(codeHash)); if (c === undefined) { throw new Error("Unknown code with code hash: " + codeHash); } diff --git a/packages/cli/src/codegen.ts b/packages/cli/src/codegen.ts index cf5648fd1..fb98781b3 100644 --- a/packages/cli/src/codegen.ts +++ b/packages/cli/src/codegen.ts @@ -111,7 +111,7 @@ function genCallMethod(contractName: string, functionSig: FunctionSig): string { } function genCallMethods(contract: Contract): string { - const functions = contract.publicFunctions() + const functions = contract.functions if (functions.length === 0) { return '' } @@ -123,7 +123,7 @@ function genCallMethods(contract: Contract): string { } function genTxCallMethods(contract: Contract): string { - const functions = contract.publicFunctions() + const functions = contract.functions if (functions.length === 0) { return '' } @@ -379,7 +379,7 @@ function genTestMethods(contract: Contract): string { } function genCallMethodTypes(contract: Contract): string { - const entities = contract.publicFunctions().map((functionSig) => { + const entities = contract.functions.map((functionSig) => { const funcHasArgs = functionSig.paramNames.length > 0 const params = funcHasArgs ? `CallContractParams<{${formatParameters({ @@ -416,7 +416,7 @@ function genCallMethodTypes(contract: Contract): string { } function genSignExecuteMethodTypes(contract: Contract): string { - const entities = contract.publicFunctions().map((functionSig) => { + const entities = contract.functions.map((functionSig) => { const funcHasArgs = functionSig.paramNames.length > 0 const params = funcHasArgs ? `SignExecuteContractMethodParams<{${formatParameters({ @@ -445,8 +445,7 @@ function genSignExecuteMethodTypes(contract: Contract): string { function genMulticall(contract: Contract): string { const types = contractTypes(contract.name) - const supportMulticall = - contract.publicFunctions().filter((functionSig) => functionSig.returnTypes.length > 0).length > 0 + const supportMulticall = contract.functions.filter((functionSig) => functionSig.returnTypes.length > 0).length > 0 return supportMulticall ? ` async multicall( @@ -624,7 +623,7 @@ function genContractByCodeHash(outDir: string, contractNames: string[]) { if (contracts === undefined) { contracts = [${contracts}] } - const c = contracts.find((c) => c.contract.codeHash === codeHash || c.contract.codeHashDebug === codeHash) + const c = contracts.find((c) => c.contract.hasCodeHash(codeHash)) if (c === undefined) { throw new Error("Unknown code with code hash: " + codeHash) } diff --git a/packages/web3/src/codec/contract-codec.test.ts b/packages/web3/src/codec/contract-codec.test.ts index 696be7a7d..4c17fde21 100644 --- a/packages/web3/src/codec/contract-codec.test.ts +++ b/packages/web3/src/codec/contract-codec.test.ts @@ -376,7 +376,7 @@ describe('Encode & decode contract', function () { expect(decodedContract.fieldLength).toEqual(getTypesLength(contract.fieldsSig.types)) decodedContract.methods.map((decodedMethod, index) => { - const decoded = contract.decodedMethods[index] + const decoded = contract.getDecodedMethod(index) const functionSig = contract.functions[index] expect(decodedMethod.isPublic).toEqual(decoded.isPublic) expect(decodedMethod.usePreapprovedAssets).toEqual(decoded.usePreapprovedAssets) diff --git a/packages/web3/src/contract/contract.ts b/packages/web3/src/contract/contract.ts index 16e05914e..ceca35afc 100644 --- a/packages/web3/src/contract/contract.ts +++ b/packages/web3/src/contract/contract.ts @@ -162,7 +162,7 @@ export abstract class Artifact { this.functions = functions } - abstract buildByteCodeToDeploy(initialFields: Fields, isDevnet: boolean): string + abstract buildByteCodeToDeploy(initialFields: Fields, isDevnet: boolean, exposePrivateFunctions: boolean): string async isDevnet(signer: SignerProvider): Promise { if (!signer.nodeProvider) { @@ -197,7 +197,10 @@ export class Contract extends Artifact { readonly bytecodeDebug: string readonly codeHashDebug: string - readonly decodedMethods: Method[] + readonly decodedContract: contract.Contract + + private bytecodeForTesting: string | undefined + private codeHashForTesting: string | undefined constructor( version: string, @@ -230,19 +233,54 @@ export class Contract extends Artifact { this.bytecodeDebug = ralph.buildDebugBytecode(this.bytecode, this.bytecodeDebugPatch) this.codeHashDebug = codeHashDebug - this.decodedMethods = contract.contractCodec.decodeContract(hexToBinUnsafe(bytecode)).methods + this.decodedContract = contract.contractCodec.decodeContract(hexToBinUnsafe(this.bytecode)) + this.bytecodeForTesting = undefined + this.codeHashForTesting = undefined + } + + getByteCodeForTesting(): string { + if (this.bytecodeForTesting !== undefined) return this.bytecodeForTesting + + if (this.publicFunctions().length == this.functions.length) { + this.bytecodeForTesting = this.bytecodeDebug + this.codeHashForTesting = this.codeHashDebug + return this.bytecodeForTesting + } + + const decodedDebugContract = contract.contractCodec.decodeContract(hexToBinUnsafe(this.bytecodeDebug)) + const methods = decodedDebugContract.methods.map((method) => ({ ...method, isPublic: true })) + const bytecodeForTesting = contract.contractCodec.encodeContract({ + fieldLength: decodedDebugContract.fieldLength, + methods: methods + }) + const codeHashForTesting = blake.blake2b(bytecodeForTesting, undefined, 32) + this.bytecodeForTesting = binToHex(bytecodeForTesting) + this.codeHashForTesting = binToHex(codeHashForTesting) + return this.bytecodeForTesting + } + + hasCodeHash(hash: string): boolean { + return this.codeHash === hash || this.codeHashDebug === hash || this.codeHashForTesting === hash + } + + getDecodedMethod(methodIndex: number): Method { + return this.decodedContract.methods[`${methodIndex}`] } publicFunctions(): FunctionSig[] { - return this.functions.filter((_, index) => this.decodedMethods[`${index}`].isPublic) + return this.functions.filter((_, index) => this.getDecodedMethod(index).isPublic) } usingPreapprovedAssetsFunctions(): FunctionSig[] { - return this.functions.filter((_, index) => this.decodedMethods[`${index}`].usePreapprovedAssets) + return this.functions.filter((_, index) => this.getDecodedMethod(index).usePreapprovedAssets) } usingAssetsInContractFunctions(): FunctionSig[] { - return this.functions.filter((_, index) => this.decodedMethods[`${index}`].useContractAssets) + return this.functions.filter((_, index) => this.getDecodedMethod(index).useContractAssets) + } + + isMethodUsePreapprovedAssets(methodIndex: number): boolean { + return this.getDecodedMethod(methodIndex).usePreapprovedAssets } // TODO: safely parse json @@ -530,7 +568,11 @@ export class Contract extends Artifact { ): Promise { const isDevnet = await this.isDevnet(signer) const initialFields: Fields = params.initialFields ?? {} - const bytecode = this.buildByteCodeToDeploy(addStdIdToFields(this, initialFields), isDevnet) + const bytecode = this.buildByteCodeToDeploy( + addStdIdToFields(this, initialFields), + isDevnet, + params.exposePrivateFunctions ?? false + ) const selectedAccount = await signer.getSelectedAccount() const signerParams: SignDeployContractTxParams = { signerAddress: selectedAccount.address, @@ -546,14 +588,15 @@ export class Contract extends Artifact { return signerParams } - buildByteCodeToDeploy(initialFields: Fields, isDevnet: boolean): string { + buildByteCodeToDeploy(initialFields: Fields, isDevnet: boolean, exposePrivateFunctions = false): string { try { - return ralph.buildContractByteCode( - isDevnet ? this.bytecodeDebug : this.bytecode, - initialFields, - this.fieldsSig, - this.structs - ) + const bytecode = + exposePrivateFunctions && isDevnet + ? this.getByteCodeForTesting() + : isDevnet + ? this.bytecodeDebug + : this.bytecode + return ralph.buildContractByteCode(bytecode, initialFields, this.fieldsSig, this.structs) } catch (error) { throw new Error(`Failed to build bytecode for contract ${this.name}, error: ${error}`) } @@ -975,10 +1018,11 @@ export interface DeployContractParams

{ issueTokenTo?: string gasAmount?: number gasPrice?: Number256 + exposePrivateFunctions?: boolean } assertType< Eq< - Omit, 'initialFields'>, + Omit, 'initialFields' | 'exposePrivateFunctions'>, Omit > > @@ -1687,7 +1731,7 @@ export async function signExecuteMethod { const methodIndex = contract.contract.getMethodIndex(methodName) const functionSig = contract.contract.functions[methodIndex] - const methodUsePreapprovedAssets = contract.contract.decodedMethods[methodIndex].usePreapprovedAssets + const methodUsePreapprovedAssets = contract.contract.isMethodUsePreapprovedAssets(methodIndex) const bytecodeTemplate = getBytecodeTemplate( methodIndex, methodUsePreapprovedAssets, diff --git a/test/contract.test.ts b/test/contract.test.ts index e1810dd65..ef524cc5e 100644 --- a/test/contract.test.ts +++ b/test/contract.test.ts @@ -42,7 +42,14 @@ import { MINIMAL_CONTRACT_DEPOSIT } from '../packages/web3' import { Contract, Script, getContractIdFromUnsignedTx } from '../packages/web3' -import { expectAssertionError, testAddress, randomContractAddress, getSigner, mintToken } from '../packages/web3-test' +import { + expectAssertionError, + testAddress, + randomContractAddress, + getSigner, + mintToken, + randomContractId +} from '../packages/web3-test' import { PrivateKeyWallet } from '@alephium/web3-wallet' import { Greeter, GreeterTypes } from '../artifacts/ts/Greeter' import { @@ -73,6 +80,7 @@ describe('contract', function () { let signer: PrivateKeyWallet let signerAccount: Account let signerGroup: number + let exposePrivateFunctions: boolean beforeAll(async () => { web3.setCurrentNodeProvider('http://127.0.0.1:22973', undefined, fetch) @@ -80,6 +88,7 @@ describe('contract', function () { signer = await getSigner() signerAccount = signer.account signerGroup = signerAccount.group + exposePrivateFunctions = Math.random() < 0.5 expect(signerGroup).toEqual(groupOfAddress(testAddress)) }) @@ -93,11 +102,14 @@ describe('contract', function () { it('should get contract id from tx id', async () => { const nodeProvider = web3.getCurrentNodeProvider() - const deployResult0 = await Sub.deploy(signer, { initialFields: { result: 0n } }) + const deployResult0 = await Sub.deploy(signer, { initialFields: { result: 0n }, exposePrivateFunctions }) const subContractId = await getContractIdFromUnsignedTx(nodeProvider, deployResult0.unsignedTx) expect(subContractId).toEqual(deployResult0.contractInstance.contractId) - const deployResult1 = await Add.deploy(signer, { initialFields: { sub: subContractId, result: 0n } }) + const deployResult1 = await Add.deploy(signer, { + initialFields: { sub: subContractId, result: 0n }, + exposePrivateFunctions + }) const addContractId = await getContractIdFromUnsignedTx(nodeProvider, deployResult1.unsignedTx) expect(addContractId).toEqual(deployResult1.contractInstance.contractId) }) @@ -154,9 +166,11 @@ describe('contract', function () { }) expect(testResultPrivate.returns).toEqual([3n, 1n]) - const sub = (await Sub.deploy(signer, { initialFields: { result: 0n } })).contractInstance + const sub = (await Sub.deploy(signer, { initialFields: { result: 0n }, exposePrivateFunctions })).contractInstance expect(sub.groupIndex).toEqual(signerGroup) - const add = (await Add.deploy(signer, { initialFields: { sub: sub.contractId, result: 0n } })).contractInstance + const add = ( + await Add.deploy(signer, { initialFields: { sub: sub.contractId, result: 0n }, exposePrivateFunctions }) + ).contractInstance expect(add.groupIndex).toEqual(signerGroup) // Check state for add/sub before main script is executed @@ -197,8 +211,9 @@ describe('contract', function () { expect(testResult.contracts[0].codeHash).toEqual(Greeter.contract.codeHash) expect(testResult.contracts[0].fields.btcPrice).toEqual(1n) - const greeter = (await Greeter.deploy(signer, { initialFields: { ...initialFields, btcPrice: 1n } })) - .contractInstance + const greeter = ( + await Greeter.deploy(signer, { initialFields: { ...initialFields, btcPrice: 1n }, exposePrivateFunctions }) + ).contractInstance expect(greeter.groupIndex).toEqual(signerGroup) const contractState = await greeter.fetchState() expect(contractState.fields.btcPrice).toEqual(1n) @@ -300,7 +315,7 @@ describe('contract', function () { const contractAddress = randomContractAddress() expectAssertionError(Assert.tests.test({ address: contractAddress }), contractAddress, AssertError) - const assertDeployResult = await Assert.deploy(signer, { initialFields: {} }) + const assertDeployResult = await Assert.deploy(signer, { initialFields: {}, exposePrivateFunctions }) const assertAddress = assertDeployResult.contractInstance.address expectAssertionError( @@ -441,7 +456,7 @@ describe('contract', function () { }, name: '' } - const result = await UserAccount.deploy(signer, { initialFields }) + const result = await UserAccount.deploy(signer, { initialFields, exposePrivateFunctions }) const state = await result.contractInstance.fetchState() expect(state.fields).toEqual(initialFields) @@ -502,9 +517,7 @@ describe('contract', function () { }) it('should test map(integration test)', async () => { - const result = await MapTest.deploy(signer, { - initialFields: {} - }) + const result = await MapTest.deploy(signer, { initialFields: {}, exposePrivateFunctions }) const mapTest = result.contractInstance await InsertIntoMap.execute(signer, { @@ -554,9 +567,13 @@ describe('contract', function () { }) it('should test sign execute method with primitive arguments', async () => { - const sub = await Sub.deploy(signer, { initialFields: { result: 0n } }) - const add = (await Add.deploy(signer, { initialFields: { sub: sub.contractInstance.contractId, result: 0n } })) - .contractInstance + const sub = await Sub.deploy(signer, { initialFields: { result: 0n }, exposePrivateFunctions }) + const add = ( + await Add.deploy(signer, { + initialFields: { sub: sub.contractInstance.contractId, result: 0n }, + exposePrivateFunctions + }) + ).contractInstance const caller = (await signer.getSelectedAccount()).address const provider = web3.getCurrentNodeProvider() @@ -568,9 +585,13 @@ describe('contract', function () { }) it('should test sign execute method with array arguments', async () => { - const sub = await Sub.deploy(signer, { initialFields: { result: 0n } }) - const add = (await Add.deploy(signer, { initialFields: { sub: sub.contractInstance.contractId, result: 0n } })) - .contractInstance + const sub = await Sub.deploy(signer, { initialFields: { result: 0n }, exposePrivateFunctions }) + const add = ( + await Add.deploy(signer, { + initialFields: { sub: sub.contractInstance.contractId, result: 0n }, + exposePrivateFunctions + }) + ).contractInstance const provider = web3.getCurrentNodeProvider() const stateBefore = await provider.contracts.getContractsAddressState(add.address) @@ -583,9 +604,13 @@ describe('contract', function () { }) it('should test sign execute method with struct arguments', async () => { - const sub = await Sub.deploy(signer, { initialFields: { result: 0n } }) - const add = (await Add.deploy(signer, { initialFields: { sub: sub.contractInstance.contractId, result: 0n } })) - .contractInstance + const sub = await Sub.deploy(signer, { initialFields: { result: 0n }, exposePrivateFunctions }) + const add = ( + await Add.deploy(signer, { + initialFields: { sub: sub.contractInstance.contractId, result: 0n }, + exposePrivateFunctions + }) + ).contractInstance const provider = web3.getCurrentNodeProvider() const stateBefore = await provider.contracts.getContractsAddressState(add.address) @@ -616,10 +641,15 @@ describe('contract', function () { const sub = await Sub.deploy(signer, { initialFields: { result: 0n }, issueTokenAmount: 300n, - issueTokenTo: signerAddress + issueTokenTo: signerAddress, + exposePrivateFunctions }) - const add = (await Add.deploy(signer, { initialFields: { sub: sub.contractInstance.contractId, result: 0n } })) - .contractInstance + const add = ( + await Add.deploy(signer, { + initialFields: { sub: sub.contractInstance.contractId, result: 0n }, + exposePrivateFunctions + }) + ).contractInstance const provider = web3.getCurrentNodeProvider() const state = await provider.contracts.getContractsAddressState(add.address) @@ -686,9 +716,7 @@ describe('contract', function () { }) it('should call TxScript', async () => { - const result0 = await MapTest.deploy(signer, { - initialFields: {} - }) + const result0 = await MapTest.deploy(signer, { initialFields: {}, exposePrivateFunctions }) const mapTest = result0.contractInstance await InsertIntoMap.execute(signer, { initialFields: { @@ -715,7 +743,7 @@ describe('contract', function () { }, name: '' } - const result1 = await UserAccount.deploy(signer, { initialFields }) + const result1 = await UserAccount.deploy(signer, { initialFields, exposePrivateFunctions }) const userAccount = result1.contractInstance const callResult1 = await CallScript1.call({ @@ -761,4 +789,39 @@ describe('contract', function () { }) expect(BigInt(state2.asset.alphAmount)).toEqual(MINIMAL_CONTRACT_DEPOSIT) }) + + it('should get the contract bytecode for testing', async () => { + expect(Add.contract.publicFunctions().length).not.toEqual(Add.contract.functions.length) + const instance0 = ( + await Add.deploy(signer, { initialFields: { sub: randomContractId(), result: 0n }, exposePrivateFunctions: true }) + ).contractInstance + const state0 = await instance0.fetchState() + expect(state0.bytecode).toEqual(Add.contract.getByteCodeForTesting()) + expect(state0.bytecode).not.toEqual(Add.contract.bytecode) + expect(state0.bytecode).not.toEqual(Add.contract.bytecodeDebug) + expect(state0.codeHash).not.toEqual(Add.contract.codeHash) + expect(state0.codeHash).not.toEqual(Add.contract.codeHashDebug) + expect(Add.contract.hasCodeHash(state0.codeHash)).toEqual(true) + + expect(Assert.contract.publicFunctions().length).toEqual(Assert.contract.functions.length) + const instance1 = (await Assert.deploy(signer, { initialFields: {}, exposePrivateFunctions: true })) + .contractInstance + const state1 = await instance1.fetchState() + expect(state1.bytecode).toEqual(Assert.contract.bytecodeDebug) + expect(state1.codeHash).toEqual(Assert.contract.codeHashDebug) + expect(Assert.contract.hasCodeHash(state1.codeHash)).toEqual(true) + }) + + it('should test contract private functions', async () => { + const sub = (await Sub.deploy(signer, { initialFields: { result: 0n }, exposePrivateFunctions: true })) + .contractInstance + const add = ( + await Add.deploy(signer, { initialFields: { sub: sub.contractId, result: 0n }, exposePrivateFunctions: true }) + ).contractInstance + await add.transact.addPrivate({ args: { array: [2n, 1n] }, signer }) + const state0 = await add.fetchState() + expect(state0.fields.result).toEqual(3n) + const state1 = await sub.fetchState() + expect(state1.fields.result).toEqual(1n) + }) })