Asentum

Build on Asentum

Writing Contracts

Start here · Estimated read time: 12 minutes

TL;DR

Asentum contracts are plain JavaScript files. Top-level named functions become callable methods. A handful of injected globals — storage, emit, msg, E — give you state, logs, sender info, and cross-contract calls. Everything else is just JavaScript, running inside a Hardened JavaScript sandbox.

Anatomy of a contract

The smallest useful contract:

function init() {
  storage.set('count', 0n);
}

function increment() {
  const next = storage.get('count') + 1n;
  storage.set('count', next);
  emit('Incremented', { newValue: next });
}

function get() {
  return storage.get('count');
}
  • Every top-level function is exposed as a callable method.
  • Functions that read storage but don't write are view methods — free, callable via asentum_viewCall, no transaction required.
  • Functions that write to storage or call emit are send methods — require a signed transaction and cost gas.

The storage API

Each contract gets its own keyed key/value store. Keys are strings, values are anything JSON-serializable (including BigInt and nested objects).

  • storage.get(key) — read a value (undefined if unset).
  • storage.set(key, value) — write a value.
  • storage.delete(key) — remove a value.
  • storage.has(key) — check existence.

Contract state is a Sparse Merkle Tree under the hood — individual key writes are cheap, but large payloads pay per-byte. See the fee market for the cost model.

Emitting events

emit('Transfer', { from, to, amount });

Events are indexed and queryable via eth_getLogs. The first argument is the event name (topic 0); object keys become additional indexed topics. This is how explorers and dapps listen for on-chain activity.

The msg object

Inside a method body, msg exposes the transaction context:

  • msg.sender — the caller's address.
  • msg.value — ASE attached (in wei, as a BigInt).
  • msg.block.number — current block height.
  • msg.block.timestamp — current block time (unix seconds).

Calling other contracts

Cross-contract calls are async message-passing, not synchronous. That's what makes reentrancy structurally impossible. Use the injected E() helper:

async function payout(token, to, amount) {
  const balance = await E(token).balanceOf(to);
  if (balance < amount) throw new Error('insufficient');
  await E(token).transfer(to, amount);
}

Every E(addr).method() returns a Promise; the next block commits after the call completes. For the mental model, see Smart Contracts.

Constructors and init

Asentum contracts don't have a special constructor keyword. Convention is to export an init function and guard it against replay:

function init(owner) {
  if (storage.has('initialized')) throw new Error('already initialized');
  storage.set('initialized', true);
  storage.set('owner', owner ?? msg.sender);
}

The deployer typically calls init in a second transaction immediately after deploy. The SDK can bundle deploy + init into one flow.

Gotchas

  • Use BigInt for amounts. Numbers are never safe for token math. Always use 0n, amount + 1n, etc.
  • No Date.now() or Math.random(). Both break determinism — they're removed from the contract sandbox. Use msg.block.timestamp for time; there is deliberately no randomness primitive in v1.
  • No fetch, no setTimeout, no filesystem. Contracts are deterministic pure functions over their inputs.
  • Throw to revert. Any uncaught exception reverts the transaction and refunds unused gas. Use descriptive messages.

For the complete list of allowed language features, see Hardened JavaScript.

A worked example: tip jar

function init() {
  storage.set('owner', msg.sender);
  storage.set('total', 0n);
}

function tip() {
  if (msg.value === 0n) throw new Error('send some ASE');
  storage.set('total', storage.get('total') + msg.value);
  emit('Tipped', { from: msg.sender, amount: msg.value });
}

function withdraw() {
  if (msg.sender !== storage.get('owner')) throw new Error('not owner');
  const total = storage.get('total');
  storage.set('total', 0n);
  transfer(msg.sender, total);
  emit('Withdrawn', { to: msg.sender, amount: total });
}

function getTotal() { return storage.get('total'); }

Try it live in the playground — it's one of the built-in templates. Ready to deploy for real? See Deploy Your First Contract.

Read next