diff --git a/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts b/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts index b1eac3b2..5566c3fa 100644 --- a/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts +++ b/contracts/accessControl/src/test/simulators/AccessControlSimulator.ts @@ -2,18 +2,18 @@ import { type CircuitContext, type CoinPublicKey, type ContractState, - QueryContext, constructorContext, emptyZswapLocalState, + QueryContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; import { type ContractAddress, type Either, type Ledger, + ledger, Contract as MockAccessControl, type ZswapCoinPublicKey, - ledger, } from '../../artifacts/MockAccessControl/contract/index.cjs'; // Combined imports import { type AccessControlPrivateState, diff --git a/contracts/nonFungibleToken/package.json b/contracts/nonFungibleToken/package.json index c9c39e4c..93f3201d 100644 --- a/contracts/nonFungibleToken/package.json +++ b/contracts/nonFungibleToken/package.json @@ -24,6 +24,8 @@ "@openzeppelin-compact/compact": "workspace:^" }, "devDependencies": { + "@openzeppelin-compact/compact-std": "workspace:^", + "@openzeppelin-compact/testing": "workspace:^", "@types/node": "22.14.0", "ts-node": "^10.9.2", "typescript": "^5.2.2", diff --git a/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact b/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact index a883800d..779e7a85 100644 --- a/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact +++ b/contracts/nonFungibleToken/src/test/mocks/MockNonFungibleToken.compact @@ -1,11 +1,8 @@ pragma language_version >= 0.16.0; import CompactStandardLibrary; - import "../../NonFungibleToken" prefix NonFungibleToken_; -export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; - /** * @description `init` is a param for testing. * If `init` is true, initialize the contract with `_name` and `_symbol`. diff --git a/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts b/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts index d58ca060..c304a3c8 100644 --- a/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts +++ b/contracts/nonFungibleToken/src/test/nonFungibleToken.test.ts @@ -1,13 +1,12 @@ -import type { CoinPublicKey } from '@midnight-ntwrk/compact-runtime'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { NonFungibleTokenSimulator } from './simulators/NonFungibleTokenSimulator.js'; import { createEitherTestContractAddress, createEitherTestUser, toHexPadded, ZERO_ADDRESS, ZERO_KEY, -} from './utils/address.js'; +} from '@openzeppelin-compact/testing'; +import { beforeEach, describe, expect, it } from 'vitest'; +import { NonFungibleTokenSimulator } from './simulators/NonFungibleTokenSimulator.js'; // Contract Metadata const NAME = 'NAME'; @@ -39,7 +38,6 @@ const Z_UNAUTHORIZED = createEitherTestUser('UNAUTHORIZED'); const SOME_CONTRACT = createEitherTestContractAddress('CONTRACT'); let token: NonFungibleTokenSimulator; -let _caller: CoinPublicKey; describe('NonFungibleToken', () => { describe('initializer and metadata', () => { @@ -223,65 +221,68 @@ describe('NonFungibleToken', () => { }); it('should throw if not owner', () => { - _caller = UNAUTHORIZED; + token.setCaller(UNAUTHORIZED); expect(() => { - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.approve(Z_SPENDER, TOKENID_1); }).toThrow('NonFungibleToken: Invalid Approver'); }); it('should approve spender', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); }); it('should allow operator to approve', () => { - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, _caller); - _caller = SPENDER; - token.approve(Z_OTHER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); + + token.setCaller(SPENDER); + token.approve(Z_OTHER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_OTHER); }); it('spender approved for only TOKENID_1 should not be able to approve', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); - _caller = SPENDER; + token.setCaller(SPENDER); expect(() => { - token.approve(Z_OTHER, TOKENID_1, _caller); + token.approve(Z_OTHER, TOKENID_1); }).toThrow('NonFungibleToken: Invalid Approver'); }); it('should approve same address multiple times', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); + token.approve(Z_SPENDER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); }); it('should approve after token transfer', () => { - _caller = OWNER; + token.setCaller(OWNER); token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); - _caller = SPENDER; - token.approve(Z_OTHER, TOKENID_1, _caller); + token.setCaller(SPENDER); + token.approve(Z_OTHER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_OTHER); }); it('should approve after token burn and remint', () => { - _caller = OWNER; token._burn(TOKENID_1); token._mint(Z_OWNER, TOKENID_1); - token.approve(Z_SPENDER, TOKENID_1, _caller); + + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); }); it('should approve with very long token ID', () => { - _caller = OWNER; const longTokenId = BigInt('18446744073709551615'); token._mint(Z_OWNER, longTokenId); - token.approve(Z_SPENDER, longTokenId, _caller); + + token.setCaller(OWNER); + token.approve(Z_SPENDER, longTokenId); expect(token.getApproved(longTokenId)).toEqual(Z_SPENDER); }); }); @@ -305,8 +306,8 @@ describe('NonFungibleToken', () => { }); it('should get current approved spender', () => { - _caller = OWNER; - token.approve(Z_OWNER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_OWNER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_OWNER); }); @@ -317,18 +318,19 @@ describe('NonFungibleToken', () => { describe('setApprovalForAll', () => { it('should not approve zero address', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); + token.setCaller(OWNER); + expect(() => { - token.setApprovalForAll(ZERO_KEY, true, _caller); + token.setApprovalForAll(ZERO_KEY, true); }).toThrow('NonFungibleToken: Invalid Operator'); }); it('should set operator', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, OWNER); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); }); @@ -336,67 +338,71 @@ describe('NonFungibleToken', () => { token._mint(Z_OWNER, TOKENID_1); token._mint(Z_OWNER, TOKENID_2); token._mint(Z_OWNER, TOKENID_3); - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, _caller); - _caller = SPENDER; - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); + + token.setCaller(SPENDER); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); - token.approve(Z_OTHER, TOKENID_2, _caller); + token.approve(Z_OTHER, TOKENID_2); expect(token.getApproved(TOKENID_2)).toEqual(Z_OTHER); - token.approve(Z_SPENDER, TOKENID_3, _caller); + token.approve(Z_SPENDER, TOKENID_3); expect(token.getApproved(TOKENID_3)).toEqual(Z_SPENDER); }); it('should revoke approval for all', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); - token.setApprovalForAll(Z_SPENDER, false, _caller); + token.setApprovalForAll(Z_SPENDER, false); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(false); - _caller = SPENDER; + token.setCaller(SPENDER); expect(() => { - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.approve(Z_SPENDER, TOKENID_1); }).toThrow('NonFungibleToken: Invalid Approver'); }); it('should set approval for all to same address multiple times', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, _caller); - token.setApprovalForAll(Z_SPENDER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); + token.setApprovalForAll(Z_SPENDER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); }); it('should set approval for all after token transfer', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); token._transfer(Z_OWNER, Z_SPENDER, TOKENID_1); - _caller = SPENDER; - token.setApprovalForAll(Z_OTHER, true, _caller); + token.setCaller(SPENDER); + token.setApprovalForAll(Z_OTHER, true); expect(token.isApprovedForAll(Z_SPENDER, Z_OTHER)).toBe(true); }); it('should set approval for all with multiple operators', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, _caller); - token.setApprovalForAll(Z_OTHER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); + token.setApprovalForAll(Z_OTHER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); expect(token.isApprovedForAll(Z_OWNER, Z_OTHER)).toBe(true); }); it('should set approval for all with very long token IDs', () => { - _caller = OWNER; const longTokenId = BigInt('18446744073709551615'); token._mint(Z_OWNER, longTokenId); - token.setApprovalForAll(Z_SPENDER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); }); }); @@ -407,9 +413,10 @@ describe('NonFungibleToken', () => { }); it('should return true if approval set', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, OWNER); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); }); }); @@ -438,50 +445,50 @@ describe('NonFungibleToken', () => { }); it('should not transfer from unauthorized', () => { - _caller = UNAUTHORIZED; + token.setCaller(UNAUTHORIZED); expect(() => { - token.transferFrom(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1, _caller); + token.transferFrom(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1); }).toThrow('NonFungibleToken: Insufficient Approval'); }); it('should not transfer token that has not been minted', () => { - _caller = OWNER; + token.setCaller(OWNER); expect(() => { - token.transferFrom(Z_OWNER, Z_SPENDER, NON_EXISTENT_TOKEN, _caller); + token.transferFrom(Z_OWNER, Z_SPENDER, NON_EXISTENT_TOKEN); }).toThrow('NonFungibleToken: Nonexistent Token'); }); it('should transfer token without approvers or operators', () => { - _caller = OWNER; - token.transferFrom(Z_OWNER, Z_RECIPIENT, TOKENID_1, _caller); + token.setCaller(OWNER); + token.transferFrom(Z_OWNER, Z_RECIPIENT, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(Z_RECIPIENT); }); it('should transfer token via approved operator', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, OWNER); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); - _caller = SPENDER; - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.setCaller(SPENDER); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); }); it('should transfer token via approvedForAll operator', () => { - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, OWNER); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); - _caller = SPENDER; - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.setCaller(SPENDER); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); }); it('should allow transfer to same address', () => { - _caller = OWNER; token._approve(Z_SPENDER, TOKENID_1, Z_OWNER); token._setApprovalForAll(Z_OWNER, Z_SPENDER, true); + token.setCaller(OWNER); expect(() => { - token.transferFrom(Z_OWNER, Z_OWNER, TOKENID_1, _caller); + token.transferFrom(Z_OWNER, Z_OWNER, TOKENID_1); }).not.toThrow(); expect(token.ownerOf(TOKENID_1)).toEqual(Z_OWNER); expect(token.balanceOf(Z_OWNER)).toEqual(1n); @@ -490,40 +497,40 @@ describe('NonFungibleToken', () => { }); it('should not transfer after approval revocation', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); - token.approve(ZERO_KEY, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); + token.approve(ZERO_KEY, TOKENID_1); - _caller = SPENDER; + token.setCaller(SPENDER); expect(() => { - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); }).toThrow('NonFungibleToken: Insufficient Approval'); }); it('should not transfer after approval for all revocation', () => { - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, _caller); - token.setApprovalForAll(Z_SPENDER, false, _caller); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); + token.setApprovalForAll(Z_SPENDER, false); - _caller = SPENDER; + token.setCaller(SPENDER); expect(() => { - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); }).toThrow('NonFungibleToken: Insufficient Approval'); }); it('should transfer multiple tokens in sequence', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_2); token._mint(Z_OWNER, TOKENID_3); - token.approve(Z_SPENDER, TOKENID_1, _caller); - token.approve(Z_SPENDER, TOKENID_2, _caller); - token.approve(Z_SPENDER, TOKENID_3, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); + token.approve(Z_SPENDER, TOKENID_2); + token.approve(Z_SPENDER, TOKENID_3); - _caller = SPENDER; - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_2, _caller); - token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_3, _caller); + token.setCaller(SPENDER); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_2); + token.transferFrom(Z_OWNER, Z_SPENDER, TOKENID_3); expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); expect(token.ownerOf(TOKENID_2)).toEqual(Z_SPENDER); @@ -531,31 +538,32 @@ describe('NonFungibleToken', () => { }); it('should transfer with very long token IDs', () => { - _caller = OWNER; const longTokenId = BigInt('18446744073709551615'); token._mint(Z_OWNER, longTokenId); - token.approve(Z_SPENDER, longTokenId, _caller); - _caller = SPENDER; - token.transferFrom(Z_OWNER, Z_SPENDER, longTokenId, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, longTokenId); + + token.setCaller(SPENDER); + token.transferFrom(Z_OWNER, Z_SPENDER, longTokenId); expect(token.ownerOf(longTokenId)).toEqual(Z_SPENDER); }); it('should revoke approval after transferFrom', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); token._setApprovalForAll(Z_OWNER, Z_SPENDER, true); - token.transferFrom(Z_OWNER, Z_OTHER, TOKENID_1, _caller); + token.transferFrom(Z_OWNER, Z_OTHER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); expect(token._isAuthorized(Z_OTHER, Z_SPENDER, TOKENID_1)).toBe(false); - _caller = SPENDER; + token.setCaller(SPENDER); expect(() => { - token.approve(Z_UNAUTHORIZED, TOKENID_1, _caller); + token.approve(Z_UNAUTHORIZED, TOKENID_1); }).toThrow('NonFungibleToken: Invalid Approver'); expect(() => { - token.transferFrom(Z_OTHER, Z_UNAUTHORIZED, TOKENID_1, _caller); + token.transferFrom(Z_OTHER, Z_UNAUTHORIZED, TOKENID_1); }).toThrow('NonFungibleToken: Insufficient Approval'); }); }); @@ -600,9 +608,10 @@ describe('NonFungibleToken', () => { }); it('should approve if auth is approved for all', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); token._approve(Z_SPENDER, TOKENID_1, Z_SPENDER); expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); }); @@ -637,15 +646,17 @@ describe('NonFungibleToken', () => { it('should not throw if approved', () => { token._mint(Z_OWNER, TOKENID_1); - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); token._checkAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1); }); it('should not throw if approvedForAll', () => { token._mint(Z_OWNER, TOKENID_1); - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); token._checkAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1); }); }); @@ -656,14 +667,14 @@ describe('NonFungibleToken', () => { }); it('should return true if spender is authorized', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); expect(token._isAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1)).toBe(true); }); it('should return true if spender is authorized for all', () => { - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, _caller); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); expect(token._isAuthorized(Z_OWNER, Z_SPENDER, TOKENID_1)).toBe(true); }); @@ -692,8 +703,8 @@ describe('NonFungibleToken', () => { }); it('should return approved address', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); expect(token._getApproved(TOKENID_1)).toEqual(Z_SPENDER); }); @@ -710,9 +721,10 @@ describe('NonFungibleToken', () => { }); it('should revoke operator approval', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.setApprovalForAll(Z_SPENDER, true, _caller); + + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); expect(token.isApprovedForAll(Z_OWNER, Z_SPENDER)).toBe(true); token._setApprovalForAll(Z_OWNER, Z_SPENDER, false); @@ -803,8 +815,8 @@ describe('NonFungibleToken', () => { }); it('should clear approval when token is burned', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(Z_SPENDER); token._burn(TOKENID_1); @@ -835,8 +847,8 @@ describe('NonFungibleToken', () => { }); it('should burn after approval', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); token._burn(TOKENID_1); expect(token._ownerOf(TOKENID_1)).toEqual(ZERO_KEY); expect(token._getApproved(TOKENID_1)).toEqual(ZERO_KEY); @@ -883,9 +895,10 @@ describe('NonFungibleToken', () => { }); it('should revoke approval after _transfer', () => { - _caller = OWNER; token._mint(Z_OWNER, TOKENID_1); - token.approve(Z_SPENDER, TOKENID_1, _caller); + + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); token._transfer(Z_OWNER, Z_OTHER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); }); @@ -985,8 +998,8 @@ describe('NonFungibleToken', () => { }); it('should revoke approval after _unsafeTransfer', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); token._unsafeTransfer(Z_OWNER, Z_OTHER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); }); @@ -1024,64 +1037,59 @@ describe('NonFungibleToken', () => { }); it('unapproved operator should not transfer', () => { - _caller = SPENDER; + token.setCaller(SPENDER); expect(() => { - token._unsafeTransferFrom(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1, _caller); + token._unsafeTransferFrom(Z_OWNER, Z_UNAUTHORIZED, TOKENID_1); }).toThrow('NonFungibleToken: Insufficient Approval'); }); it('should not transfer token that has not been minted', () => { - _caller = OWNER; + token.setCaller(OWNER); expect(() => { - token._unsafeTransferFrom( - Z_OWNER, - Z_SPENDER, - NON_EXISTENT_TOKEN, - _caller, - ); + token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, NON_EXISTENT_TOKEN); }).toThrow('NonFungibleToken: Nonexistent Token'); }); it('should transfer token to spender via approved operator', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, OWNER); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); - _caller = SPENDER; - token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.setCaller(SPENDER); + token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); }); it('should transfer token to ContractAddress via approved operator', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, OWNER); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); - _caller = SPENDER; - token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1, _caller); + token.setCaller(SPENDER); + token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); }); it('should transfer token to spender via approvedForAll operator', () => { - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, OWNER); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); - _caller = SPENDER; - token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, TOKENID_1, _caller); + token.setCaller(SPENDER); + token._unsafeTransferFrom(Z_OWNER, Z_SPENDER, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(Z_SPENDER); }); it('should transfer token to ContractAddress via approvedForAll operator', () => { - _caller = OWNER; - token.setApprovalForAll(Z_SPENDER, true, OWNER); + token.setCaller(OWNER); + token.setApprovalForAll(Z_SPENDER, true); - _caller = SPENDER; - token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1, _caller); + token.setCaller(SPENDER); + token._unsafeTransferFrom(Z_OWNER, SOME_CONTRACT, TOKENID_1); expect(token.ownerOf(TOKENID_1)).toEqual(SOME_CONTRACT); }); it('should revoke approval after _unsafeTransferFrom', () => { - _caller = OWNER; - token.approve(Z_SPENDER, TOKENID_1, _caller); - token._unsafeTransferFrom(Z_OWNER, Z_OTHER, TOKENID_1, _caller); + token.setCaller(OWNER); + token.approve(Z_SPENDER, TOKENID_1); + token._unsafeTransferFrom(Z_OWNER, Z_OTHER, TOKENID_1); expect(token.getApproved(TOKENID_1)).toEqual(ZERO_KEY); }); }); diff --git a/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts b/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts index 1ff228ef..c32356c0 100644 --- a/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts +++ b/contracts/nonFungibleToken/src/test/simulators/NonFungibleTokenSimulator.ts @@ -1,47 +1,53 @@ import { type CircuitContext, type CoinPublicKey, - type ContractState, constructorContext, emptyZswapLocalState, QueryContext, } from '@midnight-ntwrk/compact-runtime'; import { sampleContractAddress } from '@midnight-ntwrk/zswap'; +import type { + ContractAddress, + Either, + ZswapCoinPublicKey, +} from '@openzeppelin-compact/compact-std'; +import { + AbstractContractSimulator, + type ContextlessCircuits, + type ExtractImpureCircuits, + type ExtractPureCircuits, +} from '@openzeppelin-compact/testing'; import { - type ContractAddress, - type Either, type Ledger, ledger, Contract as MockNonFungibleToken, - type ZswapCoinPublicKey, -} from '../../artifacts/MockNonFungibleToken/contract/index.cjs'; // Combined imports +} from '../../artifacts/MockNonFungibleToken/contract/index.cjs'; import { type NonFungibleTokenPrivateState, NonFungibleTokenWitnesses, } from '../../witnesses/NonFungibleTokenWitnesses.js'; -import type { IContractSimulator } from '../types/test.js'; - -/** - * @description A simulator implementation of an nonFungibleToken contract for testing purposes. - * @template P - The private state type, fixed to NonFungibleTokenPrivateState. - * @template L - The ledger type, fixed to Contract.Ledger. - */ -export class NonFungibleTokenSimulator - implements IContractSimulator -{ - /** @description The underlying contract instance managing contract logic. */ - readonly contract: MockNonFungibleToken; - /** @description The deployed address of the contract. */ +export class NonFungibleTokenSimulator extends AbstractContractSimulator< + NonFungibleTokenPrivateState, + Ledger +> { + readonly contract: MockNonFungibleToken; readonly contractAddress: string; - - /** @description The current circuit context, updated by contract operations. */ circuitContext: CircuitContext; + private callerOverride: CoinPublicKey | null = null; + + private _pureCircuitProxy?: ContextlessCircuits< + ExtractPureCircuits>, + NonFungibleTokenPrivateState + >; + + private _impureCircuitProxy?: ContextlessCircuits< + ExtractImpureCircuits>, + NonFungibleTokenPrivateState + >; - /** - * @description Initializes the mock contract. - */ constructor(name: string, symbol: string, init: boolean) { + super(); this.contract = new MockNonFungibleToken( NonFungibleTokenWitnesses, ); @@ -68,27 +74,104 @@ export class NonFungibleTokenSimulator } /** - * @description Retrieves the current public ledger state of the contract. - * @returns The ledger state as defined by the contract. + * @description Constructs a caller-specific circuit context. + * If a caller override is present, it replaces the current Zswap local state with an empty one + * scoped to the overridden caller. Otherwise, the existing context is reused as-is. + * @returns A circuit context adjusted for the current simulated caller. */ - public getCurrentPublicState(): Ledger { - return ledger(this.circuitContext.transactionContext.state); + protected getCallerContext(): CircuitContext { + return { + ...this.circuitContext, + currentZswapLocalState: this.callerOverride + ? emptyZswapLocalState(this.callerOverride) + : this.circuitContext.currentZswapLocalState, + }; + } + + /** + * @description Initializes and returns a proxy to pure contract circuits. + * The proxy automatically injects the current circuit context into each call, + * and returns only the result portion of each circuit's output. + * @notice The proxy is created only when first accessed a.k.a lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing pure circuit functions without requiring explicit context. + */ + protected get pureCircuit(): ContextlessCircuits< + ExtractPureCircuits>, + NonFungibleTokenPrivateState + > { + if (!this._pureCircuitProxy) { + this._pureCircuitProxy = this.createPureCircuitProxy< + MockNonFungibleToken['circuits'] + >(this.contract.circuits, () => this.circuitContext); + } + return this._pureCircuitProxy; + } + + /** + * @description Initializes and returns a proxy to impure contract circuits. + * The proxy automatically injects the current (possibly caller-modified) context into each call, + * and updates the circuit context with the one returned by the circuit after execution. + * @notice The proxy is created only when first accessed a.k.a. lazy initialization. + * This approach is efficient in cases where only pure or only impure circuits are used, + * avoiding unnecessary proxy creation. + * @returns A proxy object exposing impure circuit functions without requiring explicit context management. + */ + protected get impureCircuit(): ContextlessCircuits< + ExtractImpureCircuits>, + NonFungibleTokenPrivateState + > { + if (!this._impureCircuitProxy) { + this._impureCircuitProxy = this.createImpureCircuitProxy< + MockNonFungibleToken['impureCircuits'] + >( + this.contract.impureCircuits, + () => this.getCallerContext(), + (ctx: any) => { + this.circuitContext = ctx; + }, + ); + } + return this._impureCircuitProxy; } /** - * @description Retrieves the current private state of the contract. - * @returns The private state of type NonFungibleTokenPrivateState. + * @description Sets the caller context. + * @param caller The caller in context of the proceeding circuit calls. */ - public getCurrentPrivateState(): NonFungibleTokenPrivateState { - return this.circuitContext.currentPrivateState; + public setCaller(caller: CoinPublicKey | null): void { + this.callerOverride = caller; } /** - * @description Retrieves the current contract state. - * @returns The contract state object. + * @description Resets the cached circuit proxy instances. + * This is useful if the underlying contract state or circuit context has changed, + * and you want to ensure the proxies are recreated with updated context on next access. */ - public getCurrentContractState(): ContractState { - return this.circuitContext.originalState; + public resetCircuitProxies(): void { + this._pureCircuitProxy = undefined; + this._impureCircuitProxy = undefined; + } + + /** + * @description Helper method that provides access to both pure and impure circuit proxies. + * These proxies automatically inject the appropriate circuit context when invoked. + * @returns An object containing `pure` and `impure` circuit proxy interfaces. + */ + public get circuits() { + return { + pure: this.pureCircuit, + impure: this.impureCircuit, + }; + } + + /** + * @description Retrieves the current public ledger state of the contract. + * @returns The ledger state as defined by the contract. + */ + public getPublicState(): Ledger { + return ledger(this.circuitContext.transactionContext.state); } /** @@ -96,7 +179,7 @@ export class NonFungibleTokenSimulator * @returns The token name. */ public name(): string { - return this.contract.impureCircuits.name(this.circuitContext).result; + return this.circuits.impure.name(); } /** @@ -104,7 +187,7 @@ export class NonFungibleTokenSimulator * @returns The token name. */ public symbol(): string { - return this.contract.impureCircuits.symbol(this.circuitContext).result; + return this.circuits.impure.symbol(); } /** @@ -115,8 +198,7 @@ export class NonFungibleTokenSimulator public balanceOf( account: Either, ): bigint { - return this.contract.impureCircuits.balanceOf(this.circuitContext, account) - .result; + return this.circuits.impure.balanceOf(account); } /** @@ -125,8 +207,7 @@ export class NonFungibleTokenSimulator * @return The public key that owns the token. */ public ownerOf(tokenId: bigint): Either { - return this.contract.impureCircuits.ownerOf(this.circuitContext, tokenId) - .result; + return this.circuits.impure.ownerOf(tokenId); } /** @@ -140,8 +221,7 @@ export class NonFungibleTokenSimulator * @returns The token id's URI. */ public tokenURI(tokenId: bigint): string { - return this.contract.impureCircuits.tokenURI(this.circuitContext, tokenId) - .result; + return this.circuits.impure.tokenURI(tokenId); } /** @@ -161,20 +241,8 @@ export class NonFungibleTokenSimulator public approve( to: Either, tokenId: bigint, - sender?: CoinPublicKey, ) { - const res = this.contract.impureCircuits.approve( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - to, - tokenId, - ); - - this.circuitContext = res.context; + return this.circuits.impure.approve(to, tokenId); } /** @@ -185,10 +253,7 @@ export class NonFungibleTokenSimulator public getApproved( tokenId: bigint, ): Either { - return this.contract.impureCircuits.getApproved( - this.circuitContext, - tokenId, - ).result; + return this.circuits.impure.getApproved(tokenId); } /** @@ -205,20 +270,8 @@ export class NonFungibleTokenSimulator public setApprovalForAll( operator: Either, approved: boolean, - sender?: CoinPublicKey, ) { - const res = this.contract.impureCircuits.setApprovalForAll( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - operator, - approved, - ); - - this.circuitContext = res.context; + return this.circuits.impure.setApprovalForAll(operator, approved); } /** @@ -232,11 +285,7 @@ export class NonFungibleTokenSimulator owner: Either, operator: Either, ): boolean { - return this.contract.impureCircuits.isApprovedForAll( - this.circuitContext, - owner, - operator, - ).result; + return this.circuits.impure.isApprovedForAll(owner, operator); } /** @@ -257,21 +306,8 @@ export class NonFungibleTokenSimulator from: Either, to: Either, tokenId: bigint, - sender?: CoinPublicKey, ) { - const res = this.contract.impureCircuits.transferFrom( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - from, - to, - tokenId, - ); - - this.circuitContext = res.context; + return this.circuits.impure.transferFrom(from, to, tokenId); } /** @@ -286,10 +322,7 @@ export class NonFungibleTokenSimulator public _requireOwned( tokenId: bigint, ): Either { - return this.contract.impureCircuits._requireOwned( - this.circuitContext, - tokenId, - ).result; + return this.circuits.impure._requireOwned(tokenId); } /** @@ -301,8 +334,7 @@ export class NonFungibleTokenSimulator public _ownerOf( tokenId: bigint, ): Either { - return this.contract.impureCircuits._ownerOf(this.circuitContext, tokenId) - .result; + return this.circuits.impure._ownerOf(tokenId); } /** @@ -320,12 +352,7 @@ export class NonFungibleTokenSimulator tokenId: bigint, auth: Either, ) { - this.circuitContext = this.contract.impureCircuits._approve( - this.circuitContext, - to, - tokenId, - auth, - ).context; + return this.circuits.impure._approve(to, tokenId, auth); } /** @@ -346,12 +373,7 @@ export class NonFungibleTokenSimulator spender: Either, tokenId: bigint, ) { - this.circuitContext = this.contract.impureCircuits._checkAuthorized( - this.circuitContext, - owner, - spender, - tokenId, - ).context; + return this.circuits.impure._checkAuthorized(owner, spender, tokenId); } /** @@ -371,12 +393,7 @@ export class NonFungibleTokenSimulator spender: Either, tokenId: bigint, ): boolean { - return this.contract.impureCircuits._isAuthorized( - this.circuitContext, - owner, - spender, - tokenId, - ).result; + return this.circuits.impure._isAuthorized(owner, spender, tokenId); } /** @@ -388,10 +405,7 @@ export class NonFungibleTokenSimulator public _getApproved( tokenId: bigint, ): Either { - return this.contract.impureCircuits._getApproved( - this.circuitContext, - tokenId, - ).result; + return this.circuits.impure._getApproved(tokenId); } /** @@ -410,12 +424,7 @@ export class NonFungibleTokenSimulator operator: Either, approved: boolean, ) { - this.circuitContext = this.contract.impureCircuits._setApprovalForAll( - this.circuitContext, - owner, - operator, - approved, - ).context; + return this.circuits.impure._setApprovalForAll(owner, operator, approved); } /** @@ -433,11 +442,7 @@ export class NonFungibleTokenSimulator to: Either, tokenId: bigint, ) { - this.circuitContext = this.contract.impureCircuits._mint( - this.circuitContext, - to, - tokenId, - ).context; + return this.circuits.impure._mint(to, tokenId); } /** @@ -452,10 +457,7 @@ export class NonFungibleTokenSimulator * @param tokenId The token to burn */ public _burn(tokenId: bigint) { - this.circuitContext = this.contract.impureCircuits._burn( - this.circuitContext, - tokenId, - ).context; + return this.circuits.impure._burn(tokenId); } /** @@ -476,12 +478,7 @@ export class NonFungibleTokenSimulator to: Either, tokenId: bigint, ) { - this.circuitContext = this.contract.impureCircuits._transfer( - this.circuitContext, - from, - to, - tokenId, - ).context; + return this.circuits.impure._transfer(from, to, tokenId); } /** @@ -494,11 +491,7 @@ export class NonFungibleTokenSimulator * @param tokenURI The URI of `tokenId`. */ public _setTokenURI(tokenId: bigint, tokenURI: string) { - this.circuitContext = this.contract.impureCircuits._setTokenURI( - this.circuitContext, - tokenId, - tokenURI, - ).context; + return this.circuits.impure._setTokenURI(tokenId, tokenURI); } /** @@ -523,21 +516,8 @@ export class NonFungibleTokenSimulator from: Either, to: Either, tokenId: bigint, - sender?: CoinPublicKey, ) { - const res = this.contract.impureCircuits._unsafeTransferFrom( - { - ...this.circuitContext, - currentZswapLocalState: sender - ? emptyZswapLocalState(sender) - : this.circuitContext.currentZswapLocalState, - }, - from, - to, - tokenId, - ); - - this.circuitContext = res.context; + return this.circuits.impure._unsafeTransferFrom(from, to, tokenId); } /** @@ -563,12 +543,7 @@ export class NonFungibleTokenSimulator to: Either, tokenId: bigint, ) { - this.circuitContext = this.contract.impureCircuits._unsafeTransfer( - this.circuitContext, - from, - to, - tokenId, - ).context; + return this.circuits.impure._unsafeTransfer(from, to, tokenId); } /** @@ -590,10 +565,6 @@ export class NonFungibleTokenSimulator to: Either, tokenId: bigint, ) { - this.circuitContext = this.contract.impureCircuits._unsafeMint( - this.circuitContext, - to, - tokenId, - ).context; + return this.circuits.impure._unsafeMint(to, tokenId); } } diff --git a/contracts/nonFungibleToken/src/test/types/test.ts b/contracts/nonFungibleToken/src/test/types/test.ts deleted file mode 100644 index 7a909543..00000000 --- a/contracts/nonFungibleToken/src/test/types/test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { - CircuitContext, - ContractState, -} from '@midnight-ntwrk/compact-runtime'; - -/** - * Generic interface for mock contract implementations. - * @template P - The type of the contract's private state. - * @template L - The type of the contract's ledger (public state). - */ -export interface IContractSimulator { - /** The contract's deployed address. */ - readonly contractAddress: string; - - /** The current circuit context. */ - circuitContext: CircuitContext

; - - /** Retrieves the current ledger state. */ - getCurrentPublicState(): L; - - /** Retrieves the current private state. */ - getCurrentPrivateState(): P; - - /** Retrieves the current contract state. */ - getCurrentContractState(): ContractState; -} diff --git a/package.json b/package.json index 0a293a35..a5221012 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "private": true, "packageManager": "yarn@4.1.0", "workspaces": [ - "compact/", - "contracts/*/", - "docs/" + "docs/", + "contracts/*", + "packages/*", + "packages/contracts/*" ], "scripts": { "docs": "turbo run docs --filter=docs", diff --git a/packages/compact-std/package.json b/packages/compact-std/package.json new file mode 100644 index 00000000..dc17230d --- /dev/null +++ b/packages/compact-std/package.json @@ -0,0 +1,31 @@ +{ + "name": "@openzeppelin-compact/compact-std", + "private": true, + "version": "0.0.1", + "description": "", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "compact": "compact-compiler", + "build": "compact-builder && tsc", + "test": "vitest run", + "types": "tsc -p tsconfig.json --noEmit", + "clean": "git clean -fXd" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@types/node": "22.14.0", + "fast-check": "^3.15.0", + "typescript": "^5.8.3", + "vitest": "^3.1.4" + }, + "dependencies": { + "@midnight-ntwrk/compact-runtime": "^0.8.1", + "@midnight-ntwrk/midnight-js-network-id": "^2.0.1", + "@midnight-ntwrk/zswap": "^4.0.0", + "@openzeppelin-compact/compact": "workspace:^" + } +} diff --git a/packages/compact-std/src/Index.compact b/packages/compact-std/src/Index.compact new file mode 100644 index 00000000..d7c93b0b --- /dev/null +++ b/packages/compact-std/src/Index.compact @@ -0,0 +1,20 @@ +pragma language_version >= 0.14.0; + +import CompactStandardLibrary; + +/** + * @description Standard structs from CompactStandardLibrary for use in contracts and TypeScript. + */ +export { + Maybe, // Encapsulates an optionally present value + Either, // Disjoint union of two types + CurvePoint, // Point on the proof system's embedded curve + MerkleTreeDigest, // Root hash of a Merkle tree + MerkleTreePathEntry,// Entry in a Merkle tree path + MerkleTreePath, // Path in a Merkle tree leading to a leaf + ContractAddress, // Address of a contract + CoinInfo, // Description of a newly created coin + QualifiedCoinInfo, // Description of an existing coin in the ledger + ZswapCoinPublicKey, // Public key for coin outputs + SendResult // Result of send/send_immediate operations +}; diff --git a/packages/compact-std/src/index.test.ts b/packages/compact-std/src/index.test.ts new file mode 100644 index 00000000..c326de19 --- /dev/null +++ b/packages/compact-std/src/index.test.ts @@ -0,0 +1,129 @@ +import { describe, expectTypeOf, it } from 'vitest'; +import type { + CoinInfo, + ContractAddress, + CurvePoint, + Either, + Maybe, + MerkleTreeDigest, + MerkleTreePath, + MerkleTreePathEntry, + QualifiedCoinInfo, + SendResult, + ZswapCoinPublicKey, +} from './index'; + +describe('@midnight-dapps/compact-std', () => { + it('should export Maybe type correctly', () => { + const maybeNumber: Maybe = { is_some: true, value: 42 }; + expectTypeOf(maybeNumber).toEqualTypeOf<{ + is_some: boolean; + value: number; + }>(); + }); + + it('should export Either type correctly', () => { + const eitherStringNumber: Either = { + is_left: true, + left: 'test', + right: 0, + }; + expectTypeOf(eitherStringNumber).toEqualTypeOf<{ + is_left: boolean; + left: string; + right: number; + }>(); + }); + + it('should export CurvePoint type correctly', () => { + const curvePoint: CurvePoint = { x: BigInt(1), y: BigInt(2) }; + expectTypeOf(curvePoint).toEqualTypeOf<{ x: bigint; y: bigint }>(); + }); + + it('should export MerkleTreeDigest type correctly', () => { + const digest: MerkleTreeDigest = { field: BigInt(123) }; + expectTypeOf(digest).toEqualTypeOf<{ field: bigint }>(); + }); + + it('should export MerkleTreePathEntry type correctly', () => { + const entry: MerkleTreePathEntry = { + sibling: { field: BigInt(456) }, + goes_left: false, + }; + expectTypeOf(entry).toEqualTypeOf<{ + sibling: { field: bigint }; + goes_left: boolean; + }>(); + }); + + it('should export MerkleTreePath type correctly', () => { + const path: MerkleTreePath = { + leaf: new Uint8Array([1, 2, 3]), + path: [{ sibling: { field: BigInt(789) }, goes_left: true }], + }; + expectTypeOf(path).toEqualTypeOf<{ + leaf: Uint8Array; + path: { sibling: { field: bigint }; goes_left: boolean }[]; + }>(); + }); + + it('should export ContractAddress type correctly', () => { + const address: ContractAddress = { bytes: new Uint8Array(32) }; + expectTypeOf(address).toEqualTypeOf<{ bytes: Uint8Array }>(); + }); + + it('should export CoinInfo type correctly', () => { + const coin: CoinInfo = { + nonce: new Uint8Array(32), + color: new Uint8Array(32), + value: BigInt(100), + }; + expectTypeOf(coin).toEqualTypeOf<{ + nonce: Uint8Array; + color: Uint8Array; + value: bigint; + }>(); + }); + + it('should export QualifiedCoinInfo type correctly', () => { + const qualifiedCoin: QualifiedCoinInfo = { + nonce: new Uint8Array(32), + color: new Uint8Array(32), + value: BigInt(200), + mt_index: BigInt(1), + }; + expectTypeOf(qualifiedCoin).toEqualTypeOf<{ + nonce: Uint8Array; + color: Uint8Array; + value: bigint; + mt_index: bigint; + }>(); + }); + + it('should export ZswapCoinPublicKey type correctly', () => { + const pubKey: ZswapCoinPublicKey = { bytes: new Uint8Array(32) }; + expectTypeOf(pubKey).toEqualTypeOf<{ bytes: Uint8Array }>(); + }); + + it('should export SendResult type correctly', () => { + const result: SendResult = { + change: { + is_some: false, + value: { + nonce: new Uint8Array(32), + color: new Uint8Array(32), + value: BigInt(0), + }, + }, + sent: { + nonce: new Uint8Array(32), + color: new Uint8Array(32), + value: BigInt(50), + }, + }; + expectTypeOf(result).toEqualTypeOf<{ + change: Maybe; + sent: CoinInfo; + }>(); + }); +}); diff --git a/packages/compact-std/src/index.ts b/packages/compact-std/src/index.ts new file mode 100644 index 00000000..04ee005d --- /dev/null +++ b/packages/compact-std/src/index.ts @@ -0,0 +1,18 @@ +/** + * @module @midnight-dapps/compact-stdlib + * @description Re-exports custom structs from CompactStandardLibrary for use in TypeScript code. + * Excludes standard runtime types from @midnight-ntwrk/compact-runtime. + */ +export type { + CoinInfo, + ContractAddress, + CurvePoint, + Either, + Maybe, + MerkleTreeDigest, + MerkleTreePath, + MerkleTreePathEntry, + QualifiedCoinInfo, + SendResult, + ZswapCoinPublicKey, +} from './artifacts/Index/contract/index.cjs'; diff --git a/packages/compact-std/tsconfig.build.json b/packages/compact-std/tsconfig.build.json new file mode 100644 index 00000000..94e9dab8 --- /dev/null +++ b/packages/compact-std/tsconfig.build.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "src/test/**/*.ts" + ], + "compilerOptions": {} +} diff --git a/packages/compact-std/tsconfig.json b/packages/compact-std/tsconfig.json new file mode 100644 index 00000000..2ecae56b --- /dev/null +++ b/packages/compact-std/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node", + "declaration": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/artifacts"] +} diff --git a/packages/compact-std/vitest.config.ts b/packages/compact-std/vitest.config.ts new file mode 100644 index 00000000..aef1790c --- /dev/null +++ b/packages/compact-std/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + globals: true, + include: ['**/*.test.ts'], + hookTimeout: 100000, + }, +}); diff --git a/compact/package.json b/packages/compact/package.json similarity index 100% rename from compact/package.json rename to packages/compact/package.json diff --git a/compact/src/Builder.ts b/packages/compact/src/Builder.ts similarity index 100% rename from compact/src/Builder.ts rename to packages/compact/src/Builder.ts diff --git a/compact/src/Compiler.ts b/packages/compact/src/Compiler.ts similarity index 100% rename from compact/src/Compiler.ts rename to packages/compact/src/Compiler.ts diff --git a/compact/src/runBuilder.ts b/packages/compact/src/runBuilder.ts similarity index 100% rename from compact/src/runBuilder.ts rename to packages/compact/src/runBuilder.ts diff --git a/compact/src/runCompiler.ts b/packages/compact/src/runCompiler.ts similarity index 100% rename from compact/src/runCompiler.ts rename to packages/compact/src/runCompiler.ts diff --git a/compact/src/types/errors.ts b/packages/compact/src/types/errors.ts similarity index 100% rename from compact/src/types/errors.ts rename to packages/compact/src/types/errors.ts diff --git a/compact/tsconfig.json b/packages/compact/tsconfig.json similarity index 100% rename from compact/tsconfig.json rename to packages/compact/tsconfig.json diff --git a/compact/turbo.json b/packages/compact/turbo.json similarity index 100% rename from compact/turbo.json rename to packages/compact/turbo.json diff --git a/packages/testing/package.json b/packages/testing/package.json new file mode 100644 index 00000000..270e4a67 --- /dev/null +++ b/packages/testing/package.json @@ -0,0 +1,27 @@ +{ + "name": "@openzeppelin-compact/testing", + "private": true, + "version": "0.0.1", + "description": "", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "compact-builder && tsc", + "types": "tsc -p tsconfig.json --noEmit", + "clean": "git clean -fXd" + }, + "dependencies": { + "@openzeppelin-compact/compact": "workspace:^" + }, + "devDependencies": { + "@openzeppelin-compact/compact-std": "workspace:^", + "@types/node": "22.14.0", + "ts-node": "^10.9.2", + "typescript": "^5.2.2", + "vitest": "^3.1.3" + } +} diff --git a/packages/testing/src/AbstractContractSimulator.ts b/packages/testing/src/AbstractContractSimulator.ts new file mode 100644 index 00000000..4c1bd254 --- /dev/null +++ b/packages/testing/src/AbstractContractSimulator.ts @@ -0,0 +1,131 @@ +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; +import type { ContextlessCircuits, IContractSimulator } from './types.js'; + +/** + * Abstract base class for simulating contract behavior. + * Provides common functionality for managing circuit contexts and creating proxies + * for pure and impure circuit functions. + * + * @template P - The type representing the private state of the contract. + * @template L - The type representing the public ledger (contract) state. + */ +export abstract class AbstractContractSimulator + implements IContractSimulator +{ + /** + * The deployed contract's address. + * Must be implemented by concrete subclasses. + */ + abstract readonly contractAddress: string; + + /** + * The current circuit context containing private state, contract state, and transaction context. + * Must be implemented by concrete subclasses. + */ + abstract circuitContext: CircuitContext

; + + /** + * Retrieves the current public ledger state of the contract. + * Must be implemented by concrete subclasses. + * + * @returns The current public ledger state. + */ + abstract getPublicState(): L; + + /** + * Retrieves the current private state from the circuit context. + * + * @returns The current private state of the contract. + */ + public getPrivateState(): P { + return this.circuitContext.currentPrivateState; + } + + /** + * Retrieves the original contract state from the circuit context. + * + * @returns The original contract state. + */ + public getContractState(): ContractState { + return this.circuitContext.originalState; + } + + /** + * Creates a proxy wrapper around pure circuits. + * Pure circuits do not modify contract state, so only the result is returned. + * + * @template Circuits - The type of the circuits object to proxy. + * @param circuits - The original circuits object containing functions accepting a CircuitContext. + * @param context - A function returning the current CircuitContext to pass to circuit functions. + * @returns A proxy with contextless circuits that accept the original arguments and return only results. + */ + protected createPureCircuitProxy( + circuits: Circuits, + context: () => CircuitContext

, + ): ContextlessCircuits { + return new Proxy(circuits, { + get(target, prop, receiver) { + const original = Reflect.get(target, prop, receiver); + + if (typeof original !== 'function') return original; + + return (...args: any[]) => { + const ctx = context(); + + const fn = original as ( + ctx: CircuitContext

, + ...args: any[] + ) => { result: any }; + + return fn(ctx, ...args).result; + }; + }, + }) as ContextlessCircuits; + } + + /** + * Creates a proxy wrapper around impure circuits. + * Impure circuits can modify contract state, so the circuit context is updated accordingly. + * + * @template Circuits - The type of the circuits object to proxy. + * @param circuits - The original circuits object containing functions accepting a CircuitContext. + * @param context - A function returning the current CircuitContext to pass to circuit functions. + * @param updateContext - A callback to update the circuit context with the new context returned by the circuit. + * @returns A proxy with contextless circuits that accept the original arguments, update context, and return results. + */ + protected createImpureCircuitProxy( + circuits: Circuits, + context: () => CircuitContext

, + updateContext: (ctx: CircuitContext

) => void, + ): ContextlessCircuits { + return new Proxy(circuits, { + get(target, prop, receiver) { + const original = Reflect.get(target, prop, receiver); + + if (typeof original !== 'function') return original; + + return (...args: any[]) => { + const ctx = context(); + + const fn = original as ( + ctx: CircuitContext

, + ...args: any[] + ) => { result: any; context: CircuitContext

}; + + const { result, context: newCtx } = fn(ctx, ...args); + updateContext(newCtx); + return result; + }; + }, + }) as ContextlessCircuits; + } + + /** + * Optional method to reset any cached circuit proxies. + * Concrete subclasses can override this to clear proxies if needed. + */ + public resetCircuitProxies?(): void {} +} diff --git a/contracts/nonFungibleToken/src/test/utils/address.ts b/packages/testing/src/address.ts similarity index 66% rename from contracts/nonFungibleToken/src/test/utils/address.ts rename to packages/testing/src/address.ts index af4ed548..3596d6cc 100644 --- a/contracts/nonFungibleToken/src/test/utils/address.ts +++ b/packages/testing/src/address.ts @@ -3,7 +3,10 @@ import { encodeCoinPublicKey, } from '@midnight-ntwrk/compact-runtime'; import { encodeContractAddress } from '@midnight-ntwrk/ledger'; -import type * as Compact from '../../artifacts/MockNonFungibleToken/contract/index.cjs'; +import type { + ContractAddress, + ZswapCoinPublicKey, +} from '@openzeppelin-compact/compact-std'; const PREFIX_ADDRESS = '0200'; @@ -23,10 +26,9 @@ export const toHexPadded = (str: string, len = 64) => * @param str String to hexify and encode. * @returns Encoded `ZswapCoinPublicKey`. */ -export const encodeToPK = (str: string): Compact.ZswapCoinPublicKey => { - const toHex = Buffer.from(str, 'ascii').toString('hex'); - return { bytes: encodeCoinPublicKey(String(toHex).padStart(64, '0')) }; -}; +export const encodeToPK = (str: string): ZswapCoinPublicKey => ({ + bytes: encodeCoinPublicKey(toHexPadded(str)), +}); /** * @description Generates ContractAddress from `str` for testing purposes. @@ -34,11 +36,9 @@ export const encodeToPK = (str: string): Compact.ZswapCoinPublicKey => { * @param str String to hexify and encode. * @returns Encoded `ZswapCoinPublicKey`. */ -export const encodeToAddress = (str: string): Compact.ContractAddress => { - const toHex = Buffer.from(str, 'ascii').toString('hex'); - const fullAddress = PREFIX_ADDRESS + String(toHex).padStart(64, '0'); - return { bytes: encodeContractAddress(fullAddress) }; -}; +export const encodeToAddress = (str: string): ContractAddress => ({ + bytes: encodeContractAddress(PREFIX_ADDRESS + toHexPadded(str)), +}); /** * @description Generates an Either object for ZswapCoinPublicKey for testing. @@ -46,13 +46,11 @@ export const encodeToAddress = (str: string): Compact.ContractAddress => { * @param str String to hexify and encode. * @returns Defined Either object for ZswapCoinPublicKey. */ -export const createEitherTestUser = (str: string) => { - return { - is_left: true, - left: encodeToPK(str), - right: encodeToAddress(''), - }; -}; +export const createEitherTestUser = (str: string) => ({ + is_left: true, + left: encodeToPK(str), + right: encodeToAddress(''), +}); /** * @description Generates an Either object for ContractAddress for testing. @@ -60,22 +58,23 @@ export const createEitherTestUser = (str: string) => { * @param str String to hexify and encode. * @returns Defined Either object for ContractAddress. */ -export const createEitherTestContractAddress = (str: string) => { - return { - is_left: false, - left: encodeToPK(''), - right: encodeToAddress(str), - }; -}; +export const createEitherTestContractAddress = (str: string) => ({ + is_left: false, + left: encodeToPK(''), + right: encodeToAddress(str), +}); + +const zeroUint8Array = (length = 32) => + convert_bigint_to_Uint8Array(length, 0n); export const ZERO_KEY = { is_left: true, - left: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, + left: { bytes: zeroUint8Array() }, right: encodeToAddress(''), }; export const ZERO_ADDRESS = { is_left: false, left: encodeToPK(''), - right: { bytes: convert_bigint_to_Uint8Array(32, BigInt(0)) }, + right: { bytes: zeroUint8Array() }, }; diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts new file mode 100644 index 00000000..7a20db16 --- /dev/null +++ b/packages/testing/src/index.ts @@ -0,0 +1,15 @@ +// biome-ignore lint/performance/noBarrelFile: Centralized exports are intentional; package is small and used internally +export { AbstractContractSimulator } from './AbstractContractSimulator.js'; +export { + createEitherTestContractAddress, + createEitherTestUser, + toHexPadded, + ZERO_ADDRESS, + ZERO_KEY, +} from './address'; +export type { + ContextlessCircuits, + ExtractImpureCircuits, + ExtractPureCircuits, + IContractSimulator, +} from './types.js'; diff --git a/packages/testing/src/types.ts b/packages/testing/src/types.ts new file mode 100644 index 00000000..c5511715 --- /dev/null +++ b/packages/testing/src/types.ts @@ -0,0 +1,95 @@ +import type { + CircuitContext, + ContractState, +} from '@midnight-ntwrk/compact-runtime'; + +/** + * Interface defining a generic contract simulator. + * + * @template P - Type representing the private contract state. + * @template L - Type representing the public ledger state. + */ +export interface IContractSimulator { + /** + * The deployed contract's address. + */ + readonly contractAddress: string; + + /** + * The current circuit context holding the contract state. + */ + circuitContext: CircuitContext

; + + /** + * Returns the current public ledger state. + * + * @returns The current ledger state of type L. + */ + getPublicState(): L; + + /** + * Returns the current private contract state. + * + * @returns The current private state of type P. + */ + getPrivateState(): P; + + /** + * Returns the original contract state. + * + * @returns The current contract state. + */ + getContractState(): ContractState; +} + +/** + * Extracts pure circuits from a contract type. + * + * Pure circuits are those in `circuits` but not in `impureCircuits`. + * + * @template TContract - Contract type with `circuits` and `impureCircuits`. + */ +export type ExtractPureCircuits = TContract extends { + circuits: infer TCircuits; + impureCircuits: infer TImpureCircuits; +} + ? Omit + : never; + +/** + * Extracts impure circuits from a contract type. + * + * Impure circuits are those in `impureCircuits`. + * + * @template TContract - Contract type with `circuits` and `impureCircuits`. + */ +export type ExtractImpureCircuits = TContract extends { + impureCircuits: infer TImpureCircuits; +} + ? TImpureCircuits + : never; + +/** + * Transforms a collection of circuit functions by removing the explicit `CircuitContext` parameter, + * producing a version of each function that can be called without passing the context explicitly. + * + * Each original circuit function is expected to have the signature: + * `(ctx: CircuitContext, ...args) => { result: R; context: CircuitContext }` + * or a compatible shape. + * + * The transformed type maps each key `K` of the input `Circuits` type to a new function + * that takes the same parameters as the original, *except* the first `CircuitContext` argument, + * and returns the `result` part `R` directly. + * + * @template Circuits - An object type whose values are circuit functions accepting a `CircuitContext` + * and returning an object with `result` and optionally `context`. + * @template TState - The type representing the private or contract state passed inside `CircuitContext`. + */ +export type ContextlessCircuits = { + [K in keyof Circuits]: Circuits[K] extends ( + ctx: CircuitContext, + ...args: infer P + ) => { result: infer R; context: CircuitContext } + ? (...args: P) => R + : never; +}; diff --git a/packages/testing/tsconfig.build.json b/packages/testing/tsconfig.build.json new file mode 100644 index 00000000..f1132509 --- /dev/null +++ b/packages/testing/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["src/test/**/*.ts"], + "compilerOptions": {} +} diff --git a/packages/testing/tsconfig.json b/packages/testing/tsconfig.json new file mode 100644 index 00000000..3e90b0a9 --- /dev/null +++ b/packages/testing/tsconfig.json @@ -0,0 +1,21 @@ +{ + "include": ["src/**/*.ts"], + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "lib": ["ESNext"], + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strict": true, + "isolatedModules": true, + "sourceMap": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/yarn.lock b/yarn.lock index bfe8e4de..bc8a39bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -359,6 +359,13 @@ __metadata: languageName: node linkType: hard +"@midnight-ntwrk/midnight-js-network-id@npm:^2.0.1": + version: 2.0.2 + resolution: "@midnight-ntwrk/midnight-js-network-id@npm:2.0.2" + checksum: 10/116399be0e0038e7acfb8d7172c207fd23d54eff66526a6f6ca65ce26402cb2ceb3ad6f888cb02b46acb1cf0662a2f933bb71e7a014026b05b83909916af9134 + languageName: node + linkType: hard + "@midnight-ntwrk/onchain-runtime@npm:^0.3.0": version: 0.3.0 resolution: "@midnight-ntwrk/onchain-runtime@npm:0.3.0" @@ -419,9 +426,24 @@ __metadata: languageName: unknown linkType: soft -"@openzeppelin-compact/compact@workspace:^, @openzeppelin-compact/compact@workspace:compact": +"@openzeppelin-compact/compact-std@workspace:^, @openzeppelin-compact/compact-std@workspace:packages/compact-std": + version: 0.0.0-use.local + resolution: "@openzeppelin-compact/compact-std@workspace:packages/compact-std" + dependencies: + "@midnight-ntwrk/compact-runtime": "npm:^0.8.1" + "@midnight-ntwrk/midnight-js-network-id": "npm:^2.0.1" + "@midnight-ntwrk/zswap": "npm:^4.0.0" + "@openzeppelin-compact/compact": "workspace:^" + "@types/node": "npm:22.14.0" + fast-check: "npm:^3.15.0" + typescript: "npm:^5.8.3" + vitest: "npm:^3.1.4" + languageName: unknown + linkType: soft + +"@openzeppelin-compact/compact@workspace:^, @openzeppelin-compact/compact@workspace:packages/compact": version: 0.0.0-use.local - resolution: "@openzeppelin-compact/compact@workspace:compact" + resolution: "@openzeppelin-compact/compact@workspace:packages/compact" dependencies: "@types/node": "npm:22.14.0" chalk: "npm:^5.4.1" @@ -464,6 +486,8 @@ __metadata: resolution: "@openzeppelin-compact/non-fungible-token@workspace:contracts/nonFungibleToken" dependencies: "@openzeppelin-compact/compact": "workspace:^" + "@openzeppelin-compact/compact-std": "workspace:^" + "@openzeppelin-compact/testing": "workspace:^" "@types/node": "npm:22.14.0" ts-node: "npm:^10.9.2" typescript: "npm:^5.2.2" @@ -483,6 +507,19 @@ __metadata: languageName: unknown linkType: soft +"@openzeppelin-compact/testing@workspace:^, @openzeppelin-compact/testing@workspace:packages/testing": + version: 0.0.0-use.local + resolution: "@openzeppelin-compact/testing@workspace:packages/testing" + dependencies: + "@openzeppelin-compact/compact": "workspace:^" + "@openzeppelin-compact/compact-std": "workspace:^" + "@types/node": "npm:22.14.0" + ts-node: "npm:^10.9.2" + typescript: "npm:^5.2.2" + vitest: "npm:^3.1.3" + languageName: unknown + linkType: soft + "@openzeppelin-compact/utils@workspace:contracts/utils": version: 0.0.0-use.local resolution: "@openzeppelin-compact/utils@workspace:contracts/utils" @@ -2507,7 +2544,7 @@ __metadata: languageName: node linkType: hard -"typescript@npm:^5.2.2, typescript@npm:^5.8.2": +"typescript@npm:^5.2.2, typescript@npm:^5.8.2, typescript@npm:^5.8.3": version: 5.8.3 resolution: "typescript@npm:5.8.3" bin: @@ -2517,7 +2554,7 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin": +"typescript@patch:typescript@npm%3A^5.2.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.2#optional!builtin, typescript@patch:typescript@npm%3A^5.8.3#optional!builtin": version: 5.8.3 resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=d69c25" bin: @@ -2629,7 +2666,7 @@ __metadata: languageName: node linkType: hard -"vitest@npm:^3.1.3": +"vitest@npm:^3.1.3, vitest@npm:^3.1.4": version: 3.2.4 resolution: "vitest@npm:3.2.4" dependencies: