TypeScript Implementation
Witnesses are declared in Compact but implemented in TypeScript. The implementation lives in a separate TypeScript file, not in your .compact file.
Workflow
Step 1: Declare the Witness in Compact
In your .compact file, declare what the witness looks like:
pragma language_version >= 0.16;
import CompactStandardLibrary;
// Declaration only - no body
witness getSecretKey(): Bytes<32>;
export ledger owner: Bytes<32>;
export circuit setOwner(): [] {
const key = getSecretKey();
owner = disclose(key);
}
Step 2: Compile
compact compile my-contract.compact managed/my-contract
This generates TypeScript types in managed/my-contract/.
Step 3: Implement in TypeScript
Create a .ts file with the actual implementation:
export type MyPrivateState = {
secretKey: Uint8Array;
};
export const witnesses = {
getSecretKey: ({ privateState }) => {
return [privateState, privateState.secretKey];
},
};
Step 4: Use in Your DApp
import { witnesses, MyPrivateState } from "./witnesses";
const initialPrivateState: MyPrivateState = {
secretKey: new Uint8Array(32),
};
Project Structure
your-project/
├── contracts/
│ └── my-contract.compact # Step 1: Compact code
├── contracts/managed/
│ └── my-contract/ # Step 2: Generated files (after compile)
│ ├── contract/
│ └── ...
└── src/
└── witnesses.ts # Step 3: Your TypeScript implementation
Source: Create MN App
Note: The Compact compiler only checks
.compactsyntax. To test the full witness flow, you need a complete DApp with the Midnight runtime.
Generated API
When you compile, the TypeScript target generates a Witnesses<T> type. Each witness becomes a function:
type Witness<PS> = (
context: WitnessContext<any, PS>,
...rest: any[]
) => [PS, any];
Source: Witness type
WitnessContext provides:
ledger- Current ledger stateprivateState- Your private statecontractAddress- The contract’s address
Source: WitnessContext
Example from BBoard DApp
Compact:
witness localSecretKey(): Bytes<32>;
TypeScript:
export type BBoardPrivateState = {
secretKey: Uint8Array;
};
export const witnesses = createBBoardWitnesses<BBoardPrivateState>({
localSecretKey: ({ privateState }) => {
return [privateState, privateState.secretKey];
},
});
Source: BBoard DApp
Key Points
- First argument is
WitnessContext<Ledger, PrivateState> - Return
[newPrivateState, result] - Don’t mutate
privateState- return updated state
Contracts Without Witnesses
If your contract has no witnesses, provide an empty object:
export type CounterPrivateState = {
privateCounter: number;
};
export const witnesses = {};
Source: Counter DApp
How the Runtime Uses Witnesses
When you instantiate the contract, you pass your witnesses. The runtime:
- Calls the witness function when Compact code invokes it
- Passes current
privateStateviacontext.privateState - Receives
[updatedPrivateState, value]back - Threads the updated state through subsequent calls
Source: TypeScript target
Next Steps
Now that you understand witnesses, learn about Control Flow.