This project generates unsigned Mina zkApp transactions as JSON files for external signing and deployment.
mina-tx-generator/
├── src/
│ ├── commit.ts # Update transaction fields
│ ├── payment.ts # Simple payment transaction
│ ├── update_state.ts # zkApp state modification
│ ├── update_permissions.ts # zkApp permissions update
│ └── update_verification_key.ts # zkApp verification key update
├── tx-json/ # Output directory for unsigned transactions
├── package.json
└── tsconfig.json
{
"dependencies": {
"o1js": "^1.4.1"
},
"devDependencies": {
"@types/node": "^20.4.2",
"ts-node": "^10.9.2",
"typescript": "^5.2.2"
}
}tsconfig.json requirements:
{
"compilerOptions": {
"target": "es2021",
"module": "nodenext",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}npm run payment # Generate payment transaction
npm run update-state # Generate state update transactions
npm run update-permissions # Generate permissions update transactions
npm run update-vk # Generate verification key update transactions| Script | Output Files |
|---|---|
update-state |
tx-json/deploy-state-contract.json, tx-json/update-state-transaction.json |
update-permissions |
tx-json/deploy-permissions-contract.json, tx-json/update-permissions-transaction.json |
update-vk |
tx-json/deploy-updatable-contract.json, tx-json/update-verification-key-transaction.json |
payment |
tx-json/payment-transaction.json |
Each JSON file contains:
feePayer: object withpublicKeyfield (signature is empty/null)accountUpdates: array of account updates for the transactionmemo: transaction memo field
| Transaction Type | Required Signatures |
|---|---|
| Deploy transactions | Fee payer key + Contract account key |
| Interaction transactions (state/permissions/vk update) | Fee payer key + Contract account key (due to requireSignature()) |
| Payment transactions | Fee payer key |
import { Bool, Mina } from "o1js";
export function UpdateFullCommitment(...tx: Mina.Transaction<false, false>[]) {
tx.forEach((transaction) => {
transaction.transaction.accountUpdates.map(
(au) => au.body.useFullCommitment = Bool(true)
);
});
}import { AccountUpdate, Mina, PrivateKey, PublicKey, UInt64 } from 'o1js';
import * as fs from 'fs';
async function createPayment() {
let Local = await Mina.LocalBlockchain({ proofsEnabled: true });
Mina.setActiveInstance(Local);
let [sender, receiver] = Local.testAccounts;
const tx = await Mina.transaction(
sender,
async () => {
senderUpdate.send({ to: receiver, amount: UInt64.from(1e9) });
}
);
await tx.prove(); // optional for basic payment
const json = tx.toJSON();
const outputPath = './tx-json/payment-transaction.json';
fs.writeFileSync(outputPath, json);
console.log(`Transaction saved to ${outputPath}`);
}
createPayment();import {
SmartContract,
State,
state,
method,
Field,
Permissions,
Mina,
AccountUpdate,
PublicKey,
} from 'o1js';
import * as fs from 'fs';
import { UpdateFullCommitment } from './commit';
const MINA_TESTNET_URL = 'https://api.minascan.io/node/devnet/v1/graphql';
const FEE = 100_000_000; // 0.1 MINA
const DEPLOYER_KEY = '<PUBLIC_KEY>';
class StateContract extends SmartContract {
@state(Field) counter = State<Field>();
init() {
super.init();
this.counter.set(Field(0));
this.account.permissions.set({
...Permissions.default(),
editState: Permissions.signature(),
});
}
@method async incrementCounter() {
this.requireSignature();
const currentValue = this.counter.get();
this.counter.requireEquals(currentValue);
this.counter.set(currentValue.add(1));
}
}
async function generateUpdateStateTx() {
// 1. Setup Mina testnet
const network = Mina.Network(MINA_TESTNET_URL);
Mina.setActiveInstance(network);
// 2. Get accounts
const deployer = PublicKey.fromBase58(DEPLOYER_KEY);
const contractAccount = PublicKey.fromBase58(DEPLOYER_KEY);
const contract = new StateContract(contractAccount);
// 3. Compile (needed for verification key)
await StateContract.compile();
// 4. Create deploy transaction (unsigned)
const deployTx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.deploy();
}
);
// 5. Create update transaction (unsigned)
const tx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.incrementCounter();
}
);
UpdateFullCommitment(deployTx, tx);
fs.writeFileSync('./tx-json/deploy-state-contract.json', deployTx.toJSON());
console.log('Deploy transaction saved to ./tx-json/deploy-state-contract.json');
fs.writeFileSync('./tx-json/update-state-transaction.json', tx.toJSON());
console.log('Update transaction saved to ./tx-json/update-state-transaction.json');
}
generateUpdateStateTx();import {
SmartContract,
method,
Permissions,
Mina,
AccountUpdate,
PublicKey,
} from 'o1js';
import * as fs from 'fs';
import { UpdateFullCommitment } from './commit';
const MINA_TESTNET_URL = 'https://api.minascan.io/node/devnet/v1/graphql';
const FEE = 100_000_000; // 0.1 MINA
const DEPLOYER_KEY = '<PUBLIC_KEY>';
class PermissionsContract extends SmartContract {
init() {
super.init();
this.account.permissions.set({
...Permissions.default(),
setPermissions: Permissions.signature(),
});
}
@method async updatePermissions() {
this.requireSignature();
this.account.permissions.set({
...Permissions.default(),
editState: Permissions.proofOrSignature(),
send: Permissions.proof(),
setPermissions: Permissions.signature(),
setVerificationKey: Permissions.VerificationKey.impossibleDuringCurrentVersion(),
});
}
}
async function generateUpdatePermissionsTx() {
// 1. Setup Mina testnet
const network = Mina.Network(MINA_TESTNET_URL);
Mina.setActiveInstance(network);
// 2. Get accounts
const deployer = PublicKey.fromBase58(DEPLOYER_KEY);
const contractAccount = PublicKey.fromBase58(DEPLOYER_KEY);
const contract = new PermissionsContract(contractAccount);
// 3. Compile (needed for verification key)
await PermissionsContract.compile();
// 4. Create deploy transaction (unsigned)
const deployTx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.deploy();
}
);
// 5. Create update transaction (unsigned)
const tx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.updatePermissions();
}
);
UpdateFullCommitment(deployTx, tx);
fs.writeFileSync('./tx-json/deploy-permissions-contract.json', deployTx.toJSON());
console.log('Deploy transaction saved to ./tx-json/deploy-permissions-contract.json');
fs.writeFileSync('./tx-json/update-permissions-transaction.json', tx.toJSON());
console.log('Update transaction saved to ./tx-json/update-permissions-transaction.json');
}
generateUpdatePermissionsTx();import {
SmartContract,
VerificationKey,
method,
Permissions,
Mina,
AccountUpdate,
PublicKey,
} from 'o1js';
import * as fs from 'fs';
import { UpdateFullCommitment } from './commit';
const MINA_TESTNET_URL = 'https://api.minascan.io/node/devnet/v1/graphql';
const FEE = 100_000_000; // 0.1 MINA
const DEPLOYER_KEY = '<PUBLIC_KEY>';
class UpdatableContract extends SmartContract {
init() {
super.init();
this.account.permissions.set({
...Permissions.default(),
setVerificationKey: Permissions.VerificationKey.signature(),
});
}
@method async updateVerificationKey(verificationKey: VerificationKey) {
this.requireSignature();
this.account.verificationKey.set(verificationKey);
}
}
class NewContract extends SmartContract {
@method async dummy() {
// Different contract = different verification key
}
}
async function generateUpdateVerificationKeyTx() {
// 1. Setup Mina testnet
const network = Mina.Network(MINA_TESTNET_URL);
Mina.setActiveInstance(network);
// 2. Get accounts
const deployer = PublicKey.fromBase58(DEPLOYER_KEY);
const contractAccount = PublicKey.fromBase58(DEPLOYER_KEY);
const contract = new UpdatableContract(contractAccount);
// 3. Compile original contract (needed for verification key)
await UpdatableContract.compile();
// 4. Create deploy transaction (unsigned)
const deployTx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.deploy();
}
);
// 5. Compile new contract for different verification key
const { verificationKey: newVerificationKey } = await NewContract.compile();
// 6. Create update transaction (unsigned)
const tx = await Mina.transaction(
{ sender: deployer, fee: FEE },
async () => {
await contract.updateVerificationKey(newVerificationKey);
}
);
UpdateFullCommitment(deployTx, tx);
fs.writeFileSync('./tx-json/deploy-updatable-contract.json', deployTx.toJSON());
console.log('Deploy transaction saved to ./tx-json/deploy-updatable-contract.json');
fs.writeFileSync('./tx-json/update-verification-key-transaction.json', tx.toJSON());
console.log('Update transaction saved to ./tx-json/update-verification-key-transaction.json');
}
generateUpdateVerificationKeyTx();proofsEnabled: false— transactions use signatures instead of zero-knowledge proofsPermissions.signature()— allows authorization via signature instead of proofthis.requireSignature()— method call that requires contract account signature for authorization- Compilation is still required — generates verification key needed for contract deployment
- Generate transactions: Run npm script → outputs unsigned JSON to
./tx-json/ - Sign externally: External tool reads JSON, signs with required private keys
- Submit: Signed transaction submitted to Mina network
| API | Purpose |
|---|---|
SmartContract |
Base class for zkApps |
@method decorator |
Defines callable contract methods |
@state(Field) decorator |
Declares on-chain state variables |
State<T> |
Type wrapper for state variables |
VerificationKey |
Type for verification keys |
Permissions |
Permission configuration object |
Permissions.signature() |
Require signature for authorization |
Permissions.VerificationKey.signature() |
Require signature for VK updates |
this.account.permissions.set() |
Update account permissions |
this.account.verificationKey.set() |
Update verification key |
this.requireSignature() |
Require contract key signature in method |
Mina.LocalBlockchain() |
Create local test blockchain |
Mina.transaction() |
Create a transaction |
AccountUpdate.fundNewAccount() |
Fund a new account (pays creation fee) |
tx.toJSON() |
Serialize transaction to JSON |