GithubHelp home page GithubHelp logo

kinobi-so / kinobi Goto Github PK

View Code? Open in Web Editor NEW
39.0 3.0 11.0 1.56 MB

Generate clients, CLIs, documentation and more from your Solana programs

License: MIT License

JavaScript 0.56% TypeScript 85.10% Shell 0.05% Nunjucks 4.32% Rust 9.97%

kinobi's Introduction

Kinobi

npm npm-downloads ci

Kinobi is a tool that describes any Solana program in a powerful standardised format known as the Kinobi IDL. This IDL can then be used to create a variety of utility such as rendering client code for your programs in various languages/frameworks, generating CLIs and providing more information to explorers.

Kinobi header: A small double-sided mind-map with the Kinobi logo in the middle. On the left, we see the various ways to get a Kinobi IDL from your Solana programs such as "Anchor Program" and "Shank macros". On the right, we see the various utility tools that are offered for the IDL such as "Rendering client code" or "Rendering documentation".

Nodes and visitors

The Kinobi IDL is designed as a tree of nodes starting with the RootNode which contains a ProgramNode and additional data such as the Kinobi version used when the IDL was created. Kinobi provides over 60 different types of nodes that help describe just about any aspect of your Solana programs. You can read more about the Kinobi nodes here.

A small example of a Kinobi IDL as a tree of nodes. It starts with a RootNode and goes down to ProgramNode, AccountNode, InstructionNode, etc.

Because everything is designed as a Node, we can transform the IDL, aggregate information and output various utility tools using special objects that can traverse node trees known as visitors. See this documentation to learn more about Kinobi visitors.

A small example of how a visitor can transform a Kinobi IDL into another Kinobi IDL. This example illustrates the "deleteNodesVisitor" which recursively removes NumberTypeNodes from a tree of nested TypleTypeNodes.

From program to Kinobi

There are various ways to extract information from your Solana programs in order to obtain a Kinobi IDL.

  • Using Kinobi macros. This is not yet available but you will soon have access to a set of Rust macros that help attach IDL information directly within your Rust code. These macros enable Kinobi IDLs to be generated whenever you build your programs.

  • From Anchor IDLs. If you are using Anchor programs or Shank macros, then you can get an Anchor IDL from them. You can then use the @kinobi-so/nodes-from-anchor package to convert that IDL into a Kinobi IDL as shown in the code snippet below. Note that the Anchor IDL might not offer all the information that Kinobi can hold and therefore, you may want to transform your Kinobi IDL to provide additional information. You can learn more about this in the next section.

    import { createFromRoot } from 'kinobi';
    import { rootNodeFromAnchor } from '@kinobi-so/nodes-from-anchor';
    import anchorIdl from 'anchor-idl.json';
    
    const kinobi = createFromRoot(rootNodeFromAnchor(anchorIdl));
  • By hand. If your Solana program cannot be updated to use Kinobi macros and you don’t have an Anchor IDL, you may design your Kinobi IDL by hand. We may provide tools such as a Kinobi Playground to help with that in the future.

Transforming Kinobi

Once you have your Kinobi IDL, you may use visitors to transform it. This can be useful when the Kinobi IDL was obtained from another source that may not contain some necessary information. Here is an example using two provided visitors that adjusts the accounts and instructions on the program.

import { updateAccountsVisitor, updateInstructionsVisitor } from 'kinobi';

kinobi.update(updateAccountsVisitor({ ... }));
kinobi.update(updateInstructionsVisitor({ ... }));

From Kinobi to utility

Now that you have the perfect Kinobi IDL for your Solana program, you can benefit from all the visitors and tools that provide utility such as rendering client code or registering your IDL on-chain so explorers can dynamically display relevant information for your program.

Note that some features such as rendering CLIs are not yet available. However, because the Kinobi IDL is designed as a tree of nodes, these features are only a visitor away from being ready. Feel free to reach out if you’d like to contribute to this Kinobi ecosystem.

  • Rendering client code. Want people to start interacting with your Solana program? You can use special visitors that go through your Kinobi IDL and generate client code that you can then publish for your end-users. Currently, we have the following renderers available:

    • @kinobi-so/renderers-js: Renders a JavaScript client compatible with the soon-to-be-released 2.0 line of @solana/web3.js.
    • @kinobi-so/renderers-js-umi: Renders a JavaScript client compatible with Metaplex’s Umi framework.
    • @kinobi-so/renderers-rust: Renders a Rust client that removes the need for publishing the program crate and offers a better developer experience.
    • And more to come.

    Here’s an example of how to generate JavaScript and Rust client code for your program.

    import { renderJavaScriptVisitor, renderRustVisitor } from '@kinobi-so/renderers';
    
    kinobi.accept(renderJavaScriptVisitor('clients/js/src/generated', { ... }));
    kinobi.accept(renderRustVisitor('clients/rust/src/generated', { ... }));
  • Registering your Kinobi IDL on-chain (Coming soon). Perhaps the biggest benefit of having a Kinobi IDL from your program is that you can share it on-chain with the rest of the ecosystem. This means explorers may now use this information to provide a better experience for users of your programs. Additionally, anyone can now grab your Kinobi IDL, select the portion they are interested in and benefit from the same ecosystem of Kinobi visitors to iterate over it. For instance, an app could decide to grab the IDLs of all programs they depend on, filter out the accounts and instructions they don’t need and generate a bespoke client for their app that only contains the functions the app needs.

  • Rendering CLIs (Not yet available). Whilst not available yet, we can imagine a set of CLI commands that can be generated from our Kinobi IDL (much like our clients) so that end-users can fetch decoded accounts and send instructions directly from their terminal.

  • Rendering documentation (Not yet available). Similarly to CLIs, we may easily generate documentation in various formats from the information held by our Kinobi IDL.

kinobi's People

Contributors

lorisleiva avatar dependabot[bot] avatar github-actions[bot] avatar febo avatar kespinola avatar mcintyre94 avatar lithdew avatar buffalojoec avatar samuelvanderwaal avatar iamvon avatar

Stargazers

mohit avatar buffalu avatar Ryan Conceicao avatar M O T I L O L A avatar 0xStone avatar Guenit avatar Will avatar Anders Murphy avatar 开来超 avatar K V Madhan Raj avatar  avatar  avatar Christopher Carvalho avatar Aadil | mous avatar kitakitsune avatar 0xrinegade avatar  avatar Andrew avatar Pratik Saria avatar Zhe avatar Brandt Cormorant avatar  avatar Mukesh avatar  avatar  avatar Ifiok Jr. avatar Febby avatar SharkChilixyz avatar Will avatar Yutaro Mori avatar Kevin Rodríguez avatar Stone Gao avatar amilz avatar  avatar  avatar Zamiel Chia avatar Sundeep Charan Ramkumar avatar  avatar  avatar

Watchers

 avatar  avatar Brandon Tulsi avatar

kinobi's Issues

[renderers] Support enum with custom number indexes

If enum variants have custom discriminator values, we should consider using them when generating the enum in JavaScript or Rust.

enum MyEnum {
  First = 1,
  Second,
  Fifth = 5,
}

The same goes for discriminated unions in JavaScript which is going to be a combination of the type discriminator and/or the codec's options.

Add package that dynamically resolve IDL components

Currently, we only use the Kinobi IDL statically by generating code that can they be used by consumers to interact with the program.

We also need a dynamic version of this such that any app can dynamically interact with a program's components (accounts, instructions, types, errors, etc.) by simply providing it's Kinobi IDL.

This would be hugely beneficial for apps like explorer that need to access that information on demand.

This package would offer various helper functions that would always accept a Kinobi IDL first and then any required parameters afterwards.

For instance:

  • getAccountCodec(rootNode, accountName), getDefinedTypeCodec(rootNode, typeName), getInstructionCodec(rootNode, instructionName), etc.: Returns a web3.js Codec for a given component. Encoder / decoder versions can also be provided.
  • decodeAccount(rootNode, accountName, rawData), decodeInstruction(rootNode, instructionName, rawData), decodeDefinedType(rootNode, typeName, rawData), etc.: Similar but decodes directly.
  • identifyAccount(rootNode, rawData), identifyInstruction(rootNode, rawData), etc.: Tells us which components we are dealing with based on its raw data.

[renderers-js] Improve data enum helper when using a tuple variant with a single item

When a EnumTupleVariantTypeNode contains a single item in the enum, we should transform the data enum helper such that it requires that item's value directly instead of having to wrap it in an array.

For instance, consider the following enum:

definedTypeNode({
  name: 'message',
  type: enumTypeNode([
    enumTupleVariantTypeNode(
      'write',
      tupleTypeNode([numberTypeNode('u16')])
    )
  ]),
})

Then instead of generating the following helper:

const myMessage = message('write', [42]);

We could simplify it to:

const myMessage = message('write', 42);

Add default value to `variablePdaSeedNode`

This could be useful when we know the value of a PDA seed most of the time but would like to allow users to override it when necessary.

For instance, in the following example, we could parameterize the tokenProgram seed whilst making the SPL token the default value.

const node = pdaNode({
    name: 'associatedToken',
    seeds: [
        variablePdaSeedNode('mint', publicKeyTypeNode()),
        constantPdaSeedNode(
            publicKeyTypeNode(),
            publicKeyValueNode('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'),
        ),
        variablePdaSeedNode('owner', publicKeyTypeNode()),
    ],
});

Remove `hooked` and `importFrom` ability from link nodes

This is a proposal that needs further discussion before implementing.


Currently, the importFrom attribute of link nodes such as DefinedTypeLinkNode enable users to opt-out of defining the type using TypeNodes and instead grab them from a hooked folder or a named module (such as a package for JS or a crate for Rust using a dependency map).

Whilst this enables renderers to inject "custom types" into the generated code, it is a hole in the information tree and something that will affect explorer or anyone else requiring that information.

This issue propose that we no longer allow link nodes to "link outside of the tree" to avoid information loss.

As for enabling custom type injection for renderers, this could be achieved using special renderer options that lets the visitor know which defined type should be replaced by a custom one. This may even be a better outcome as it'll allow each renderer to decide if they want to inject custom code or not.

Use a single EnumVariantTypeNode that can use any TypeNode as data

This is a proposal that needs further discussion before implementing.


Currently, we have three kinds of EnumVariantTypeNode:

  • EmptyEnumVariantTypeNode: For variants with no data, e.g. Message.Quit.
  • TupleEnumVariantTypeNode: For variants with data of type TupleTypeNode, e.g. Message.Move(x, y).
  • StructEnumVariantTypeNode: For variants with data of type StructTypeNode, e.g. Message.Login { username, password }.

This is mimicking the Rust behaviour but the Kinobi IDL is language agnostic and should be use to describe data structure regardless of the language or framework used by the program or clients.

As such we could instead combine these three nodes into a single EnumVariantTypeNode that can optionally accept any TypeNode.

{
  kind: 'enumVariantTypeNode',
  name: string,
  type?: TypeNode,
}

Now, say we had to use that node to render a Rust enum. We could use the following logic to achieve this taks:

  • No type attribute → Render an empty variant.
  • type attribute of type StructTypeNode → Render a struct variant.
  • type attribute of type TupleTypeNode → Render a tuple variant.
  • type attribute of any other type → Render a tuple variant such that it's only item is that type.

Additionally, we could consider accepting a DefinedTypeLinkNode on top of TypeNode which would allow enum variants to link to defined types instead of having to wrap them in TupleTypeNodes to achieve this.

Update option type nodes

In preparation for TP4 of the new web3.js which consolidates option codecs, we should adjust the OptionTypeNodes and the code they render.

Currently, we have an OptionTypeNode which assumes a prefixed Option type and a ZeroableOptionTypeNode which assumes a fixed, un-prefixed Option type. We need to add support for a "remainder option type node" which returns None if and only if no more bytes are available on the buffer.

This can be done in several ways:

  1. We add yet another option type node like RemainderOptionTypeNode.
  2. We consolidate all option type nodes in a single OptionTypeNode that can be configured to supports all three scenarios as TP4 of the new web3.js did. The node attributes could look like this:
    some: TypeNode,
    prefix?: NumberTypeNode
    none?: ConstantTypeNode | "zeroes"

Opinion: I think 2 might be more elegant as, with 1, it's unclear that OptionTypeNode is the prefixed one.

Write a CLI renderer

Write a renderer that produces a CLI that enables devs to send instructions, deserialise types and fetch deserialised accounts.

This can be achieve in many different ways such as rendering the whole code in JS or Rust or simply by relying on a generated JS package or Rust crate to provide the main code and simply provide a CLI wrapper around it.

Add visitors that add more components inside a `ProgramNode`

It would be useful to have visitors such as addAccountsVisitor, addInstructionsVisitor, addDefinedTypesVisitor, addErrorsVisitor, etc.

By default these visitors would add the provided nodes to the main program of the RootNode.

// Example.
kinobi.update(
  addDefinedTypesVisitor([
    definedTypeNode({ name: 'slot', type: numberTypeNode('u64') }),
    definedTypeNode({ name: 'hash', type: publicKeyTypeNode() })
  ])
);

However, we could also support an additional signature that would look for the correct ProgramNode using the provided program name.

// Example.
kinobi.update(
  addDefinedTypesVisitor('someAdditionalProgram', [
    definedTypeNode({ name: 'slot', type: numberTypeNode('u64') }),
    definedTypeNode({ name: 'hash', type: publicKeyTypeNode() })
  ])
);

[renderers-js] Include account/instruction enum in parsed data

When parsing an account or instruction from its raw data, it would be useful to also include a type attribute that uses the generate account or instruction enums such as MyProgramInstruction.Transfer.

That way, someone can use that type attribute to easily distinguish between the objects they've parsed.

Use any TypeNode for Account data

This is a proposal that needs further discussion before implementing.


Currently, the data of an AccountNode must be a StructAccountNode meaning its top-level data-structure must include named fields.

Technically, since data is just data, we could imagine an AccountNode with a simple BooleanTypeNode as data.

The issue with this approach is some other nodes such as the FieldDiscriminatorNode points to a field on the account and therefore expects the data to be a struct with named fields.

Improve node data for inlined PdaValueNodes

Since #43, we can now inline PdaNodes directly inside PdaValueNodes, allowing us to defined PDAs that are not defined at the top-level of the program, and therefore, don't need to render helper methods.

PR #42 is in the process of supporting these inlined PDAs when parsing an Anchor IDL.

This issue proposes two further improvements that needs discussing.

  • A. Add a default Anchor visitor after #42, that goes through relevant inlined PDAs and extracts them as top-level PdaNodes on the ProgramNode.

  • B. Potentially find a way to link an inlined variable Pda seed to an instruction argument to avoid type repetition. Note that this may make it more difficult for us to achieve A as top-level PdaNodes do not have access to the context of instruction arguments.

Cc @kespinola

Delete `IdentityValueNode`?

Since we already have a PayerValueNode, do we also need a IdentityValueNode?

Both of these aim to refer to the "main wallet" we are using. For instance, if you are inside an app, that's the connected wallet. If you are inside a terminal, that's whoever solana address is, etc.

Since Kinobi was initially created to render Umi packages and Umi distinguishes between the "main wallet that pays for things" (i.e. the payer) and the "main wallet that gets things" (i.e. the identity), there was a need for both nodes.

However, since Kinobi is now aiming to be a framework agnostic standard that describe programs, does it make sense to still have that distinction regardless of Umi (we can always add that information on the Umi renderer itself to avoid breaking changes).

Personally, I'm on the fence with this one. I still think the distinction makes sense but I'm not sure if having two "main wallet" pointer is more confusing than helping.

Explore instruction bundle nodes

There is still a lot to explore here so this issue mainly offer a space to discuss this in more detail before committing to this.

The idea here is to describe bundles of instructions that are typically used together to create more complex operations.

For instance createAccount from the System program and initializeMint from the Token program could both be part of a createMint bundle which would link to these two instructions and provide a mapping regarding the accounts and arguments that refer to the same reference on both instructions. For instance, the created account on the createAccount instruction would also match the mint account of the initializeMint instruction.

By describing these instruction bundles, we allow renderers to offer helper methods for these, CLIs to offer more useful commands and documentation to provide more useful examples.

Consider moving account data to defined types and linking it using a `DefinedTypeLinkNode`

This is a proposal that needs further discussion before implementing.


Similarly to how AccountNodes link to PdaNodes using PdaLinkNodes, we could extract the account data to a defined type like the new Anchor spec does.

This assumes this issue is implemented first: #66.

The issue with this approach is that DefinedTypeLinkNode can point to external data structures (hooked or custom package). Meaning we would loose all information about the account data on the IDL.

[renderers-js] Render `SolAmountTypeNode` as `Lamports`

The SolAmountTypeNode represents an amount in Lamports, and wraps an arbitrary number type node. Currently the implementation in renderers-js defers to that wrapped number type:

visitSolAmountType(solAmountType, { self }) {
return visit(solAmountType.number, self);
},

This means that it renders as either a number or bigint depending on that wrapped number, with a loose type of number | bigint in the latter case.

We could instead render it with the web3js type LamportsUnsafeBeyond2Pow53Minus1, which is a branded bigint. We'd then use the lamports encoder/decoder/codec from web3js. This would mean that when something is typed SolAmountTypeNode in the IDL, it ends up typed as the web3js Lamports type.

This will require a small change to the web3js lamports codec. Currently it uses a default u64 codec with no configuration. We should make it instead optionally wrap any arbitrary number codec.

Some values of bigint cannot be represented by all number types, and non-integers can't be represented by bigint, so there will be some incompatible values. But for integer values representable by the wrapped number type, this should work correctly in both directions.

Name Conflict with Anchor V01 IDL Types

Issue

Generated types have name conflicts with accounts.

Config

import path from "path";
import { fileURLToPath } from "url";
import {
  renderRustVisitor,
  renderJavaScriptUmiVisitor,
} from "@kinobi-so/renderers";
import { rootNodeFromAnchorWithoutDefaultVisitor } from "@kinobi-so/nodes-from-anchor";
import { readJson } from "@kinobi-so/renderers-core";
import { visit } from "@kinobi-so/visitors-core";
import fs from "fs/promises";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const clientDir = path.join(__dirname, "clients");
const idlDir = path.join(__dirname, "target", "idl");

const idlFiles = await fs.readdir(idlDir);

for (const idlFile of idlFiles) {
  const idlPath = path.join(idlDir, idlFile);
  const idl = readJson(idlPath);

  const node = rootNodeFromAnchorWithoutDefaultVisitor(idl);

  const sdkName = idl.metadata.name;

  await visit(
    node,
    renderJavaScriptUmiVisitor(
      path.join(clientDir, "js", sdkName, "src", "generated")
    )
  );

  await visit(
    node,
    renderRustVisitor(
      path.join(clientDir, "rust", sdkName, "src", "generated"),
      { format: true }
    )
  );

Logs

src/generated/index.ts:14:1 - error TS2308: Module './accounts' has already exported a member named 'ApproveAccount'. Consider explicitly re-exporting to resolve the ambiguity.

14 export * from './types';
   ~~~~~~~~~~~~~~~~~~~~~~~~

src/generated/index.ts:14:1 - error TS2308: Module './accounts' has already exported a member named 'ApproveAccountArgs'. Consider explicitly re-exporting to resolve the ambiguity.

14 export * from './types';
   ~~~~~~~~~~~~~~~~~~~~~~~~

src/generated/index.ts:14:1 - error TS2308: Module './accounts' has already exported a member named 'Manager'. Consider explicitly re-exporting to resolve the ambiguity.

14 export * from './types';
   ~~~~~~~~~~~~~~~~~~~~~~~~

src/generated/index.ts:14:1 - error TS2308: Module './accounts' has already exported a member named 'ManagerArgs'. Consider explicitly re-exporting to resolve the ambiguity.

14 export * from './types';
   ~~~~~~~~~~~~~~~~~~~~~~~~

src/generated/index.ts:14:1 - error TS2308: Module './accounts' has already exported a member named 'TokenGroup'. Consider explicitly re-exporting to resolve the ambiguity.

14 export * from './types';
   ~~~~~~~~~~~~~~~~~~~~~~~~

src/generated/index.ts:14:1 - error TS2308: Module './accounts' has already exported a member named 'TokenGroupArgs'. Consider explicitly re-exporting to resolve the ambiguity.

14 export * from './types';
   ~~~~~~~~~~~~~~~~~~~~~~~~
  context: {
    __code: 1200008,
    formattedHistogram: 'errors: 4',
    validationItems: [
      {
        level: 'error',
        message: 'The "approveAccount" defined type exports a "ApproveAccount" type that conflicts with the "ApproveAccount" type exported by the "approveAccount" account.\n' +
          '|> Conflicting stack: [rootNode] > [programNode]wenNewStandard.',
        node: {
          kind: 'definedTypeNode',
          name: 'approveAccount',
          docs: [],
          type: { kind: 'structTypeNode', fields: [Array] }
        },
        stack: [
          {
            kind: 'rootNode',
            standard: 'kinobi',
            version: '0.20.2',
            program: [Object],
            additionalPrograms: []
          },
          {
            kind: 'programNode',
            name: 'wenNewStandard',
            publicKey: 'wns1gDLt8fgLcGhWi5MqAqgXpwEP1JftKE9eZnXS1HM',
            version: '0.3.2-alpha',
            origin: 'anchor',
            docs: [],
            accounts: [Array],
            instructions: [Array],
            definedTypes: [Array],
            pdas: [Array],
            errors: [Array]
          }
        ]
      },

Describe instruction events

Since instructions can "emit events" by writing logs to the ledger — e.g. Anchor events, Account compression, etc. — it would be useful to describe these "events" in an framework agnostic way. Perhaps a simple InstructionEventNode array on the InstructionNode with simple name and docs attributes could be enough as a first step?

Generate generic instruction codecs for programs.

The JavaScript renderer currently renders the following fields and types given a program:

export const PASS_PROGRAM_ADDRESS =
  'nabZn3LTGJpknPtSDiqLXZq98VwfYVicy6zDfPu82Cs' as Address<'nabZn3LTGJpknPtSDiqLXZq98VwfYVicy6zDfPu82Cs'>;

export enum PassInstruction {
  CreatePass,
  UpdatePass,
  MintPass,
  MintPass2,
}

export function identifyPassInstruction(
  instruction: { data: Uint8Array } | Uint8Array
): PassInstruction {
  const data =
    instruction instanceof Uint8Array ? instruction : instruction.data;
  if (containsBytes(data, getU8Encoder().encode(0), 0)) {
    return PassInstruction.CreatePass;
  }
  if (containsBytes(data, getU8Encoder().encode(1), 1)) {
    return PassInstruction.UpdatePass;
  }
  if (containsBytes(data, getU8Encoder().encode(2), 2)) {
    return PassInstruction.MintPass;
  }
  if (containsBytes(data, getU8Encoder().encode(3), 3)) {
    return PassInstruction.MintPass2;
  }
  throw new Error(
    'The provided instruction could not be identified as a pass instruction.'
  );
}

export type ParsedPassInstruction<
  TProgram extends string = 'nabZn3LTGJpknPtSDiqLXZq98VwfYVicy6zDfPu82Cs',
> =
  | ({
      instructionType: PassInstruction.CreatePass;
    } & ParsedCreatePassInstruction<TProgram>)
  | ({
      instructionType: PassInstruction.UpdatePass;
    } & ParsedUpdatePassInstruction<TProgram>)
  | ({
      instructionType: PassInstruction.MintPass;
    } & ParsedMintPassInstruction<TProgram>)
  | ({
      instructionType: PassInstruction.MintPass2;
    } & ParsedMintPass2Instruction<TProgram>);

It would be useful to additionally generate a generic instruction codec to decode/encode any of the programs instructions which may work on top of identifyInstruction.

Add `ConstantNodes`

Add support for constants array with a new ConstantNode inside ProgramNode.

Then update the nodes-from-anchor package to include them.

Then update the renderers to render the constants.

Add an `updatePdasVisitor`

In the likes of the other update visitors such as updateInstructionsVisitor, updateAccountsVisitor, etc.

Explore instruction constraints

This is a proposal that needs further discussion before implementing.


It would be useful for the accounts and arguments of an InstructionNode to optionally contain a InstructionConstraintNode. These constraints would define rules for these accounts or arguments such that, if that constraint fails, the provided account or argument will fail to pass the instruction's checks.

Ideas for such constraints could be:

  • PdaConstraintNode: The account or argument (of type PublicKey) must follow the describe derivation.
  • PublicKeyConstraintNode: The account or argument (of type PublicKey) must be equal to the provided address.
  • PublicKeyListConstraintNode: The account or argument (of type PublicKey) must be included in the provided array of addresses.
  • ValueConstraintNode: The argument must be equal to the provided value.
  • HasOneConstraintNode: The account or argument must be present in the provided account and match the value of its attribute.
  • AndConstraintNode: All nested constraints must be valid.
  • OrConstraintNode: At least one nested constraint must be valid.
  • NotConstraintNode: The nested constraint must fail.

Side node: Currently, every account or argument in an InstructionNode has the potential to include a defaultValue which can either be a simple ValueNode or more contextual nodes such as ArgumentValueNode (to default to another argument), AccountValueNode (to default to another account), PdaValueNode (to default to a PDA derivation with seeds coming from other accounts or arguments), ConditionalValueNode (to provide simple if/else values), etc.

That defaultValue is mostly useful for renderers and documentation visitors as they allow the generated helper functions to require as little information as possible.

The constraint nodes cannot replace this defaultValue since constraints alone are not enough to decide which value an account or argument should default to. However, there could be some redundancy here to tackle such as the PdaConstraintNode and the PdaValueNode.

Support bit granularity in type nodes

This is a proposal that needs further discussion before implementing.


Currently, the smallest granularity that Kinobi supports when it comes to type definition is one byte.

However, there may be cases were, within a byte, some bits are being used for different things. For instance, a byte could be used to store 8 booleans using bit flags.

There are several ways we could achieve this.

From one end of the spectrum, we could analyse the concrete use-cases where such thing would be needed and offer specific type nodes from them. For instance, a BitFlagTypeNode could represent an array or struct of booleans such that each boolean is store in a single bit.

On the other end of the spectrum, we could reduce the overall number granularity supported by Kinobi by adding number formats such as u1, u2, etc. This makes the whole type system a lot more flexible as we can now construct things like bit flats using TupleTypeNodes or even StructTypeNodes if we wanted them to be named. The issue with the approach is we now need to handle bit-granularity at every level (Codecs for JS, custom types for Rust, etc.) and ensure we do not end up with incomplete bytes.

[renderers-js] Should safeFetch helpers also be safe from decoding errors?

The safeFetch and safeFetchAll helpers from the JS renderers are currently safe from accounts that are missing on-chain. That is, if the account is missing, the function will return null.

However, if the account exists but the fetched account doesn't follow the expected serialisation of the account, this function will fail instead of returning null.

Is this the expected behaviour? Should we also allow accounts that are not following the correct serialisation to return null? How safe should these safeFetch helpers be?

Add `defaultValueStrategy` to `InstructionAccountNode`?

Currently, InstructionArgumentNodes have a defaultValue attribute as well as a defaultValueStrategy attribute which can either be "omitted" or "optional". This means, we can decide if the default value should be overridable by the end user or if, really, this default value should never be altered.

Since InstructionAccountNodes also have a defaultValue attribute, would it make sense for them to also have the same defaultValueStrategy attribute? and "omitted" strategy would then let the renderers know that the account shouldn't even be present in the input of the instruction helper.

[renderers-js] Expose `dependsOn` and `resolvedInputName` in resolver scope

When generating the resolverScope object which is then passed to every resolver function call, we could provide more available data to enable more powerful and reusable resolver logic.

For instance we could add:

  • The dependsOn array which tells the resolver the arguments and accounts it depends on.
  • The resolvedInputName string which tells the resolver the name of the account or argument currently being resolved.

That way, the same resolver function can be used for multiple values as the dependsOn array and the resolvedInputName string can be used to distinguish between them.

Add generic `PluginNodes` to the `RootNode`

There is still a lot to explore here so this issue mainly offer a space to discuss this in more detail before committing to this.

The main idea here is to allow ecosystem users to extend the Kinobi IDL using customisable PluginNodes that can be understood by anyone that wishes to support that plugin.

For instance, here's a rough implementation proposal for illustration purposes:

type PluginNode<
  TName extends CamelCaseString = CamelCaseString,
  TData extends object = object
> = {
  kind: 'pluginNode';
  name: TName;
  data: TData;
}

Such that:

type RootNode = {
  kind: 'rootNode';
  program: ProgramNode;
  plugins: PluginNode[];
  // ...
}

Now, imagine the following two concrete plugins:

type Language = "en" | "fr" | "es"
type TranslationPluginNode = PluginNode<
  'translation',
  Record<Language, Record<CamelCaseString, { name: string, docs?: string[] }>>
>

type InstructionSummaryPluginNode = PluginNode<
  'instructionSummary',
  Record<CamelCaseString, string>
>

Which could be used in that way:

const plugins: PluginNode[] = [
  {
    kind: 'pluginNode',
    name: 'translation',
    data: {
      'fr': {
        'transferSol': { name: 'transfertDeSol', docs: [...] },
        'nonceState': { name: 'étatDuNonce' },
      },
      'es': {
        'transferSol': { name: 'transferenciaDeSol', docs: [...] },
        'nonceState': { name: 'estadoDelNonce' },
      },
    },
  },
  {
    kind: 'pluginNode',
    name: 'instructionSummary',
    data: {
      'transferSol': 'Transfer $amount SOL from $source to $destination',
      'createAccount': 'Create a new account $account with $space bytes on the $programId program',
    },
  },
];

Find a way to pnpm link the monorepo when doing local tests

Now that Kinobi is a monorepo, it's harder to locally test on other repos since we can no longer just point to the main package. If we try to do that on the library package, it's not happy because of the sub-packages inter-dependencies — I.e. workspace:*.

There must surely be an easy way to do this that I'm not aware of. Please internet.

[renderers-js-umi] Missing 'eddsa' context at instruction render with pdaValueNode

While generating client with js-umi rendered, eddsa context isn't picked for context arg.

Kinobi IDL:

|   |   instructionNode [setPauseV1]
|   |   |   instructionAccountNode [fusionData.writable]
|   |   |   |   pdaValueNode
|   |   |   |   |   pdaNode [fusionData]
|   |   |   |   |   |   constantPdaSeedNode
|   |   |   |   |   |   |   bytesTypeNode
|   |   |   |   |   |   |   bytesValueNode [base16.667573696f6e5f64617461]
|   |   |   instructionAccountNode [authority.writable.signer]
|   |   |   |   identityValueNode
|   |   |   instructionArgumentNode [discriminator]
|   |   |   |   fixedSizeTypeNode [8]
|   |   |   |   |   bytesTypeNode
|   |   |   |   bytesValueNode [base16.48aed1c673030480]
|   |   |   instructionArgumentNode [paused]
|   |   |   |   booleanTypeNode
|   |   |   |   |   numberTypeNode [u8]
|   |   |   fieldDiscriminatorNode [discriminator]

Generated instruction signature:

// Instruction.
export function setPauseV1(
  context: Pick<Context, 'identity' | 'programs'>,
  input: SetPauseV1InstructionAccounts & SetPauseV1InstructionArgs
): TransactionBuilder

Defaults from pda node:

  // Default values.
  if (!resolvedAccounts.fusionData.value) {
    resolvedAccounts.fusionData.value = context.eddsa.findPda(programId, [
      bytes().serialize(
        new Uint8Array([102, 117, 115, 105, 111, 110, 95, 100, 97, 116, 97])
      ),
    ]);
  }

This leads to typescript compilation error.

Related part of the code:

case 'pdaValueNode':
// Inlined PDA value.
if (isNode(defaultValue.pda, 'pdaNode')) {
const pdaProgram = defaultValue.pda.programId
? `context.programs.getPublicKey('${defaultValue.pda.programId}', '${defaultValue.pda.programId}')`
: 'programId';
const pdaSeeds = defaultValue.pda.seeds.flatMap((seed): string[] => {
if (isNode(seed, 'constantPdaSeedNode') && isNode(seed.value, 'programIdValueNode')) {
imports
.add('umiSerializers', 'publicKey')
.addAlias('umiSerializers', 'publicKey', 'publicKeySerializer');
return [`publicKeySerializer().serialize(${pdaProgram})`];
}
if (isNode(seed, 'constantPdaSeedNode') && !isNode(seed.value, 'programIdValueNode')) {
const typeManifest = visit(seed.type, typeManifestVisitor);
const valueManifest = visit(seed.value, typeManifestVisitor);
imports.mergeWith(typeManifest.serializerImports);
imports.mergeWith(valueManifest.valueImports);
return [`${typeManifest.serializer}.serialize(${valueManifest.value})`];
}
if (isNode(seed, 'variablePdaSeedNode')) {
const typeManifest = visit(seed.type, typeManifestVisitor);
const valueSeed = defaultValue.seeds.find(s => s.name === seed.name)?.value;
if (!valueSeed) return [];
if (isNode(valueSeed, 'accountValueNode')) {
imports.mergeWith(typeManifest.serializerImports);
imports.add('shared', 'expectPublicKey');
return [
`${typeManifest.serializer}.serialize(expectPublicKey(resolvedAccounts.${camelCase(valueSeed.name)}.value))`,
];
}
if (isNode(valueSeed, 'argumentValueNode')) {
imports.mergeWith(typeManifest.serializerImports);
imports.add('shared', 'expectSome');
return [
`${typeManifest.serializer}.serialize(expectSome(${argObject}.${camelCase(valueSeed.name)}))`,
];
}
const valueManifest = visit(valueSeed, typeManifestVisitor);
imports.mergeWith(typeManifest.serializerImports);
imports.mergeWith(valueManifest.valueImports);
return [`${typeManifest.serializer}.serialize(${valueManifest.value})`];
}
return [];
});
return render(`context.eddsa.findPda(${pdaProgram}, [${pdaSeeds.join(', ')}])`);
}

I'm not sure adding interfaces.add('eddsa'); before return would be good.

[renderers-js] Instruction input wrappers to override account metas

Currently, the generated instruction helpers make a lot of assumptions about account metas (as defined in the Kinobi IDL) that is impossible for the end-user to override. For instance, right now you cannot change the writable status of your instruction account.

Say I wanted to do this:

const ix = getTransferSolInstruction(tx, {
  source: createNoopSigner(address('1234')),
  destination: address('5678'),
});

But for some reason, I wanted to override the destination account to be a readonly account. Right now, I would need to deep copy the instruction in order to change that. However, if Kinobi accepted something like a asReadonly wrapper, it could become something like this:

const ix = getTransferSolInstruction(tx, {
  source: createNoopSigner(address('1234')),
  destination: asReadonly(address('5678')),
});

We can then take it one step further and have wrappers like asLookupMeta for accounts linked to LUTs.

const ix = getTransferSolInstruction(tx, {
  source: createNoopSigner(address('1234')),
  destination: asLookupMeta(
    address('5678'), /* real address */
    address('9999'), /* lookup address */
    7, /* lookup index */
  ),
});

Offering these extra building blocks could be interesting in the context of generated clients and would allow client users a lot more flexibility.

Under the hood, these wrapper functions could simply construct IAccountMeta type or equivalent. The Kinobi generated function would need to accept these types and treat them accordingly.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.