metaplex-foundation / umi-hotline

2 stars 0 forks source link

Kinobi client building throws #6

Closed KultureElectric closed 1 year ago

KultureElectric commented 1 year ago

Umi version

No response

Code

*Not sure if this is the right place to report a kinobi error but was led here from Metaplex Discord.

I'm creating a kinobi javascript client and have a typeerror in the account files which prevents me from building to publish it to npm:

It happens in the state deserialization function where the return type from that function does not match with the type that the deserializeAccount function returns.

export function deserializeNftState(
  context: Pick<Context, 'serializer'>,
  rawAccount: RpcAccount
): NftState {
  return deserializeAccount(
    rawAccount,
    getNftStateAccountDataSerializer(context)
  );
}

I was able to fix this by simply changing the type of 'NftState' in my case to using the accounDataArgs type instead of the NftStateAccountData type

export type NftState = Account<NftStateAccountDataArgs>;

Error

src/generated/accounts/nftState.ts:60:3 - error TS2322: Type 'Account<NftStateAccountDataArgs>' is not assignable to type 'NftState'.
  Property 'discriminator' is missing in type 'Account<NftStateAccountDataArgs>' but required in type 'NftStateAccountData'.

60   return deserializeAccount(
     ~~~~~~~~~~~~~~~~~~~~~~~~~~
61     rawAccount,
   ~~~~~~~~~~~~~~~
62     getNftStateAccountDataSerializer(context)
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
63   );
   ~~~~

  src/generated/accounts/nftState.ts:26:3
    26   discriminator: Array<number>;
         ~~~~~~~~~~~~~
    'discriminator' is declared here.

src/generated/accounts/redemption.ts:70:3 - error TS2322: Type 'Account<RedemptionAccountDataArgs>' is not assignable to type 'Redemption'.
  Property 'discriminator' is missing in type 'Account<RedemptionAccountDataArgs>' but required in type 'RedemptionAccountData'.

70   return deserializeAccount(
     ~~~~~~~~~~~~~~~~~~~~~~~~~~
71     rawAccount,
   ~~~~~~~~~~~~~~~
72     getRedemptionAccountDataSerializer(context)
   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
73   );
   ~~~~

  src/generated/accounts/redemption.ts:27:3
    27   discriminator: Array<number>;
         ~~~~~~~~~~~~~
    'discriminator' is declared here.

Found 2 errors.
lorisleiva commented 1 year ago

Hey 👋 thanks for raising this.

I'm struggling to understand how you'd get this error thought because the Account<T> type should always wrap the AccountData (which gets deserialises) and not the AccountDataArgs (which is used when serialising into a buffer).

Could you share the code that gets generated (the whole account file) as well as your Kinobi config file?

KultureElectric commented 1 year ago

Hey, sorry for the delayed response.

Here's my kinobi.js file:

const path = require("path");
const {
  RenderJavaScriptVisitor,
  createFromIdls,
} = require("@metaplex-foundation/kinobi");

// Instanciate Kinobi.
const kinobi = createFromIdls([
  path.join(__dirname, "idl", "repay_royalties_contract.json"),
]);

// Update the Kinobi tree using visitors...

// Render JavaScript.
const jsDir = path.join(__dirname, "clients", "js", "src", "generated");
kinobi.accept(new RenderJavaScriptVisitor(jsDir));

I only got this config from the UMI docs, so happy if you supply me with an updated way (just waiting for the docs)

And here's the entire account file:

/**
 * This code was AUTOGENERATED using the kinobi library.
 * Please DO NOT EDIT THIS FILE, instead use visitors
 * to add features, then rerun kinobi to update it.
 *
 * @see https://github.com/metaplex-foundation/kinobi
 */

import {
  Account,
  Context,
  PublicKey,
  RpcAccount,
  RpcGetAccountOptions,
  RpcGetAccountsOptions,
  Serializer,
  assertAccountExists,
  deserializeAccount,
  gpaBuilder,
  mapSerializer,
} from '@metaplex-foundation/umi';

export type NftState = Account<NftStateAccountData>;

export type NftStateAccountData = {
  discriminator: Array<number>;
  mint: PublicKey;
  repayTimestamp: bigint;
};

export type NftStateAccountDataArgs = {
  mint: PublicKey;
  repayTimestamp: number | bigint;
};

export function getNftStateAccountDataSerializer(
  context: Pick<Context, 'serializer'>
): Serializer<NftStateAccountDataArgs, NftStateAccountData> {
  const s = context.serializer;
  return mapSerializer<NftStateAccountDataArgs, any, NftStateAccountData>(
    s.struct<NftStateAccountData>(
      [
        ['discriminator', s.array(s.u8(), { size: 8 })],
        ['mint', s.publicKey()],
        ['repayTimestamp', s.i64()],
      ],
      { description: 'NftStateAccountData' }
    ),
    (value) => ({
      ...value,
      discriminator: [31, 9, 202, 242, 100, 75, 163, 110],
    })
  ) as Serializer<NftStateAccountDataArgs, NftStateAccountData>;
}

export function deserializeNftState(
  context: Pick<Context, 'serializer'>,
  rawAccount: RpcAccount
): NftState {
  return deserializeAccount(
    rawAccount,
    getNftStateAccountDataSerializer(context)
  );
}

export async function fetchNftState(
  context: Pick<Context, 'rpc' | 'serializer'>,
  publicKey: PublicKey,
  options?: RpcGetAccountOptions
): Promise<NftState> {
  const maybeAccount = await context.rpc.getAccount(publicKey, options);
  assertAccountExists(maybeAccount, 'NftState');
  return deserializeNftState(context, maybeAccount);
}

export async function safeFetchNftState(
  context: Pick<Context, 'rpc' | 'serializer'>,
  publicKey: PublicKey,
  options?: RpcGetAccountOptions
): Promise<NftState | null> {
  const maybeAccount = await context.rpc.getAccount(publicKey, options);
  return maybeAccount.exists
    ? deserializeNftState(context, maybeAccount)
    : null;
}

export async function fetchAllNftState(
  context: Pick<Context, 'rpc' | 'serializer'>,
  publicKeys: PublicKey[],
  options?: RpcGetAccountsOptions
): Promise<NftState[]> {
  const maybeAccounts = await context.rpc.getAccounts(publicKeys, options);
  return maybeAccounts.map((maybeAccount) => {
    assertAccountExists(maybeAccount, 'NftState');
    return deserializeNftState(context, maybeAccount);
  });
}

export async function safeFetchAllNftState(
  context: Pick<Context, 'rpc' | 'serializer'>,
  publicKeys: PublicKey[],
  options?: RpcGetAccountsOptions
): Promise<NftState[]> {
  const maybeAccounts = await context.rpc.getAccounts(publicKeys, options);
  return maybeAccounts
    .filter((maybeAccount) => maybeAccount.exists)
    .map((maybeAccount) =>
      deserializeNftState(context, maybeAccount as RpcAccount)
    );
}

export function getNftStateGpaBuilder(
  context: Pick<Context, 'rpc' | 'serializer' | 'programs'>
) {
  const s = context.serializer;
  const programId = context.programs.getPublicKey(
    'repayRoyaltiesContract',
    '9ZskGH9wtdwM9UXjBq1KDwuaLfrZyPChz41Hx7NWhTFf'
  );
  return gpaBuilder(context, programId)
    .registerFields<{
      discriminator: Array<number>;
      mint: PublicKey;
      repayTimestamp: number | bigint;
    }>({
      discriminator: [0, s.array(s.u8(), { size: 8 })],
      mint: [8, s.publicKey()],
      repayTimestamp: [40, s.i64()],
    })
    .deserializeUsing<NftState>((account) =>
      deserializeNftState(context, account)
    )
    .whereField('discriminator', [31, 9, 202, 242, 100, 75, 163, 110]);
}

export function getNftStateSize(): number {
  return 48;
}

This is what I get outputted and then I just manually change the NftState type to use NftStateAccountDataArgs instead of NftStateAccountData

Lastly I'm also interested how I can get the PDA helper function to be generated by Kinobi

Thanks!

lorisleiva commented 1 year ago

Thanks for that, it is super weird because on my side TypeScript understands that it needs the To type and not the From type of the serializer but I've updated the deserializeAccount function so it explicitly takes the To type parameter so there's no ambiguity. If you upgrade to 0.7.6, that should hopefully fix it for you.

Regarding generating the PDA helpers, you simply need to tell Kinobi about the seeds of the PDA. Here's an example: https://github.com/metaplex-foundation/solana-project-template/blob/016849ddeb966c8548445b00745166f4bc65065f/configs/kinobi.cjs#L14-L21

KultureElectric commented 1 year ago

Hey, coming back here because I get a bug when trying to assign the seeds to my accounts:

Trying to add seeds to the account like this:

kinobi.update(
  new k.UpdateAccountsVisitor({
    identifier: {
      seeds: [k.stringConstantSeed("identifier"), k.programSeed()],
    },
    stakeEntry: {
      seeds: [
        k.stringConstantSeed("stake-entry"),
        k.publicKeySeed("pool", "The address of the stake pool"),
        k.publicKeySeed("mint", "The address of the NFT Mint"),
        k.publicKeySeed("user", "The address of the user"),
        k.programSeed(),
      ],
    },
    stakePool: {
      seeds: [k.stringConstantSeed("stake-pool"), k.programSeed()],
    },
  })
);

Weird thing is this generates correct helper functions for the stakeEntry but is missing something for both the stakePool and Identifier account.

In generated/account/stakeEntry it looks like this:

export async function fetchStakeEntryFromSeeds(
  context: Pick<Context, 'eddsa' | 'programs' | 'rpc' | 'serializer'>,
  seeds: Parameters<typeof findStakeEntryPda>[1],
  options?: RpcGetAccountOptions
): Promise<StakeEntry> {
  return fetchStakeEntry(context, findStakeEntryPda(context, seeds), options);
}

export async function safeFetchStakeEntryFromSeeds(
  context: Pick<Context, 'eddsa' | 'programs' | 'rpc' | 'serializer'>,
  seeds: Parameters<typeof findStakeEntryPda>[1],
  options?: RpcGetAccountOptions
): Promise<StakeEntry | null> {
  return safeFetchStakeEntry(
    context,
    findStakeEntryPda(context, seeds),
    options
  );
}

Here these 2 functions have a seeds argument that has to be filled by the caller of the function.

In my other 2 account files this seeds argument to the function doesn't exist which causes a typescript error since seeds is needed for the findStakeEntryPda function

export async function fetchStakePoolFromSeeds(
  context: Pick<Context, 'eddsa' | 'programs' | 'rpc' | 'serializer'>,
  options?: RpcGetAccountOptions
): Promise<StakePool> {
  return fetchStakePool(context, findStakePoolPda(context, seeds), options);
}

export async function safeFetchStakePoolFromSeeds(
  context: Pick<Context, 'eddsa' | 'programs' | 'rpc' | 'serializer'>,
  options?: RpcGetAccountOptions
): Promise<StakePool | null> {
  return safeFetchStakePool(context, findStakePoolPda(context, seeds), options);
}

I might just be missing something in my kinobi.js file but couldn't get it to work. Looking forward to a response!

KultureElectric commented 1 year ago

Seems like this seeds parameter in the fetchFromSeed functions seems to be missing when there are no variable seeds being defined in the kinobi.js file.

It works now for my stake_pool and stake_entry accounts since I have variable seeds for both.

For the identifier I only have the constantString see since this PDA is only there to identify how many pools have been created. If I'd add another variable seed in the kinobi.js file the outputted client would not throw any errors.

But since I only have this constantStringSeed it does.

Here the entire Kinobi.js file

const path = require("path");
const k = require("@metaplex-foundation/kinobi");

k.numberT;

// Paths.
const clientDir = path.join(__dirname, "./", "clients");
const idlDir = path.join(__dirname, "./idl/");

// Instanciate Kinobi.
const kinobi = k.createFromIdls([
  path.join(idlDir, "cardinal_staking_starter.json"),
]);

// Update accounts.
kinobi.update(
  new k.UpdateAccountsVisitor({
    identifier: {
      seeds: [k.stringConstantSeed("identifier")],
    },
    stakeEntry: {
      seeds: [
        k.stringConstantSeed("stake-entry"),
        k.publicKeySeed("pool", "The address of the stake pool"),
        k.publicKeySeed("mint", "The address of the NFT Mint"),
        k.publicKeySeed("user", "The address of the user"),
      ],
    },
    stakePool: {
      seeds: [
        k.stringConstantSeed("stake-pool"),
        k.variableSeed(
          "identifier",
          k.numberTypeNode("u64", "le"),
          "The identifier number of the pool"
        ),
      ],
    },
  })
);

// Update instructions.
// kinobi.update(
//   new k.UpdateInstructionsVisitor({
//     create: {
//       bytesCreatedOnChain: k.bytesFromAccount("myAccount"),
//     },
//     // ...
//   })
// );

// Set ShankAccount discriminator.
// const key = (name) => ({ field: "key", value: k.vEnum("Key", name) });
// kinobi.update(
//   new k.SetAccountDiscriminatorFromFieldVisitor({
//     myAccount: key("MyAccount"),
//     myPdaAccount: key("MyPdaAccount"),
//   })
// );

// Render JavaScript.
const jsDir = path.join(clientDir, "js", "src", "generated");
// const prettier = require(path.join(clientDir, "js", ".prettierrc.json"));
kinobi.accept(new k.RenderJavaScriptVisitor(jsDir));
lorisleiva commented 1 year ago

Hi there 👋

Thanks for raising this. I think this might be a bug in Kinobi when an account has no variable seeds. Let me try and reproduce and I'll come back to you.

lorisleiva commented 1 year ago

Can you try Kinobi version 0.8.4 and let me know if that fixes your issue?

KultureElectric commented 1 year ago

Bug solved, thanks!

lorisleiva commented 1 year ago

Awesome!