Skip to content

Commit

Permalink
update README.md for reified
Browse files Browse the repository at this point in the history
  • Loading branch information
kklas committed Jan 25, 2024
1 parent c602f97 commit a3a8f69
Showing 1 changed file with 166 additions and 46 deletions.
212 changes: 166 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@

A tool for generating TS SDKs for Sui Move smart contracts. Supports code generation both for source code and on-chain packages with no IDLs or ABIs required.

## Caveats
- When specifying both source and on-chain packages, the generator will currently generate two separate dependency graphs (one for on-chain and one for source). This is due to a technical detail and will be resolved in a future version so that only a single dependency graph is generated (https://github.com/kunalabs-io/sui-client-gen/issues/1#issuecomment-1554754842).
- Since whitespace detection relies on some Rust nightly features which are currently unstable (https://github.com/udoprog/genco/issues/39#issuecomment-1569076737), the generated code is not formatted nicely. Usage of formatters on the generated code (e.g., `prettier`, `eslint`) is recommended.
- Because ESLint renames some types (e.g., `String` -> `string`) due to the `@typescript-eslint/ban-types` rule which breaks the generated code, an `.eslintrc.json` file is generated in the root directory to turn off this rule.
- When re-running the generator, the files generated on previous run will *not* be automatically deleted in order to avoid accidental data wipes. The old files can either be deleted manually before re-running the tool (it's safe to delete everything aside from `gen.toml`) or by running the generator with `--clean` (use with caution).

## Quick Start

1) Install the generator by either:
1. Install the generator by either:

- `cargo install --locked --git https://github.com/kunalabs-io/sui-client-gen.git` (you might have to install some [build dependencies](https://docs.sui.io/guides/developer/getting-started/sui-install#all-linux-prerequisites))
- or downloading a binary from https://github.com/kunalabs-io/sui-client-gen/releases/

3) Create a new directory and in it a `gen.toml` file like so:
2. Create a new directory and in it a `gen.toml` file like so:

```toml
[config]
Expand All @@ -29,71 +24,196 @@ AMM = { local = "../move/amm" }
FooPackage = { id = "0x12345" }
```

3) Run the generator from inside the directory: `sui-client-gen`
4) Run the linter / formatter on the generated code: `pnpm eslint . --fix`
3. Run the generator from inside the directory: `sui-client-gen`
4. Run the linter / formatter on the generated code: `pnpm eslint . --fix`

## Usage Examples

### Import generated functions and structs

```ts
import { faucetMint } from './gen/fixture/example-coin/functions'
import { createPoolWithCoins } from './gen/amm/util/functions'
import { createExampleStruct, specialTypes } from './gen/examples/examples/functions'
import { Pool } from './gen/amm/pool/structs'
import { faucetMint } from "./gen/fixture/example-coin/functions";
import { createPoolWithCoins } from "./gen/amm/util/functions";
import {
createExampleStruct,
specialTypes,
} from "./gen/examples/examples/functions";
import { Pool } from "./gen/amm/pool/structs";
```

### Create an AMM pool

```ts
const txb = new TransactionBlock()
const txb = new TransactionBlock();

const [suiCoin] = txb.splitCoin(tx.gas, [tx.pure(1_000_000)])
const exampleCoin = faucetMint(txb, FAUCET_ID)
const [suiCoin] = txb.splitCoin(tx.gas, [tx.pure(1_000_000)]);
const exampleCoin = faucetMint(txb, FAUCET_ID);

const lp = createPoolWithCoins(
txb,
['0x2:sui::SUI', `${EXAMPLE_PACKAGE_ID}::example_coin::EXAMPLE_COIN`],
{
registry: REGISTRY_ID, // or txb.object(REGISTRY_ID)
initA: suiCoin,
initB: exampleCoin,
lpFeeBps: 30n, // or txb.pure(30n)
adminFeePct: 10n // or txb.pure(10n)
}
)
tx.transferObjects([lp], txb.pure(addresss))

await signer.signAndExecuteTransactionBlock({ transactionBLock: txb })
txb,
["0x2:sui::SUI", `${EXAMPLE_PACKAGE_ID}::example_coin::EXAMPLE_COIN`],
{
registry: REGISTRY_ID, // or txb.object(REGISTRY_ID)
initA: suiCoin,
initB: exampleCoin,
lpFeeBps: 30n, // or txb.pure(30n)
adminFeePct: 10n, // or txb.pure(10n)
}
);
tx.transferObjects([lp], txb.pure(addresss));

await signer.signAndExecuteTransactionBlock({ transactionBLock: txb });
```

### Fetch Pool object

```ts
const pool = await Pool.fetch(provider, POOL_ID)
import { EXAMPLE_COIN } from "./gen/my-coin/example-coin/structs";
import { SUI } from "./gen/sui/sui/structs";

// see following section for explanation about "reified"
const poolReified = Pool.r(SUI.p, EXAMPLE_COIN.p); // or Pool.reified(SUI.phantom(), EXAMPLE_COIN.phantom())

const pool = await poolReified.fetch(client, POOL_ID);

// alternatively
const res = await provider.getObject({ id: POOL_ID, options: { showContent: true } })
const pool = Pool.fromSuiParsedData(res.data.content)
const res = await client.getObject({
id: POOL_ID,
options: { showContent: true },
});
const pool = poolReified.fromSuiParsedData(res.data.content);

console.log(pool);
```

### Reified

The structs code generated by `sui-client-gen` is fully type safe, including for structs with type parameters (generics). This means that:

console.log(pool)
- when loading objects from external sources (e.g. from the chain via the `fetch` call), the type of the object is checked against the type parameter at runtime
- object's generic fields are also type inferred, so the type of the field is known statically at compile time, including for any number of nested generic fields or vectors

This is achieved by using the so-called "reified" types. For example, the `Pool` struct has two phantom type parameters, `A` and `B`, which represent the types of the two assets in the pool. Its Move definition looks like this:

```move
struct Pool<phantom A, phantom B> has key { ... }
```

### Special types
So when the generator is run, it will generate classes for each type defined in the package, including for the `Pool` struct. The `Pool` class on its cannot be instantiated directly, but instead has to be "reified" with the types of the assets in the pool.
This is done by calling the `Pool.r` (shorthand for `Pool.reified`) method, which returns a new class with the type parameters filled in:

```ts
const e1 = createExampleStruct(txb)
const e2 = createExampleStruct(txb)
const reified = Pool.r(SUI.p, EXAMPLE_COIN.p);

// alternatively
const reified = Pool.reified(SUI.phantom(), EXAMPLE_COIN.phantom());
// in case of phantom parameters we can also pass in arbitrary types as strings:
const reified = Pool.reified(
phantom("0x2::sui::SUI"),
phantom(`${EXAMPLE_PACKAGE_ID}::example_coin::EXAMPLE_COIN`)
);
```

The `reified` class can then be used to fetch objects from the chain (or other things, such as decoding it from BCS or serialized JSON), which will be checked against the type parameters at runtime and decoded:

```ts
const pool = await reified.fetch(client, POOL_ID);
const pool = reified.fromBcs(bcsBytes);
const pool = reified.fromJSON(jsonData);
```

In case our struct recieves non-phantom type parameters, we need to pass in the reified types as instead of phantom. For example, the `ExampleStruct` struct has a non-phantom type parameter `T`:

```move
struct ExampleStruct<T> has key { ... }
```

So when reifying it, we need to pass in reified types as arguments. For example, this will construct a reified type for `ExampleStruct` with `T` set to `Pool` (concretely, `ExampleStruct<Pool<SUI, EXAMPLE_COIN>>`):

```ts
const reified = ExampleStruct.r(Pool.r(SUI.p, EXAMPLE_COIN.p));
```

Now when we fetch an object of type `ExampleStruct<Pool<SUI, EXAMPLE_COIN>>` from the chain by ID, its type will be checked against the type parameter `T` at runtime (so that we're not accidentally fetching an object of different type), and the inherent generic field, in this case `Pool`, will be correctly decoded and statically inferred.

#### Wrapping reified types in higher level classes

For each struct, the generator will also generate a handy type alias for the reified type, which can be used to wrap the reified type in a higher level class. For example, the `Pool` struct has `PoolReified` alias generated for it.

So now a higher level class that wraps the reified type can be defined like so:

```ts
import { PhantomTypeArgument } from "./gen/_framework/reified"; // it's TypeArgument for non-phantom types
import { PoolReified } from "./gen/amm/pool/structs";

class PoolWrapper<
A extends PhantomTypeArgument,
B extends PhantomTypeArgument
> {
readonly reified: PoolReified<A, B>;

constructor(reified: PoolReified<A, B>) {
this.reified = reified;
}
}
```

And then used like so with the type inferrence being carried over to the higher level class:

```ts
const poolWrapper = new PoolWrapper(Pool.r(SUI.p, EXAMPLE_COIN.p));
```

And if for whatever reason we don't want to define our wrapper classes as generic, we can use the `PhantomTypeArgument` (or `TypeArgument` for non-phantom) type to define them as non-generic (which will sever the type inferrence):

```ts
class PoolWrapper {
readonly reified: PoolReified<PhantomTypeArgument, PhantomTypeArgument>;

constructor(reified: PoolReified<PhantomTypeArgument, PhantomTypeArgument>) {
this.reified = reified;
}
}
```

### Function binding special type handling

The following types:

- `std::ascii::String` and `std::string::String`
- `sui::object::ID`
- `vector<T>` where either:
- `T` is a valid type for a pure inputs, checked resursively
- is a vector of objects
- is a vector of results from previous calls in the same transaction block
- `std::option::Option`

Have special handling so that they can be used directly as inputs to function bindings instead
of having to manually construct them with `txb.pure(...)`:

```ts
const e1 = createExampleStruct(txb);
const e2 = createExampleStruct(txb);

specialTypes(txb, {
asciiString: 'example ascii string', // or txb.pure('example ascii string', BCS.STRING)
utf8String: 'example utf8 string', // or txb.pure('example utf8 string', BCS.STRING)
vectorOfU64: [1n, 2n], // or txb.pure([1n, 2n], 'vector<u64>')
vectorOfObjects: [e1, e2], // or txb.makeMoveVec({ objects: [e1, e2], type: ExampleStruct.$typeName })
idField: '0x12345', // or txb.pure(normalizeSuiAddress('0x12345'), BCS.ADDRESS)
address: '0x12345', // or txb.pure(normalizeSuiAddress('0x12345'), BCS.ADDRESS)
optionSome: 5n, // or txb.pure([5n], 'vector<u64>')
optionNone: null, // or txb.pure([], 'vector<u64>')
})
asciiString: "example ascii string", // or txb.string('example ascii string')
utf8String: "example utf8 string", // or txb.string('example utf8 string')
vectorOfU64: [1n, 2n], // or txb.pure([1n, 2n], 'vector<u64>')
vectorOfObjects: [e1, e2], // or txb.makeMoveVec({ objects: [e1, e2], type: ExampleStruct.$typeName })
idField: "0x12345", // or txb.address(normalizeSuiAddress('0x12345'), BCS.ADDRESS)
address: "0x12345", // or txb.pure(normalizeSuiAddress('0x12345'), BCS.ADDRESS)
optionSome: 5n, // or txb.pure([5n], 'vector<u64>')
optionNone: null, // or txb.pure([], 'vector<u64>')
});
```

## Caveats

- When specifying both source and on-chain packages, the generator will currently generate two separate dependency graphs (one for on-chain and one for source). This is due to a technical detail and will be resolved in a future version so that only a single dependency graph is generated (https://github.com/kunalabs-io/sui-client-gen/issues/1#issuecomment-1554754842).
- Since whitespace detection relies on some Rust nightly features which are currently unstable (https://github.com/udoprog/genco/issues/39#issuecomment-1569076737), the generated code is not formatted nicely. Usage of formatters on the generated code (e.g., `prettier`, `eslint`) is recommended.
- Because ESLint renames some types (e.g., `String` -> `string`) due to the `@typescript-eslint/ban-types` rule which breaks the generated code, an `.eslintrc.json` file is generated in the root directory to turn off this rule.
- When re-running the generator, the files generated on previous run will _not_ be automatically deleted in order to avoid accidental data wipes. The old files can either be deleted manually before re-running the tool (it's safe to delete everything aside from `gen.toml`) or by running the generator with `--clean` (use with caution).

## Docs

For more detailed usage documentation, check out the [docs](https://github.com/kunalabs-io/sui-client-gen/blob/master/DOC.md).
Expand Down

0 comments on commit a3a8f69

Please sign in to comment.