From a3a8f697b76e6b28dd1cc9645c54a369509b3334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kre=C5=A1imir=20Klas?= Date: Thu, 25 Jan 2024 17:22:26 +0100 Subject: [PATCH] update README.md for reified --- README.md | 212 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 166 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index acca0be4..36b70947 100644 --- a/README.md +++ b/README.md @@ -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] @@ -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 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 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>`): + +```ts +const reified = ExampleStruct.r(Pool.r(SUI.p, EXAMPLE_COIN.p)); +``` + +Now when we fetch an object of type `ExampleStruct>` 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; + + constructor(reified: PoolReified) { + 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; + + constructor(reified: PoolReified) { + this.reified = reified; + } +} +``` + +### Function binding special type handling + +The following types: + +- `std::ascii::String` and `std::string::String` +- `sui::object::ID` +- `vector` 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') - 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') - optionNone: null, // or txb.pure([], 'vector') -}) + 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') + 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') + optionNone: null, // or txb.pure([], 'vector') +}); ``` +## 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).