Deploy and call a Compact contract¶
Outcome: your app deploys a compiled Compact contract, gets the
contract address, and calls one of its circuits with witnesses — going
end-to-end from .compact source to an on-chain transaction.
Prerequisites¶
- The previous two recipes completed (SDK added, sigil bootstrapped).
- A
.compactcontract that has been compiled with the matching compactc version — the runtime version pinned in your contract'spackage.json(@midnight-ntwrk/compact-runtime) must match what the SDK expects. If you mismatch, the runtime will throw a bytecode-version error at load. - Compiled artifacts under
contract/src/managed/<contract-name>/: contract/index.js— the contract runtime entrykeys/*.prover,keys/*.verifier— per-circuit proving + verifying keyszkir/*.bzkir— the ZK intermediate representation
If you don't have these yet, run npm run compact (or your project's
equivalent) inside the contract/ directory first.
Compact authoring is a 'you provide' prereq
This recipe wires a pre-compiled Compact contract into your
Android app. It does not teach Compact authoring — writing a
.compact source file, installing compactc, or setting up the
JS toolchain. For that, see Hello Compact,
or clone the Midnight Network sample
contracts. This recipe assumes
the contract/src/managed/<name>/ directory already exists;
getting to that point is covered there.
Step 1 — Sync the compiled artifacts into your app's assets¶
The SDK's compact engine loads contract code + circuit keys from your APK's assets. The canonical layout is:
assets/runtime/<contract-alias>-contract.jsassets/keys/*.prover,*.verifier,*.bzkir
There are two ways to wire it. The Gradle plugin is the recommended
path; it's published to Maven Central as
0.1.0-alpha03. The hand-rolled Copy task is
the equivalent if you'd rather not add the plugin — it produces the
same asset layout the plugin would.
pluginManagement {
repositories {
gradlePluginPortal()
mavenCentral() // (1)
}
}
plugins {
id("com.android.application")
id("io.github.kuiralabs.contract") version "0.1.0-alpha03"
}
kuiraContract {
source.set("contract/src/managed/your-contract")
// alias.set("your-contract") // (2)
}
- The plugin ships to Maven Central. Add
mavenCentral()topluginManagement.repositoriessoplugins { id(...) }can resolve it. (It is not listed on the Gradle Plugin Portal, so this repo entry is required.) aliasis optional — defaults to the dirname ofsource. Socontract/src/managed/penaltyresolves to aliaspenalty, which lands the contract JS asassets/runtime/penalty-contract.js.
The plugin registers two tasks:
validateKuiraContractSource— verification task that always runs and fails fast with a helpful message if the source directory is missing ("compile your contract first — npm run compact …"). Catches the "forgot to compile" mistake at build time, not at runtime.syncContractAssets— the actual copy:contract/index.js→assets/runtime/<alias>-contract.js,keys/*.{prover,verifier}andzkir/*.bzkir→assets/keys/. Wired intopreBuildso it runs automatically before any APK is assembled.
If you'd rather not add the plugin, hand-roll the same task. This is what the plugin replaces — same output, more lines.
val contractDir = rootProject.file("contract")
val contractManaged = file("$contractDir/src/managed/your-contract")
val syncContractAssets = tasks.register<Copy>("syncContractAssets") {
description = "Sync compiled Compact contract artifacts into app assets."
group = "build"
from("$contractManaged/contract") {
include("index.js")
rename { "your-contract-contract.js" }
into("runtime")
}
from("$contractManaged/keys") {
include("*.prover", "*.verifier")
into("keys")
}
from("$contractManaged/zkir") {
include("*.bzkir")
into("keys")
}
into("src/main/assets")
doFirst {
if (!contractManaged.exists()) {
throw GradleException(
"Contract not compiled at $contractManaged — run " +
"`npm run compact` in your contract directory first.",
)
}
}
}
tasks.named("preBuild") { dependsOn(syncContractAssets) }
To switch to the plugin later, replace this entire block with the
four-line kuiraContract { source.set("…") } plugin pattern in the
other tab.
Verify: after ./gradlew :app:assembleDebug, unzip the resulting
APK and confirm assets/runtime/your-contract-contract.js,
assets/keys/*.prover, assets/keys/*.verifier, assets/keys/*.bzkir
are all present.
Step 2 — Build the contract handle¶
import android.content.Context
import com.midnight.kuira.core.compact.MidnightContract
import com.midnight.kuira.core.compact.WitnessResult
suspend fun buildContract(
context: Context,
sdk: MidnightSdk,
contractAddress: String? = null, // (1)
): MidnightContract {
// Deploy embeds each circuit's verifier key on-chain — load the bytes.
val verifier = context.assets
.open("keys/yourCircuit.verifier").use { it.readBytes() }
return MidnightContract.create(sdk.config) { // (2) config is positional
name = "yourcontract"
contractJs = context.assets.open("runtime/yourcontract-contract.js") // (3)
if (contractAddress != null) this.address = contractAddress
coinPublicKey = sdk.coinPublicKey
circuitVerifierKeys = mapOf("yourCircuit" to verifier)
witness("localSecret") { WitnessResult(null, ByteArray(32) { 0 }) } // (4)
}
}
nullwhile you're still going to calldeploy()— set this to the returned address for every subsequent call.create()takes the SDK'sMidnightConfig(sdk.config) as its first argument — not ansdkbuilder property.contractJsis anInputStreamfrom your assets — the syncedruntime/<alias>-contract.js, not a path string.witness(name) { … }— stub; replace with your contract's actual witness layout. (A contract with no private state, like the counter, omits this.) For typed witnesses (Vector<N, T>,Bytes<32>, …), pack the bytes by hand.
Verify: the function returns without throwing — meaning the QuickJS runtime found and loaded your contract JS, and witness descriptors typecheck against the bytecode.
Step 3 — Deploy¶
// Required once before the first deploy/call: stage your circuit's proving
// keys (+ BLS params) from assets where the on-device prover looks. Idempotent.
ProvingKeyManager(context).installCircuitKeysFromAssets()
val deployResult = contract.deploy()
val address = deployResult.contractAddress
Log.i("MyApp", "Deployed at $address")
deploy() performs the deploy transaction, waits for the indexer to
confirm it, and returns the contract address. Re-use this address for
every subsequent MidnightContract.create(sdk.config) { … address = address }
call.
Verify: address should be a 64-character hex string. Querying
sdk.indexerClient.queryContractState(address) should return a
non-null state.
Step 4 — Call a circuit¶
val result = contract.call(
circuit = "myCircuit",
args = mapOf(
"playerNum" to 1L,
"deadline" to (System.currentTimeMillis() / 1000 + 600), // (1)
),
)
- Avoid wall-clock for chain deadlines. Use the latest block
timestamp from the indexer — wall-clock drift between the dApp
and the chain can blow your deadline calculation. See the SDK's
wallet.tip()helper for the chain-time-anchored value.
The call:
- Generates the ZK proof using the artifacts you synced in step 1.
- Submits the resulting transaction via the wallet.
- Waits for indexer confirmation.
- Returns the post-call ledger state.
For long-running circuits (sub-second to a few seconds), wire up
TransactionBalancer progress callbacks for UX feedback.
Verify: the transaction confirms within ~10s on PREPROD. Query
contract.ledger() and check the field your circuit mutates.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
Contract not compiled at … at Gradle time |
Step 1 prereq not done. | Run npm run compact in contract/. |
Unsupported bytecode version at runtime |
Your compactc emitted bytecode for a different runtime version than the SDK ships. | Pin compactc to match your contract's @midnight-ntwrk/compact-runtime version. |
Indexer says contract not found after deploy |
Indexer hasn't caught up yet. | Add a 3–5s delay between deploy and first call. |
Invalid witness at call time |
Witness ByteArray length doesn't match the circuit's declared shape. |
Cross-check the witness layout against the circuit's declared shape. |
Deadline expired even though you set it in the future |
Using System.currentTimeMillis() instead of chain time. |
Switch to chain-anchored time (last block's timestamp). |
What's next¶
- Browse the API reference for the full
MidnightContract,MidnightWallet, and circuit-execution surface. - Security § Verifying releases — pin and verify the exact SDK artifact for your release builds.