Ver código fonte

Add support for Anchor (#48)

Loris Leiva 1 ano atrás
pai
commit
64809195f4
37 arquivos alterados com 475 adições e 15 exclusões
  1. 5 0
      .changeset/popular-tomatoes-train.md
  2. 11 2
      .github/workflows/main.yml
  3. 4 0
      .gitmodules
  4. 18 8
      index.ts
  5. 2 0
      locales/en-US.json
  6. 2 0
      locales/fr-FR.json
  7. 1 0
      projects/counter-anchor
  8. 1 0
      scripts/utils.mjs
  9. 6 0
      template/anchor/base/Anchor.toml.njk
  10. 6 0
      template/anchor/base/Cargo.toml
  11. 23 0
      template/anchor/base/program/Cargo.toml.njk
  12. 0 0
      template/anchor/base/program/README.md.njk
  13. 49 0
      template/anchor/base/program/src/lib.rs.njk
  14. 103 0
      template/anchor/clients/js/clients/js/test/_setup.ts
  15. 43 0
      template/anchor/clients/js/clients/js/test/create.test.ts
  16. 115 0
      template/anchor/clients/js/clients/js/test/increment.test.ts.njk
  17. 6 0
      template/anchor/clients/rust/Cargo.toml
  18. 47 0
      template/anchor/clients/rust/clients/rust/tests/create.rs.njk
  19. 2 0
      template/clients/base/scripts/generate-clients.mjs.njk
  20. 0 0
      template/shank/base/program/Cargo.toml.njk
  21. 3 0
      template/shank/base/program/README.md.njk
  22. 0 0
      template/shank/base/program/src/assertions.rs
  23. 0 0
      template/shank/base/program/src/entrypoint.rs
  24. 0 0
      template/shank/base/program/src/error.rs
  25. 0 0
      template/shank/base/program/src/instruction.rs
  26. 0 0
      template/shank/base/program/src/lib.rs.njk
  27. 0 0
      template/shank/base/program/src/processor.rs
  28. 0 0
      template/shank/base/program/src/state.rs
  29. 0 0
      template/shank/base/program/src/utils.rs
  30. 0 0
      template/shank/clients/js/clients/js/test/_setup.ts
  31. 0 0
      template/shank/clients/js/clients/js/test/create.test.ts
  32. 0 0
      template/shank/clients/js/clients/js/test/increment.test.ts.njk
  33. 0 0
      template/shank/clients/rust/clients/rust/tests/create.rs.njk
  34. 0 1
      utils/getInputs.ts
  35. 2 0
      utils/getLanguage.ts
  36. 4 0
      utils/getRenderContext.ts
  37. 22 4
      utils/solanaCli.ts

+ 5 - 0
.changeset/popular-tomatoes-train.md

@@ -0,0 +1,5 @@
+---
+"create-solana-program": patch
+---
+
+Add support for Anchor

+ 11 - 2
.github/workflows/main.yml

@@ -33,7 +33,11 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
+        project: ["counter-anchor", "counter-shank"]
         solana: ["1.17.24", "1.18.12"]
+        include:
+          - anchor: "0.30.0"
+            project: "counter-anchor"
     steps:
       - name: Git checkout
         uses: actions/checkout@v4
@@ -52,8 +56,13 @@ jobs:
         uses: metaplex-foundation/actions/install-solana@v1
         with:
           version: ${{ matrix.solana }}
+      - name: Install Anchor
+        if: ${{ matrix.anchor }}
+        uses: metaplex-foundation/actions/install-anchor-cli@v1
+        with:
+          version: ${{ matrix.anchor }}
       - name: Pre-scaffold projects for caching purposes
-        run: pnpm snapshot --scaffold-only
+        run: pnpm snapshot ${{ matrix.project }} --scaffold-only
       - name: Cache cargo crates
         uses: actions/cache@v4
         with:
@@ -68,7 +77,7 @@ jobs:
           restore-keys: |
             ${{ runner.os }}-crates-solana-v${{ matrix.solana }}
       - name: Build and run tests
-        run: pnpm test
+        run: pnpm snapshot ${{ matrix.project }} --test
 
   release:
     name: Release

+ 4 - 0
.gitmodules

@@ -1,3 +1,7 @@
+[submodule "projects/counter-anchor"]
+path = projects/counter-anchor
+url = git@github.com:solana-program/counter-anchor.git
+
 [submodule "projects/counter-shank"]
 path = projects/counter-shank
 url = git@github.com:solana-program/counter-shank.git

+ 18 - 8
index.ts

@@ -1,6 +1,7 @@
 #!/usr/bin/env node
 
 import * as path from 'node:path';
+import * as fs from 'node:fs';
 
 import { createOrEmptyTargetDirectory } from './utils/fsHelpers';
 import { getInputs } from './utils/getInputs';
@@ -9,6 +10,7 @@ import { logBanner, logDone, logStep } from './utils/getLogs';
 import { RenderContext, getRenderContext } from './utils/getRenderContext';
 import { renderTemplate } from './utils/renderTemplates';
 import {
+  detectAnchorVersion,
   detectSolanaVersion,
   generateKeypair,
   patchSolanaDependencies,
@@ -28,12 +30,21 @@ import {
     inputs.shouldOverride
   );
 
-  // Detect the solana version.
+  // Detect the Solana version.
   const solanaVersionDetected = await logStep(
     language.infos.detectSolanaVersion,
     () => detectSolanaVersion(language)
   );
 
+  // Detect the Anchor version.
+  let anchorVersionDetected: string | undefined;
+  if (inputs.programFramework === 'anchor') {
+    anchorVersionDetected = await logStep(
+      language.infos.detectAnchorVersion,
+      () => detectAnchorVersion(language)
+    );
+  }
+
   // Generate a keypair if needed.
   const programAddress =
     inputs.programAddress ??
@@ -53,6 +64,7 @@ import {
     inputs,
     programAddress,
     solanaVersionDetected,
+    anchorVersionDetected,
   });
 
   // Render the templates.
@@ -63,7 +75,7 @@ import {
     ),
     async () => {
       renderTemplates(ctx);
-      await patchSolanaDependencies(ctx.targetDirectory, ctx.solanaVersion);
+      await patchSolanaDependencies(ctx);
     }
   );
 
@@ -74,22 +86,20 @@ import {
 function renderTemplates(ctx: RenderContext) {
   const render = (templateName: string) => {
     const directory = path.resolve(ctx.templateDirectory, templateName);
+    if (!fs.existsSync(directory)) return;
     renderTemplate(ctx, directory, ctx.targetDirectory);
   };
 
   render('base');
-
-  if (ctx.programFramework === 'anchor') {
-    render('programs/counter-anchor');
-  } else {
-    render('programs/counter-shank');
-  }
+  render(`${ctx.programFramework}/base`);
 
   if (ctx.clients.length > 0) {
     render('clients/base');
+    render(`${ctx.programFramework}/clients/base`);
   }
 
   ctx.clients.forEach((client) => {
     render(`clients/${client}`);
+    render(`${ctx.programFramework}/clients/${client}`);
   });
 }

+ 2 - 0
locales/en-US.json

@@ -47,6 +47,7 @@
     "message": "Rust client crate name:"
   },
   "errors": {
+    "anchorCliNotFound": "Command `$command` unavailable. Please install the Anchor CLI.",
     "cannotOverrideDirectory": "Cannot override target directory \"$targetDirectory\". Run with option --force to override.",
     "invalidSolanaVersion": "Invalid Solana version: $version.",
     "operationCancelled": "Operation cancelled",
@@ -62,6 +63,7 @@
     "multiselect": "[↑/↓]: Select / [space]: Toggle selection / [a]: Toggle all / [enter]: Submit answer"
   },
   "infos": {
+    "detectAnchorVersion": "Detect Anchor version",
     "detectSolanaVersion": "Detect Solana version",
     "generateKeypair": "Generate program keypair",
     "scaffold": "Scaffold project in $targetDirectory",

+ 2 - 0
locales/fr-FR.json

@@ -50,6 +50,7 @@
     "message": "Ajouter TypeScript\u00a0?"
   },
   "errors": {
+    "anchorCliNotFound": "Commande `$command` indisponible. Veuillez installer Anchor dans votre terminal.",
     "cannotOverrideDirectory": "Impossible de remplacer le répertoire cible \"$targetDirectory\". Exécutez avec l'option --force pour remplacer.",
     "invalidSolanaVersion": "Version Solana invalide\u00a0: $version.",
     "operationCancelled": "Operation annulée",
@@ -65,6 +66,7 @@
     "multiselect": "[↑/↓]: Sélectionner / [espace]: Basculer la sélection / [a]: Basculer tout / [entrée]: Valider"
   },
   "infos": {
+    "detectAnchorVersion": "Détect la version d'Anchor",
     "detectSolanaVersion": "Détect la version de Solana",
     "generateKeypair": "Génére la paire de clés du program",
     "scaffold": "Génére le projet dans $targetDirectory",

+ 1 - 0
projects/counter-anchor

@@ -0,0 +1 @@
+Subproject commit 33f2be392a25d06be9bc39626a483662a96f0b52

+ 1 - 0
scripts/utils.mjs

@@ -1,6 +1,7 @@
 export const COUNTER_ADDRESS = 'CounterProgram111111111111111111111111111111';
 export const CLIENTS = ['js', 'rust'];
 export const PROJECTS = {
+  'counter-anchor': ['counter', '--anchor', '--address', COUNTER_ADDRESS],
   'counter-shank': ['counter', '--shank', '--address', COUNTER_ADDRESS],
 };
 

+ 6 - 0
template/anchor/base/Anchor.toml.njk

@@ -0,0 +1,6 @@
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"
+
+[programs.localnet]
+{{ programName | snakeCase }} = "{{ programAddress }}"

+ 6 - 0
template/anchor/base/Cargo.toml

@@ -0,0 +1,6 @@
+[workspace]
+resolver = "2"
+members = ["program"]
+
+[profile.release]
+overflow-checks = true

+ 23 - 0
template/anchor/base/program/Cargo.toml.njk

@@ -0,0 +1,23 @@
+[package]
+name = "{{ programCrateName }}"
+version = "0.0.0"
+edition = "2021"
+readme = "./README.md"
+license-file = "../LICENSE"
+publish = false
+
+[package.metadata.solana]
+program-id = "{{ programAddress }}"
+program-dependencies = []
+
+[lib]
+crate-type = ["cdylib", "lib"]
+
+[features]
+no-entrypoint = []
+cpi = ["no-entrypoint"]
+idl-build = ["anchor-lang/idl-build"]
+
+[dependencies]
+anchor-lang = "{{ anchorVersion }}"
+solana-program = "~{{ solanaVersion }}"

+ 0 - 0
template/programs/counter-shank/program/README.md.njk → template/anchor/base/program/README.md.njk


+ 49 - 0
template/anchor/base/program/src/lib.rs.njk

@@ -0,0 +1,49 @@
+use anchor_lang::prelude::*;
+
+declare_id!("{{ programAddress }}");
+
+#[program]
+mod {{ programCrateName | snakeCase }} {
+    use super::*;
+
+    pub fn create(ctx: Context<Create>, authority: Pubkey) -> Result<()> {
+        let counter = &mut ctx.accounts.counter;
+        counter.authority = authority;
+        counter.count = 0;
+        Ok(())
+    }
+
+    pub fn increment(ctx: Context<Increment>) -> Result<()> {
+        let counter = &mut ctx.accounts.counter;
+        counter.count += 1;
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct Create<'info> {
+    #[account(init, payer = payer, space = 8 + 40)]
+    pub counter: Account<'info, Counter>,
+    #[account(mut)]
+    pub payer: Signer<'info>,
+    pub system_program: Program<'info, System>,
+}
+
+#[derive(Accounts)]
+pub struct Increment<'info> {
+    #[account(mut, has_one = authority @ CounterError::InvalidAuthority)]
+    pub counter: Account<'info, Counter>,
+    pub authority: Signer<'info>,
+}
+
+#[account]
+pub struct Counter {
+    pub authority: Pubkey,
+    pub count: u64,
+}
+
+#[error_code]
+pub enum {{ programName | pascalCase }}Error {
+    #[msg("The provided authority doesn't match the counter account's authority")]
+    InvalidAuthority,
+}

+ 103 - 0
template/anchor/clients/js/clients/js/test/_setup.ts

@@ -0,0 +1,103 @@
+import {
+  Address,
+  Commitment,
+  CompilableTransactionMessage,
+  TransactionMessageWithBlockhashLifetime,
+  Rpc,
+  RpcSubscriptions,
+  SolanaRpcApi,
+  SolanaRpcSubscriptionsApi,
+  TransactionSigner,
+  airdropFactory,
+  appendTransactionMessageInstruction,
+  createSolanaRpc,
+  createSolanaRpcSubscriptions,
+  createTransactionMessage,
+  generateKeyPairSigner,
+  getSignatureFromTransaction,
+  lamports,
+  pipe,
+  sendAndConfirmTransactionFactory,
+  setTransactionMessageFeePayerSigner,
+  setTransactionMessageLifetimeUsingBlockhash,
+  signTransactionMessageWithSigners,
+} from '@solana/web3.js';
+import { getCreateInstruction } from '../src';
+
+type Client = {
+  rpc: Rpc<SolanaRpcApi>;
+  rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
+};
+
+export const createDefaultSolanaClient = (): Client => {
+  const rpc = createSolanaRpc('http://127.0.0.1:8899');
+  const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
+  return { rpc, rpcSubscriptions };
+};
+
+export const generateKeyPairSignerWithSol = async (
+  client: Client,
+  putativeLamports: bigint = 1_000_000_000n
+) => {
+  const signer = await generateKeyPairSigner();
+  await airdropFactory(client)({
+    recipientAddress: signer.address,
+    lamports: lamports(putativeLamports),
+    commitment: 'confirmed',
+  });
+  return signer;
+};
+
+export const createDefaultTransaction = async (
+  client: Client,
+  feePayer: TransactionSigner
+) => {
+  const { value: latestBlockhash } = await client.rpc
+    .getLatestBlockhash()
+    .send();
+  return pipe(
+    createTransactionMessage({ version: 0 }),
+    (tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
+    (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
+  );
+};
+
+export const signAndSendTransaction = async (
+  client: Client,
+  transactionMessage: CompilableTransactionMessage &
+    TransactionMessageWithBlockhashLifetime,
+  commitment: Commitment = 'confirmed'
+) => {
+  const signedTransaction =
+    await signTransactionMessageWithSigners(transactionMessage);
+  const signature = getSignatureFromTransaction(signedTransaction);
+  await sendAndConfirmTransactionFactory(client)(signedTransaction, {
+    commitment,
+  });
+  return signature;
+};
+
+export const getBalance = async (client: Client, address: Address) =>
+  (await client.rpc.getBalance(address, { commitment: 'confirmed' }).send())
+    .value;
+
+export const createCounterForAuthority = async (
+  client: Client,
+  authority: TransactionSigner
+): Promise<Address> => {
+  const [transaction, counter] = await Promise.all([
+    createDefaultTransaction(client, authority),
+    generateKeyPairSigner(),
+  ]);
+  const createIx = getCreateInstruction({
+    counter,
+    payer: authority,
+    authority: authority.address,
+  });
+  await pipe(
+    transaction,
+    (tx) => appendTransactionMessageInstruction(createIx, tx),
+    (tx) => signAndSendTransaction(client, tx)
+  );
+  return counter.address;
+};

+ 43 - 0
template/anchor/clients/js/clients/js/test/create.test.ts

@@ -0,0 +1,43 @@
+import {
+  Account,
+  appendTransactionMessageInstruction,
+  generateKeyPairSigner,
+  pipe,
+} from '@solana/web3.js';
+import test from 'ava';
+import { Counter, fetchCounter, getCreateInstruction } from '../src';
+import {
+  createDefaultSolanaClient,
+  createDefaultTransaction,
+  generateKeyPairSignerWithSol,
+  signAndSendTransaction,
+} from './_setup';
+
+test('it creates a new counter account', async (t) => {
+  // Given an authority key pair with some SOL.
+  const client = createDefaultSolanaClient();
+  const [authority, counter] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSigner(),
+  ]);
+
+  // When we create a new counter account.
+  const createIx = getCreateInstruction({
+    authority: authority.address,
+    counter,
+    payer: authority,
+  });
+  await pipe(
+    await createDefaultTransaction(client, authority),
+    (tx) => appendTransactionMessageInstruction(createIx, tx),
+    (tx) => signAndSendTransaction(client, tx)
+  );
+
+  // Then we expect the counter account to exist and have a value of 0.
+  t.like(await fetchCounter(client.rpc, counter.address), <Account<Counter>>{
+    data: {
+      authority: authority.address,
+      count: 0n,
+    },
+  });
+});

+ 115 - 0
template/anchor/clients/js/clients/js/test/increment.test.ts.njk

@@ -0,0 +1,115 @@
+import {
+  SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE,
+  appendTransactionMessageInstruction,
+  generateKeyPairSigner,
+  isProgramError,
+  isSolanaError,
+  lamports,
+  pipe,
+} from '@solana/web3.js';
+import test from 'ava';
+import {
+  {{ programName | snakeCase | upper }}_ERROR__INVALID_AUTHORITY,
+  {{ programName | snakeCase | upper }}_PROGRAM_ADDRESS,
+  fetchCounter,
+  getIncrementInstruction,
+} from '../src';
+import {
+  createCounterForAuthority,
+  createDefaultSolanaClient,
+  createDefaultTransaction,
+  generateKeyPairSignerWithSol,
+  getBalance,
+  signAndSendTransaction,
+} from './_setup';
+
+test('it increments an existing counter by 1', async (t) => {
+  // Given an authority key pair with an associated counter account of value 0.
+  const client = createDefaultSolanaClient();
+  const authority = await generateKeyPairSignerWithSol(client);
+  const counter = await createCounterForAuthority(client, authority);
+  t.is((await fetchCounter(client.rpc, counter)).data.count, 0n);
+
+  // When we increment the counter account.
+  const incrementIx = getIncrementInstruction({ authority, counter });
+  await pipe(
+    await createDefaultTransaction(client, authority),
+    (tx) => appendTransactionMessageInstruction(incrementIx, tx),
+    (tx) => signAndSendTransaction(client, tx)
+  );
+
+  // Then we expect the counter account to have a value of 1.
+  t.is((await fetchCounter(client.rpc, counter)).data.count, 1n);
+});
+
+test('it cannot increment a counter that does not exist', async (t) => {
+  // Given an authority key pair with no associated counter account.
+  const client = createDefaultSolanaClient();
+  const authority = await generateKeyPairSignerWithSol(client);
+  const counter = (await generateKeyPairSigner()).address;
+  t.is(await getBalance(client, counter), lamports(0n));
+
+  // When we try to increment the inexistent counter account.
+  const incrementIx = getIncrementInstruction({ authority, counter });
+  const transactionMessage = pipe(
+    await createDefaultTransaction(client, authority),
+    (tx) => appendTransactionMessageInstruction(incrementIx, tx)
+  );
+  const promise = signAndSendTransaction(client, transactionMessage);
+
+  // Then we expect the program to throw an error.
+  const error = await t.throwsAsync(promise);
+  t.true(
+    isSolanaError(
+      error,
+      SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE
+    )
+  );
+  t.true(
+    isProgramError(
+      error.cause,
+      transactionMessage,
+      {{ programName | snakeCase | upper }}_PROGRAM_ADDRESS,
+      3012 // Account not initialized.
+    )
+  );
+});
+
+test('it cannot increment a counter that belongs to another authority', async (t) => {
+  // Given two authority key pairs such that
+  // only one of them (authority A) is associated with a counter account.
+  const client = createDefaultSolanaClient();
+  const [authorityA, authorityB] = await Promise.all([
+    generateKeyPairSignerWithSol(client),
+    generateKeyPairSignerWithSol(client),
+  ]);
+  const counter = await createCounterForAuthority(client, authorityA);
+
+  // When authority B tries to increment the counter account of authority A.
+  const incrementIx = getIncrementInstruction({
+    authority: authorityB,
+    counter,
+  });
+  const transactionMessage = pipe(
+    await createDefaultTransaction(client, authorityB),
+    (tx) => appendTransactionMessageInstruction(incrementIx, tx)
+  );
+  const promise = signAndSendTransaction(client, transactionMessage);
+
+  // Then we expect the program to throw an error.
+  const error = await t.throwsAsync(promise);
+  t.true(
+    isSolanaError(
+      error,
+      SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE
+    )
+  );
+  t.true(
+    isProgramError(
+      error.cause,
+      transactionMessage,
+      {{ programName | snakeCase | upper }}_PROGRAM_ADDRESS,
+      {{ programName | snakeCase | upper }}_ERROR__INVALID_AUTHORITY
+    )
+  );
+});

+ 6 - 0
template/anchor/clients/rust/Cargo.toml

@@ -0,0 +1,6 @@
+[workspace]
+resolver = "2"
+members = ["clients/rust", "program"]
+
+[profile.release]
+overflow-checks = true

+ 47 - 0
template/anchor/clients/rust/clients/rust/tests/create.rs.njk

@@ -0,0 +1,47 @@
+#![cfg(feature = "test-sbf")]
+
+use {{ rustClientCrateName | snakeCase }}::{accounts::Counter, instructions::CreateBuilder};
+use borsh::BorshDeserialize;
+use solana_program_test::{tokio, ProgramTest};
+use solana_sdk::{signature::Signer, signer::keypair::Keypair, transaction::Transaction};
+
+#[tokio::test]
+async fn create() {
+    let mut context = ProgramTest::new("{{ programCrateName | snakeCase }}", {{ rustClientCrateName | snakeCase }}::ID, None)
+        .start_with_context()
+        .await;
+
+    // Given a new empty account.
+
+    let address = Keypair::new();
+
+    let ix = CreateBuilder::new()
+        .counter(address.pubkey())
+        .authority(context.payer.pubkey())
+        .payer(context.payer.pubkey())
+        .instruction();
+
+    // When we create a new counter at this address.
+
+    let tx = Transaction::new_signed_with_payer(
+        &[ix],
+        Some(&context.payer.pubkey()),
+        &[&context.payer, &address],
+        context.last_blockhash,
+    );
+    context.banks_client.process_transaction(tx).await.unwrap();
+
+    // Then an account was created with the correct data.
+
+    let account = context.banks_client.get_account(address.pubkey()).await.unwrap();
+
+    assert!(account.is_some());
+
+    let account = account.unwrap();
+    assert_eq!(account.data.len(), Counter::LEN);
+
+    let mut account_data = account.data.as_ref();
+    let counter = Counter::deserialize(&mut account_data).unwrap();
+    assert_eq!(counter.authority, context.payer.pubkey());
+    assert_eq!(counter.count, 0);
+}

+ 2 - 0
template/clients/base/scripts/generate-clients.mjs.njk

@@ -21,6 +21,7 @@ kinobi.update(
   })
 );
 
+{% if programFramework === 'shank' %}
 // Update accounts.
 kinobi.update(
   k.updateAccountsVisitor({
@@ -65,6 +66,7 @@ kinobi.update(
     counter: key("counter"),
   })
 );
+{% endif %}
 
 {% if jsClient %}
 // Render JavaScript.

+ 0 - 0
template/programs/counter-shank/program/Cargo.toml.njk → template/shank/base/program/Cargo.toml.njk


+ 3 - 0
template/shank/base/program/README.md.njk

@@ -0,0 +1,3 @@
+# {{ programName | titleCase }}
+
+Your generated Solana program. Have fun!

+ 0 - 0
template/programs/counter-shank/program/src/assertions.rs → template/shank/base/program/src/assertions.rs


+ 0 - 0
template/programs/counter-shank/program/src/entrypoint.rs → template/shank/base/program/src/entrypoint.rs


+ 0 - 0
template/programs/counter-shank/program/src/error.rs → template/shank/base/program/src/error.rs


+ 0 - 0
template/programs/counter-shank/program/src/instruction.rs → template/shank/base/program/src/instruction.rs


+ 0 - 0
template/programs/counter-shank/program/src/lib.rs.njk → template/shank/base/program/src/lib.rs.njk


+ 0 - 0
template/programs/counter-shank/program/src/processor.rs → template/shank/base/program/src/processor.rs


+ 0 - 0
template/programs/counter-shank/program/src/state.rs → template/shank/base/program/src/state.rs


+ 0 - 0
template/programs/counter-shank/program/src/utils.rs → template/shank/base/program/src/utils.rs


+ 0 - 0
template/clients/js/clients/js/test/_setup.ts → template/shank/clients/js/clients/js/test/_setup.ts


+ 0 - 0
template/clients/js/clients/js/test/create.test.ts → template/shank/clients/js/clients/js/test/create.test.ts


+ 0 - 0
template/clients/js/clients/js/test/increment.test.ts.njk → template/shank/clients/js/clients/js/test/increment.test.ts.njk


+ 0 - 0
template/clients/rust/clients/rust/tests/create.rs.njk → template/shank/clients/rust/clients/rust/tests/create.rs.njk


+ 0 - 1
utils/getInputs.ts

@@ -150,7 +150,6 @@ async function getInputsFromPrompts(
               title: language.programFramework.selectOptions!.anchor.title,
               description: language.programFramework.selectOptions!.anchor.desc,
               value: 'anchor',
-              disabled: true,
             },
           ],
         },

+ 2 - 0
utils/getLanguage.ts

@@ -29,6 +29,7 @@ export interface Language {
   jsClientPackageName: LanguageItem;
   rustClientCrateName: LanguageItem;
   errors: {
+    anchorCliNotFound: string;
     cannotOverrideDirectory: string;
     invalidSolanaVersion: string;
     operationCancelled: string;
@@ -44,6 +45,7 @@ export interface Language {
     multiselect: string;
   };
   infos: {
+    detectAnchorVersion: string;
     detectSolanaVersion: string;
     generateKeypair: string;
     scaffold: string;

+ 4 - 0
utils/getRenderContext.ts

@@ -10,6 +10,7 @@ import {
 import { toMinorSolanaVersion } from './solanaCli';
 
 export type RenderContext = Omit<Inputs, 'programAddress' | 'solanaVersion'> & {
+  anchorVersion: string;
   clientDirectory: string;
   clients: Client[];
   currentDirectory: string;
@@ -29,11 +30,13 @@ export function getRenderContext({
   language,
   programAddress,
   solanaVersionDetected,
+  anchorVersionDetected,
 }: {
   inputs: Inputs;
   language: Language;
   programAddress: string;
   solanaVersionDetected: string;
+  anchorVersionDetected?: string;
 }): RenderContext {
   const packageManager = getPackageManager();
   const clients = allClients.flatMap((client) =>
@@ -54,6 +57,7 @@ export function getRenderContext({
 
   return {
     ...inputs,
+    anchorVersion: anchorVersionDetected ?? '',
     clientDirectory,
     clients,
     currentDirectory,

+ 22 - 4
utils/solanaCli.ts

@@ -1,4 +1,5 @@
 import { Language } from './getLanguage';
+import { RenderContext } from './getRenderContext';
 import {
   hasCommand,
   readStdout,
@@ -24,20 +25,37 @@ export async function detectSolanaVersion(language: Language): Promise<string> {
   return version!;
 }
 
+export async function detectAnchorVersion(language: Language): Promise<string> {
+  const hasAnchorCli = await hasCommand('anchor');
+  if (!hasAnchorCli) {
+    throw new Error(
+      language.errors.solanaCliNotFound.replace('$command', 'anchor')
+    );
+  }
+
+  const child = spawnCommand('anchor', ['--version']);
+  const [stdout] = await Promise.all([
+    readStdout(child),
+    waitForCommand(child),
+  ]);
+
+  const version = stdout.join('').match(/(\d+\.\d+\.\d+)/)?.[1];
+  return version!;
+}
+
 export async function patchSolanaDependencies(
-  targetDirectory: string,
-  solanaVersion: string
+  ctx: Pick<RenderContext, 'solanaVersion' | 'targetDirectory'>
 ): Promise<void> {
   const patchMap: Record<string, string[]> = {
     '1.17': ['-p ahash@0.8 --precise 0.8.6'],
   };
 
-  const patches = patchMap[solanaVersion] ?? [];
+  const patches = patchMap[ctx.solanaVersion] ?? [];
   await Promise.all(
     patches.map((patch) =>
       waitForCommand(
         spawnCommand('cargo', ['update', ...patch.split(' ')], {
-          cwd: targetDirectory,
+          cwd: ctx.targetDirectory,
         })
       )
     )