Skip to content

Contract API

Each Canvas application is defined as a contract. We support two contract syntaxes:

Class Contracts

The class syntax is recommended for most applications.

ts
import { Contract } from "@canvas-js/core/contract"

class Chat extends Contract<typeof Chat.models> {
  static models = {
    messages: {
      id: "primary",
      content: "string",
    }
  }

  async createMessage(content: string) {
    this.db.create("messages", {
      content,
      address: this.address
    })
  }
}

A constructor is optional, but if provided, it should pass the topic to super(). Additionally, actions will only be generated for methods on the contract directly.

ts
class NamespacedChat extends Contract<typeof Chat.models> {
  createMessage(content: string) {
    super(content)
  }
  constructor(namespace: string) {
    super(namespace + ".chat.canvas.xyz")
  }
}

You can initialize a class contract by providing it to Canvas.initialize, or calling initialize() on it directly:

ts
const app = await Canvas.initialize({
  contract: Chat,
  topic: "example.xyz"
})
ts
const app = await NamespacedChat.initialize("mynamespace")

Once initialized, methods on the contract are called via this.actions.

ts
app.actions.createMessage("I'm a scary and powerful fire demon!")

Contracts with Permissions

You can also create a Firebase-style contract with permissions for each table, using a $rules object on the model.

This is useful for creating simpler contracts, and applications that resemble databases but still have constraints on what data can be stored in the application.

If you use $rules, you must use it for all models on a contract instead of specifying actions.

ts
import { ModelSchema } from "@canvas-js/core"

export const models = {
  items: {
    id: "primary",
    creator: "string",
    content: "string",
    $rules: {
      create: "creator === this.did",
      update: "creator === this.did",
      delete: false
    }
  }
}

Inside each permission check, this is the current ActionContext, and other fields on the database are in the global scope.

External API for Contracts with Permissions

Once you've set up a contract with rules, you can use the app.create, app.update, app.delete methods to access the application, like a regular database:

ts
const app = await Canvas.initialize({
  topic: "example.xyz",
  contract: { models },
  signers: [new SIWESigner({ burner: true })]
})

const did = await app.signers.getFirst().getDid()

await app.create("items", {
  creator: did,
  content: "I'm a scary and powerful fire demon!"
})

Contract APIs

Inside contracts, actions can access these fields:

  • ActionContext: Contains information like the current message ID for the action.
  • ModelAPI: Contains database APIs for reading, writing, transacting, generating IDs, etc.

In class contracts, actions are called with this set to an ActionContext:

PropertyDescription
dbA ModelAPI instance. See below.
idThe message ID of the current action, a 32-character hex string.
didThe DID identifier for the user.
addressA shortened DID identifier for the user. If you are writing an application for a specific chain, you can use this to get the e.g. Ethereum address of your user.
publicKeyA session-specific public key that the user authorized, which was used to sign this specific action, provided as a did:key identifier.
timestampA user-reported timestamp of their action.
blockhashNot currently used.

Import Styles

Each contract can be declared either inline or as a separate file or string, which will be imported as an ES module. Contracts that are imported as an ES module will be run inside a QuickJS WASM container.

Some application platforms do not work well with the QuickJS WASM runtime. For those applications, we recommend declaring your contract inline instead.