Skip to main content

Class: CubistORM

This class extends the Cubist class with project-specific contracts and contract factories. For most applications you should use this class over Cubist: it gives you a higher-level, ORM-like interface.

Since this class is generated at build time, the easiest way to describe it is to walk through an example. The overview has one example; here we'll walk through one of our templates—the cross-chain Storage template—and look at the generated code.

tip

If you haven't gone through the Quick Start guide, it's worth doing so now. You might also find the in-depth walkthough of the multich-chain storage app useful, too. We're using this app in the guide.

Let's start by creating a new project:

cubist new --type TypeScript --template Storage storageApp
cd storageApp

Next, we'll install dependencies and build the project:

npm install
cubist build

Recall from the Quick Start guide that this app consists of two contracts: StorageSender stores a number and then calls StorageReceiver.store to store the number to the StorageReceiver contract, too.

The cubist build command, among other things, generates ORM interfaces for these contracts. Here is what those interfaces look like:

/// Auto-generated by Cubist. Do not edit manually.
import {
Config,
Contract,
ContractFactory,
Cubist,
Target,
} from '@cubist-labs/cubist';

// Export contract types
import type { StorageSender as EthersStorageSender } from '../polygon/types'
export type { StorageSender as EthersStorageSender } from '../polygon/types'

import type { StorageReceiver as EthersStorageReceiver } from '../ethereum/types'
export type { StorageReceiver as EthersStorageReceiver } from '../ethereum/types'


// Export contract and contract factory types
export type StorageSender = Contract<EthersStorageSender>;
export type StorageSenderFactory = ContractFactory<EthersStorageSender>;

export type StorageReceiver = Contract<EthersStorageReceiver>;
export type StorageReceiverFactory = ContractFactory<EthersStorageReceiver>;


// Export targets

export const Avalanche = Target.Avalanche;
export const Ethereum = Target.Ethereum;
export const Polygon = Target.Polygon;

/** Project-specific CubistORM. */
export class CubistORM extends Cubist {
/** Create new project per target
* @param {Config?} config - Optional config (using near otherwise).
*/
constructor(config?: Config) {
super(config);
}
/** Get contract factory for StorageSender
* @return { StorageSenderFactory } - The contract factory.
*/
get StorageSender(): StorageSenderFactory {
return this.getContractFactory('StorageSender');
}

/** Get contract factory for StorageReceiver
* @return { StorageReceiverFactory } - The contract factory.
*/
get StorageReceiver(): StorageReceiverFactory {
return this.getContractFactory('StorageReceiver');
}

}

This means that your app code doesn't have to use getContractFactory itself; you can just import the ORM and move on:

import { CubistORM, } from '../build/orm/index.js';

// Project instance
const cubist = new CubistORM();

export async function inc(val: number): Promise<void> {
const sender = (await cubist.StorageSender.deployed()).inner;
await (await sender.inc(val)).wait(1);
}

This eliminates a bunch of boilterplate code. And, if you're writing your app in TypeScript, it ensures that the factories StorageSender and StorageReceiver are well typed and that the contracts these factories return are well typed, too. This means you can't accidentally call the wrong method on a contract or call a method with an ill-typed value. For example, calling the inc method with a boolean:

await (await sender.inc(true)).wait(1);

results in a type error:

error TS2345: Argument of type 'boolean' is not assignable
to parameter of type 'PromiseOrValue<BigNumberish>'.

The way we enforce this under the hood is by using TypeChain to generate the type definitions for each contract (and its shims). The imports you see at the top of the ./build/orm/index.ts file import these definitions:

// Export contract types
import type { StorageSender as EthersStorageSender } from '../polygon/types'
export type { StorageSender as EthersStorageSender } from '../polygon/types'

import type { StorageReceiver as EthersStorageReceiver } from '../ethereum/types'
export type { StorageReceiver as EthersStorageReceiver } from '../ethereum/types'
danger

CubistORM currently has two important limitations:

  1. Because Cubist contract factories don't simply wrap ethers.js contract factories, the deploy() method is not well-typed and just takes any arguments. We plan to change this in future versions of the SDK.

  2. The ethers.js type definitions that TypeChain generates are more permissive than you might want (e.g., you can call the above inc method with a string and the type checker won't complain). We plan to explore generating stricter type definitions (to eliminate bugs due to implicit casts); if this is something you need, please reach out on discord or at [email protected].