@canvas-js/core
TypeScript icon, indicating that this package has built-in type declarations

0.4.11 • Public • Published

@canvas-js/core

A Core verifies, executes, and applies the effects of signed messages.

⚠️ The simplest way to run a Canvas application is to use the CLI or Canvas Hub (coming soon). Only use @canvas-js/core directly if you're embedding a Canvas app in a browser context or other JavaScript system.

Table of Contents

Installation

$ npm i @canvas-js/core

Usage

To initialize a core, import @canvas-js/core and call Core.initialize({ ... }) with the contract and path to application data directory.

import { Core } from "@canvas-js/core"

const contract = `// A sample Canvas application
export const models = {
  posts: {
    id: "string",
    updated_at: "datetime",
    from: "string",
    content: "string",
  }
}

export const actions = {
  createPost({ content }, { hash, from, db }) {
    const postId = from + "/" + hash
    db.posts.set(postId, { content })
  },
  deletePost({ postId }, { from, db }) {
    assert(postId.startsWith(from + "/"))
    db.posts.delete(postId)
  },
}

export const routes = { 
  "/all": ({}) => "SELECT * FROM posts"
}
`

const core = await Core.initialize({
  spec: contract,
  directory: null, // run in-memory
  unchecked: true, // don't require blockhashes
  offline: true,   // don't start libp2p
})

console.log(core.app) // ipfs://QmTFwv6NF78V64CVdWrabYhQhmV4bWJw63aTRcVhvBAU2u

Applications are identified by the ipfs://... hash of the contract. Sessions and actions must have contract's ipfs://... URI in payload.app.

Configuration options

spec and directory are the only values strictly required, but in general running a Core involves understanding and configuring several additional properties.

declare interface CoreOptions {
  unchecked?: boolean // don't require or validate blockhashes in messages
  verbose?: boolean // print verbose logging to stdout
  offline?: boolean // disable libp2p
  replay?: boolean  // replay the entire message log on start-up
}

declare interface CoreConfig extends CoreOptions {
  // Path to application data directory, or `null` to run in-memory (NodeJS only)
  directory: string | null

  // Full text of the contract to run.
  spec: string

  // Provide chain implementations for the chains declared by the contract.
  // Defaults to [new EthereumChainImplementation(1, "localhost")] if not provided.
  // Core.intialize will throw an error if there are any chains delcared by the
  // contract that have no implementations provided here.
  chains?: ChainImplementation<unknown, unknown>[]

  // Internal /ws multiaddrs to listen for libp2p connections.
  // These should be local addresses, most likely `/ip4/0.0.0.0/tcp/${port}/ws`.
  // If not provided, the core will join the libp2p mesh via the public relay servers.
  listen?: string[] 

  // External /ws and /wss multiaddrs to announce to the DHT.
  // These should be publicly-resolvable, most likely `/dns4/${hostname}/tcp/${port}/wss`.
  announce?: string[]

  // Override the default list of libp2p bootstrap/relay servers.
  // Must be an array of multiaddrs for libp2p servers supporting circuit-relay v2.
  bootstrapList?: string[]

  // Set a custom app URI. Defaults to ipfs:// of spec. Don't use this!
  uri?: string
}

Applying messages

Once you have a core running, you can apply messages using core.apply, which returns a promise resolving to an object { hash: string } containin the hash of the message if it succeeds, or rejecting with an error if it fails.

import { EthereumChainImplementation } from "@canvas-js/chain-ethereum"

// ...

const chain = new EthereumChainImplementation(1) // ethereum mainnet
const wallet = ethers.Wallet.createRandom()

console.log(wallet.address) // 0x6431584d547d210560Cd170CeF61cF7eE8486013

const action = await chain.signAction(wallet, {
  from: wallet.address,
  app: core.app,
  call: "createPost",
  callArgs: { content: "hello world" },
  timestamp: Date.now(),
  chain: chain.chain,
  block: null,
})

// {
//   type: 'action',
//   signature: '0xfe715f94239d5e4b8db86e424e373e9378d7e92ab392cc5996cddbebf2860eda4b0625f7be6de551ec9563b361809ae8fd6af5711c882b2c86323052d10675031c',
//   session: null,
//   payload: {
//     from: '0x6431584d547d210560Cd170CeF61cF7eE8486013',
//     app: 'ipfs://QmcHBPw1gVvrTDUbqAtNfgDBndmbBpbDmnps9CLnbH5YQk',
//     call: 'createPost',
//     callArgs: { content: 'hello world' },
//     timestamp: 1681857137057,
//     chain: 'eip155:1',
//     block: null
//   }
// }

const { hash } = await core.apply(action)
console.log(hash) // 0x4a45a783cfecab42b0b14a0fb239dc95d17df47a2d49ddb1259d431519a45c48

Evaluating routes

You can evaluate the SQL routes with core.getRoute, which takes the full string route name and an optional object of parameter values.

const posts = await core.getRoute("/all")
// [
//   {
//     id: '0x6431584d547d210560Cd170CeF61cF7eE8486013/0x4a45a783cfecab42b0b14a0fb239dc95d17df47a2d49ddb1259d431519a45c48',
//     updated_at: 1681857375435,
//     content: 'hello world',
//     from: '0x6431584d547d210560Cd170CeF61cF7eE8486013'
//   }
// ]

Get application metadata

const data = await core.getApplicationData()
// {
//   peerId: null, // null if in offline mode
//   uri: 'ipfs://QmcHBPw1gVvrTDUbqAtNfgDBndmbBpbDmnps9CLnbH5YQk',
//   cid: 'QmcHBPw1gVvrTDUbqAtNfgDBndmbBpbDmnps9CLnbH5YQk',
//   actions: [ 'createPost', 'deletePost' ],
//   routes: [ '/all' ],
//   chains: [ 'eip155:1' ],
//   peers: [],
//   merkleRoots: {}
// }

Development

Regenerate the RPC protobuf message types with npm run generate-rpc.

The package should be built with typescript in composite build mode from the repo root, not from the package directory here.

Testing

npm run test

API

import { CID } from "multiformats/cid"
import { Libp2p } from "libp2p"

import { Message, ModelValue, Model, Chain, ChainId, ApplicationData } from "@canvas-js/interfaces"

declare interface CoreOptions {
  unchecked?: boolean
  verbose?: boolean
  offline?: boolean
  replay?: boolean
}

declare interface CoreConfig extends CoreOptions {
  directory: string | null
  spec: string
  chains?: ChainImplementation<unknown, unknown>[]
  listen?: string[]
  announce?: string[]
  bootstrapList?: string[]
  uri?: string
}

declare class Core extends EventEmitter<CoreEvents> implements CoreAPI {
  public static initialize(config: CoreConfig): Promise<Core>

  public readonly app: string
  public readonly cid: CID
  public readonly directory: string | null
  public readonly libp2p: Libp2p | null

  public close(): Promise<void>
  public apply(message: Message): Promise<{ hash: string }>
  public getRoute(route: string, params: Record<string, string>): Promise<Record<string, ModelValue>[]>
  public getApplicationData(): Promise<ApplicationData>
}

Keywords

none

Install

DownloadsWeekly Downloads

516

Version

0.4.11

License

none

Unpacked Size

1.68 MB

Total Files

118

Last publish

Collaborators

  • rjwebb
  • joelg
  • raykyri