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 Arkade libraries.
# Specify output file
arkadec contract.ark -o contract.jsonThe compiler produces a JSON file containing:
- Contract metadata (name, version, etc.)
- Constructor parameters
- Spend groups in
functions[] - Optional
arkadecovenant assembly per function-backed group - One or more L1 tapleaves per group, each with witness metadata and ASM
Example — SingleSig compiled output:
{
"contractName": "SingleSig",
"constructorInputs": [
{ "name": "user", "type": "pubkey" },
{ "name": "exit", "type": "int" }
],
"functions": [
{
"name": "spend",
"arkade": {
"inputs": [{ "name": "userSig", "type": "signature" }],
"asm": ["<user>", "<userSig>", "OP_CHECKSIG"]
},
"leaves": [
{
"name": "spend",
"witness": [
{
"name": "serverSig",
"type": "signature",
"encoding": "schnorr-64",
"injected": true
},
{
"name": "emulatorSig",
"type": "signature",
"encoding": "schnorr-64",
"injected": true
}
],
"asm": [
"<SERVER_KEY>",
"OP_CHECKSIGVERIFY",
"<EMULATOR_KEY:spend>",
"OP_CHECKSIG"
]
}
]
}
],
"source": "...",
"compiler": {
"name": "arkade-compiler",
"version": "0.1.0"
},
"updatedAt": "2024-01-01T00:00:00Z"
}The simplest VTXO: a single public key controls spending.
contract SingleSig(pubkey user, int exit) {
function spend(signature userSig) {
require(checkSig(userSig, user));
}
function unilateral(signature userSig) tapscript {
require(older(exit));
require(checkSig(userSig, user));
}
}The covenant function emits arkade ASM. The unilateral tapscript is a pure L1 CSV exit leaf.
contract HTLC(
pubkey sender,
pubkey receiver,
bytes20 preimageHash,
int refundTime,
int exit
) {
function claim() {
require(tx.outputs[0].value >= tx.inputs[0].value);
}
function refund() {
require(tx.outputs[0].value >= tx.inputs[0].value);
}
function claim(bytes preimage, signature serverSig, signature emulatorSig) tapscript {
require(hash160(preimage) == preimageHash);
require(checkMultisig([server, emulator], [serverSig, emulatorSig], 2));
}
function refund(signature serverSig, signature emulatorSig) tapscript {
require(tx.time >= refundTime);
require(checkMultisig([server, emulator], [serverSig, emulatorSig], 2));
}
function unilateral(signature senderSig) tapscript {
require(older(exit));
require(checkSig(senderSig, sender));
}
}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";
contract RecursiveVtxo(pubkey ownerPk, int exit) {
// Forward ownership to output 0, maintaining the SingleSig VTXO shape.
function send() {
require(tx.outputs[0].scriptPubKey == new SingleSig(ownerPk, exit));
}
}The new SingleSig(ownerPk, exit) expression compiles to a <VTXO:SingleSig(<ownerPk>,<exit>)> placeholder. At runtime the Arkade Operator resolves this placeholder to the actual Taproot scriptPubKey of the child contract, so the introspection check is pure Bitcoin Script.
Arkade covenant ASM:
0 OP_INSPECTOUTPUTSCRIPTPUBKEY <VTXO:SingleSig(<ownerPk>,<exit>)> OP_EQUAL
If no matching tapscript is declared for send, the compiler synthesizes a default collaborative leaf:
<SERVER_KEY> OP_CHECKSIGVERIFY <EMULATOR_KEY:send> OP_CHECKSIG
import "single_sig.ark";
contract Splitter(pubkey alicePk, pubkey bobPk, int exit) {
function split() {
require(tx.outputs[0].scriptPubKey == new SingleSig(alicePk, exit));
require(tx.outputs[1].scriptPubKey == new SingleSig(bobPk, exit));
}
}import "self.ark";
contract FujiSafe(
bytes assetCommitmentHash,
int borrowAmount,
pubkey borrowerPk,
pubkey treasuryPk,
int expirationTimeout,
int priceLevel,
int setupTimestamp,
pubkey oraclePk,
bytes assetPair,
int exit
) {
// 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 a contract declaration:
import "other_contract.ark"; // optional — imports for contract instantiation
contract MyContract(pubkey user, int exit) {
function spend(signature userSig) {
require(checkSig(userSig, user));
}
function unilateral(signature userSig) tapscript {
require(older(exit));
require(checkSig(userSig, user));
}
}Functions without a modifier define arkade covenants. tapscript functions define L1 tapleaves. A covenant function with no matching or tweaked tapleaf receives a synthesized collaborative leaf using server and tweak(emulator, functionName).
// Arkade covenant.
function spend(signature userSig) {
require(checkSig(userSig, user));
}
// L1 CSV exit leaf.
function unilateral(signature userSig) tapscript {
require(older(exit));
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());Pure L1 exits are expressed as explicit tapscript functions, usually with older(exit).
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 Arkade libraries.
| Field | Description |
|---|---|
contractName |
Contract identifier |
constructorInputs |
Constructor parameters baked into instantiated scripts |
functions |
Spend groups: { name, arkade?, leaves[] } |
arkade |
Optional emulator-run covenant { inputs, asm } |
leaves |
L1 tapleaf objects { name, witness, asm } |
witness |
Spend-time witness fields, with injected: true for infrastructure sigs |
asm |
Assembly tokens; <name> = placeholder resolved at runtime |
Contract instantiation expressions in ASM use the format:
<VTXO:ContractName(<arg1>,<arg2>)>
The Arkade runtime resolves this placeholder to the Taproot scriptPubKey of the named contract instantiated with the given arguments.