Arkade Language is a high-level contract language that compiles down to Arkade Script, an extended version of Bitcoin Script designed for the Arkade OS. Arkade Language lets developers write expressive, stateful smart contracts that compile to scripts executable by Arkade's Virtual Machine.
Arkade Script supports advanced primitives for arithmetic, introspection, and asset flows across Virtual Transaction Outputs (VTXOs), enabling rich offchain transaction logic with unilateral onchain exit guarantees. Contracts are verified and executed inside secure Trusted Execution Environments (TEEs) and signed by the Arkade Signer, ensuring verifiable and tamper-proof execution.
This language significantly lowers the barrier for Bitcoin-native app development, allowing contracts to be written in a structured, Ivy-like syntax and compiled into Arkade-native scripts.
- Setup pre-commit checks
cp ./scripts/pre-commit .git/hooks
Try Arkade Script in your browser — no installation required:
Prerequisites:
-
Rust toolchain
-
cargo install wasm-pack # or curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
Build and serve:
# Build the WASM package and set up the playground
./playground/build.sh
# Serve locally (default port 8080)
./playground/serve.sh
# Or specify a custom port
./playground/serve.sh 3000Then open http://localhost:8080 in your browser.
What the build script does:
- Generates
contracts.jsfrom the.arkexample files inexamples/ - Compiles the Rust compiler to WebAssembly using
wasm-pack - Outputs the WASM package to
playground/pkg/
arkadec contract.arkThis compiles your Arkade Language contract to a JSON artifact for use with Ark libraries.
# Specify output file
arkadec contract.ark -o contract.jsonThe compiler produces a JSON file containing:
- Contract metadata (name, version, etc.)
- Constructor parameters
- Function definitions with both cooperative and exit spending paths
- Assembly for each path
Example — SingleSig compiled output:
{
"contractName": "SingleSig",
"constructorInputs": [
{ "name": "user", "type": "pubkey" }
],
"functions": [
{
"name": "spend",
"functionInputs": [
{ "name": "userSig", "type": "signature" }
],
"serverVariant": true,
"require": [
{ "type": "signature" },
{ "type": "serverSignature" }
],
"asm": [
"<user>",
"<userSig>",
"OP_CHECKSIG",
"<SERVER_KEY>",
"<serverSig>",
"OP_CHECKSIG"
]
},
{
"name": "spend",
"functionInputs": [
{ "name": "userSig", "type": "signature" }
],
"serverVariant": false,
"require": [
{ "type": "signature" },
{ "type": "older", "message": "Exit timelock of 144 blocks" }
],
"asm": [
"<user>",
"<userSig>",
"OP_CHECKSIG",
"144",
"OP_CHECKSEQUENCEVERIFY",
"OP_DROP"
]
}
],
"source": "...",
"compiler": {
"name": "arkade-script",
"version": "0.1.0"
},
"updatedAt": "2024-01-01T00:00:00Z"
}The simplest VTXO: a single public key controls spending.
options {
server = server;
renew = 1008;
exit = 144;
}
contract SingleSig(pubkey user) {
function spend(signature userSig) {
require(checkSig(userSig, user));
}
}Each function compiles to two variants automatically:
- Cooperative (
serverVariant: true):checkSig(user) && checkSig(server) - Exit (
serverVariant: false):checkSig(user) && after 144 blocks
options {
server = server;
renew = 1008;
exit = 144;
}
contract HTLC(pubkey sender, pubkey receiver, bytes hash, int refundTime) {
function together(signature senderSig, signature receiverSig) {
require(checkMultisig([sender, receiver], [senderSig, receiverSig]));
}
function refund(signature senderSig) {
require(checkSig(senderSig, sender));
require(tx.time >= refundTime);
}
function claim(signature receiverSig, bytes preimage) {
require(checkSig(receiverSig, receiver));
require(sha256(preimage) == hash);
}
}Use import and new ContractName(args) to enforce that a transaction output carries a specific VTXO contract. This is how VTXOs are forwarded or transformed on-chain.
import "single_sig.ark";
options {
server = operator;
exit = 144;
}
contract RecursiveVtxo(pubkey ownerPk) {
// Forward ownership to output 0, maintaining the SingleSig VTXO shape.
function send() {
require(tx.outputs[0].scriptPubKey == new SingleSig(ownerPk));
}
}The new SingleSig(ownerPk) expression compiles to a <VTXO:SingleSig(<ownerPk>)> placeholder. At runtime the Ark server resolves this placeholder to the actual Taproot scriptPubKey of the child contract, so the introspection check is pure Bitcoin Script.
Cooperative path ASM:
0 OP_INSPECTOUTPUTSCRIPTPUBKEY <VTXO:SingleSig(<ownerPk>)> OP_EQUAL
<SERVER_KEY> <serverSig> OP_CHECKSIG
Exit path ASM — because introspection opcodes are not available on pure Bitcoin Script exit paths, the compiler automatically falls back to N-of-N CHECKSIG:
<ownerPk> <ownerPkSig> OP_CHECKSIG
144 OP_CHECKSEQUENCEVERIFY OP_DROP
import "single_sig.ark";
options {
server = operator;
exit = 144;
}
contract Splitter(pubkey alicePk, pubkey bobPk) {
function split() {
require(tx.outputs[0].scriptPubKey == new SingleSig(alicePk));
require(tx.outputs[1].scriptPubKey == new SingleSig(bobPk));
}
}import "self.ark";
options {
server = operator;
exit = 144;
}
contract FujiSafe(
bytes assetCommitmentHash,
int borrowAmount,
pubkey borrowerPk,
pubkey treasuryPk,
int expirationTimeout,
int priceLevel,
int setupTimestamp,
pubkey oraclePk,
bytes assetPair
) {
// Treasury can renew the VTXO without changing any parameters.
function renew(signature treasurySig) {
int currentValue = tx.input.current.value;
require(
tx.outputs[0].scriptPubKey == new FujiSafe(
assetCommitmentHash, borrowAmount, borrowerPk, treasuryPk,
expirationTimeout, priceLevel, setupTimestamp, oraclePk, assetPair
),
"contract mismatch"
);
require(tx.outputs[0].value == currentValue, "Value mismatch");
require(checkSig(treasurySig, treasuryPk), "Invalid treasury signature");
}
}pubkey: Bitcoin public key (32-byte x-only, BIP340)signature: Bitcoin signature (64-byte BIP340 Schnorr)bytes: Arbitrary byte arraybytes20: 20-byte arraybytes32: 32-byte arrayint: Integer value (CScriptNum)bool: Boolean valueasset: Asset identifier (for asset-aware contracts)
An Arkade Language file may start with zero or more import declarations, followed by an options block and a contract declaration:
import "other_contract.ark"; // optional — imports for contract instantiation
options {
server = operator; // Ark operator key
renew = 1008; // renewal timelock in blocks (optional)
exit = 144; // exit timelock in blocks
}
contract MyContract(pubkey user) {
function spend(signature userSig) {
require(checkSig(userSig, user));
}
}| Field | Required | Description |
|---|---|---|
server |
yes | Parameter name holding the Ark operator public key |
exit |
yes | Unilateral exit timelock in blocks |
renew |
no | Cooperative renewal timelock in blocks |
Functions define spending paths. Every non-internal function produces two compiled variants:
// Spending path — compiled to cooperative + exit variants
function spend(signature userSig) {
require(checkSig(userSig, user));
}
// Helper — not a spending path, inlined into callers
function verify() internal {
require(tx.outputs[0].value > 0);
}Use import to declare which contracts may appear in new expressions:
import "single_sig.ark";
import "htlc.ark";Use new ContractName(arg1, arg2, ...) as the right-hand side of a scriptPubKey comparison to enforce the shape of an output or input VTXO:
// Output enforcement
require(tx.outputs[0].scriptPubKey == new SingleSig(ownerPk));
// Input enforcement
require(tx.inputs[0].scriptPubKey == new HTLC(sender, receiver, hash, refundTime));
// Current input enforcement (recursive covenant)
require(tx.input.current.scriptPubKey == new SingleSig(ownerPk));Zero-argument constructors are supported:
require(tx.outputs[0].scriptPubKey == new StaticContract());Exit path fallback: any function that uses new ContractName(...) automatically falls back to an N-of-N CHECKSIG chain on the exit path, because the OP_INSPECTOUTPUTSCRIPTPUBKEY opcode is not available in pure Bitcoin Script.
require(checkSig(userSig, user));
require(checkMultisig([user, admin], [userSig, adminSig]));
require(checkSigFromStack(oracleSig, oraclePk, message));require(sha256(preimage) == hash);require(tx.time >= expirationTime); // absolute (CHECKLOCKTIMEVERIFY)// Outputs
require(tx.outputs[0].value == amount);
require(tx.outputs[0].scriptPubKey == new SingleSig(ownerPk));
// Indexed inputs
require(tx.inputs[0].value == amount);
require(tx.inputs[0].scriptPubKey == script);
// Current input (self-reference)
require(tx.input.current.value == amount);
require(tx.input.current.scriptPubKey == script);tx.input.current properties: value, scriptPubKey, sequence, outpoint.
bytes message = sha256(timestamp + currentPrice + assetPair);
int currentValue = tx.input.current.value;require(tx.time >= expirationTimeout, "Expiration timeout not reached");Arkade Language compiles to Arkade Script and produces a JSON artifact for use with Ark libraries.
| Field | Description |
|---|---|
contractName |
Contract identifier |
constructorInputs |
Parameters baked into the tapscript leaf at instantiation |
functions |
Spending paths — each appears twice (cooperative + exit) |
serverVariant |
true = cooperative (needs server sig), false = exit (needs timelock) |
require |
Human-readable spending conditions |
asm |
Arkade Script assembly; <name> = placeholder resolved at runtime |
Contract instantiation expressions in ASM use the format:
<VTXO:ContractName(<arg1>,<arg2>)>
The Ark runtime resolves this placeholder to the Taproot scriptPubKey of the named contract instantiated with the given arguments. Options (server, exit, renew) are inherited from the enclosing contract.