Skip to content

Commit 6480919

Browse files
authored
Add support for Anchor (#48)
1 parent baede97 commit 6480919

37 files changed

+475
-15
lines changed

.changeset/popular-tomatoes-train.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-solana-program": patch
3+
---
4+
5+
Add support for Anchor

.github/workflows/main.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ jobs:
3333
runs-on: ubuntu-latest
3434
strategy:
3535
matrix:
36+
project: ["counter-anchor", "counter-shank"]
3637
solana: ["1.17.24", "1.18.12"]
38+
include:
39+
- anchor: "0.30.0"
40+
project: "counter-anchor"
3741
steps:
3842
- name: Git checkout
3943
uses: actions/checkout@v4
@@ -52,8 +56,13 @@ jobs:
5256
uses: metaplex-foundation/actions/install-solana@v1
5357
with:
5458
version: ${{ matrix.solana }}
59+
- name: Install Anchor
60+
if: ${{ matrix.anchor }}
61+
uses: metaplex-foundation/actions/install-anchor-cli@v1
62+
with:
63+
version: ${{ matrix.anchor }}
5564
- name: Pre-scaffold projects for caching purposes
56-
run: pnpm snapshot --scaffold-only
65+
run: pnpm snapshot ${{ matrix.project }} --scaffold-only
5766
- name: Cache cargo crates
5867
uses: actions/cache@v4
5968
with:
@@ -68,7 +77,7 @@ jobs:
6877
restore-keys: |
6978
${{ runner.os }}-crates-solana-v${{ matrix.solana }}
7079
- name: Build and run tests
71-
run: pnpm test
80+
run: pnpm snapshot ${{ matrix.project }} --test
7281

7382
release:
7483
name: Release

.gitmodules

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[submodule "projects/counter-anchor"]
2+
path = projects/counter-anchor
3+
url = [email protected]:solana-program/counter-anchor.git
4+
15
[submodule "projects/counter-shank"]
26
path = projects/counter-shank
37
url = [email protected]:solana-program/counter-shank.git

index.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
import * as path from 'node:path';
4+
import * as fs from 'node:fs';
45

56
import { createOrEmptyTargetDirectory } from './utils/fsHelpers';
67
import { getInputs } from './utils/getInputs';
@@ -9,6 +10,7 @@ import { logBanner, logDone, logStep } from './utils/getLogs';
910
import { RenderContext, getRenderContext } from './utils/getRenderContext';
1011
import { renderTemplate } from './utils/renderTemplates';
1112
import {
13+
detectAnchorVersion,
1214
detectSolanaVersion,
1315
generateKeypair,
1416
patchSolanaDependencies,
@@ -28,12 +30,21 @@ import {
2830
inputs.shouldOverride
2931
);
3032

31-
// Detect the solana version.
33+
// Detect the Solana version.
3234
const solanaVersionDetected = await logStep(
3335
language.infos.detectSolanaVersion,
3436
() => detectSolanaVersion(language)
3537
);
3638

39+
// Detect the Anchor version.
40+
let anchorVersionDetected: string | undefined;
41+
if (inputs.programFramework === 'anchor') {
42+
anchorVersionDetected = await logStep(
43+
language.infos.detectAnchorVersion,
44+
() => detectAnchorVersion(language)
45+
);
46+
}
47+
3748
// Generate a keypair if needed.
3849
const programAddress =
3950
inputs.programAddress ??
@@ -53,6 +64,7 @@ import {
5364
inputs,
5465
programAddress,
5566
solanaVersionDetected,
67+
anchorVersionDetected,
5668
});
5769

5870
// Render the templates.
@@ -63,7 +75,7 @@ import {
6375
),
6476
async () => {
6577
renderTemplates(ctx);
66-
await patchSolanaDependencies(ctx.targetDirectory, ctx.solanaVersion);
78+
await patchSolanaDependencies(ctx);
6779
}
6880
);
6981

@@ -74,22 +86,20 @@ import {
7486
function renderTemplates(ctx: RenderContext) {
7587
const render = (templateName: string) => {
7688
const directory = path.resolve(ctx.templateDirectory, templateName);
89+
if (!fs.existsSync(directory)) return;
7790
renderTemplate(ctx, directory, ctx.targetDirectory);
7891
};
7992

8093
render('base');
81-
82-
if (ctx.programFramework === 'anchor') {
83-
render('programs/counter-anchor');
84-
} else {
85-
render('programs/counter-shank');
86-
}
94+
render(`${ctx.programFramework}/base`);
8795

8896
if (ctx.clients.length > 0) {
8997
render('clients/base');
98+
render(`${ctx.programFramework}/clients/base`);
9099
}
91100

92101
ctx.clients.forEach((client) => {
93102
render(`clients/${client}`);
103+
render(`${ctx.programFramework}/clients/${client}`);
94104
});
95105
}

locales/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"message": "Rust client crate name:"
4848
},
4949
"errors": {
50+
"anchorCliNotFound": "Command `$command` unavailable. Please install the Anchor CLI.",
5051
"cannotOverrideDirectory": "Cannot override target directory \"$targetDirectory\". Run with option --force to override.",
5152
"invalidSolanaVersion": "Invalid Solana version: $version.",
5253
"operationCancelled": "Operation cancelled",
@@ -62,6 +63,7 @@
6263
"multiselect": "[↑/↓]: Select / [space]: Toggle selection / [a]: Toggle all / [enter]: Submit answer"
6364
},
6465
"infos": {
66+
"detectAnchorVersion": "Detect Anchor version",
6567
"detectSolanaVersion": "Detect Solana version",
6668
"generateKeypair": "Generate program keypair",
6769
"scaffold": "Scaffold project in $targetDirectory",

locales/fr-FR.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"message": "Ajouter TypeScript\u00a0?"
5151
},
5252
"errors": {
53+
"anchorCliNotFound": "Commande `$command` indisponible. Veuillez installer Anchor dans votre terminal.",
5354
"cannotOverrideDirectory": "Impossible de remplacer le répertoire cible \"$targetDirectory\". Exécutez avec l'option --force pour remplacer.",
5455
"invalidSolanaVersion": "Version Solana invalide\u00a0: $version.",
5556
"operationCancelled": "Operation annulée",
@@ -65,6 +66,7 @@
6566
"multiselect": "[↑/↓]: Sélectionner / [espace]: Basculer la sélection / [a]: Basculer tout / [entrée]: Valider"
6667
},
6768
"infos": {
69+
"detectAnchorVersion": "Détect la version d'Anchor",
6870
"detectSolanaVersion": "Détect la version de Solana",
6971
"generateKeypair": "Génére la paire de clés du program",
7072
"scaffold": "Génére le projet dans $targetDirectory",

projects/counter-anchor

Submodule counter-anchor added at 33f2be3

scripts/utils.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const COUNTER_ADDRESS = 'CounterProgram111111111111111111111111111111';
22
export const CLIENTS = ['js', 'rust'];
33
export const PROJECTS = {
4+
'counter-anchor': ['counter', '--anchor', '--address', COUNTER_ADDRESS],
45
'counter-shank': ['counter', '--shank', '--address', COUNTER_ADDRESS],
56
};
67

template/anchor/base/Anchor.toml.njk

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[provider]
2+
cluster = "localnet"
3+
wallet = "~/.config/solana/id.json"
4+
5+
[programs.localnet]
6+
{{ programName | snakeCase }} = "{{ programAddress }}"

template/anchor/base/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[workspace]
2+
resolver = "2"
3+
members = ["program"]
4+
5+
[profile.release]
6+
overflow-checks = true
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "{{ programCrateName }}"
3+
version = "0.0.0"
4+
edition = "2021"
5+
readme = "./README.md"
6+
license-file = "../LICENSE"
7+
publish = false
8+
9+
[package.metadata.solana]
10+
program-id = "{{ programAddress }}"
11+
program-dependencies = []
12+
13+
[lib]
14+
crate-type = ["cdylib", "lib"]
15+
16+
[features]
17+
no-entrypoint = []
18+
cpi = ["no-entrypoint"]
19+
idl-build = ["anchor-lang/idl-build"]
20+
21+
[dependencies]
22+
anchor-lang = "{{ anchorVersion }}"
23+
solana-program = "~{{ solanaVersion }}"
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use anchor_lang::prelude::*;
2+
3+
declare_id!("{{ programAddress }}");
4+
5+
#[program]
6+
mod {{ programCrateName | snakeCase }} {
7+
use super::*;
8+
9+
pub fn create(ctx: Context<Create>, authority: Pubkey) -> Result<()> {
10+
let counter = &mut ctx.accounts.counter;
11+
counter.authority = authority;
12+
counter.count = 0;
13+
Ok(())
14+
}
15+
16+
pub fn increment(ctx: Context<Increment>) -> Result<()> {
17+
let counter = &mut ctx.accounts.counter;
18+
counter.count += 1;
19+
Ok(())
20+
}
21+
}
22+
23+
#[derive(Accounts)]
24+
pub struct Create<'info> {
25+
#[account(init, payer = payer, space = 8 + 40)]
26+
pub counter: Account<'info, Counter>,
27+
#[account(mut)]
28+
pub payer: Signer<'info>,
29+
pub system_program: Program<'info, System>,
30+
}
31+
32+
#[derive(Accounts)]
33+
pub struct Increment<'info> {
34+
#[account(mut, has_one = authority @ CounterError::InvalidAuthority)]
35+
pub counter: Account<'info, Counter>,
36+
pub authority: Signer<'info>,
37+
}
38+
39+
#[account]
40+
pub struct Counter {
41+
pub authority: Pubkey,
42+
pub count: u64,
43+
}
44+
45+
#[error_code]
46+
pub enum {{ programName | pascalCase }}Error {
47+
#[msg("The provided authority doesn't match the counter account's authority")]
48+
InvalidAuthority,
49+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import {
2+
Address,
3+
Commitment,
4+
CompilableTransactionMessage,
5+
TransactionMessageWithBlockhashLifetime,
6+
Rpc,
7+
RpcSubscriptions,
8+
SolanaRpcApi,
9+
SolanaRpcSubscriptionsApi,
10+
TransactionSigner,
11+
airdropFactory,
12+
appendTransactionMessageInstruction,
13+
createSolanaRpc,
14+
createSolanaRpcSubscriptions,
15+
createTransactionMessage,
16+
generateKeyPairSigner,
17+
getSignatureFromTransaction,
18+
lamports,
19+
pipe,
20+
sendAndConfirmTransactionFactory,
21+
setTransactionMessageFeePayerSigner,
22+
setTransactionMessageLifetimeUsingBlockhash,
23+
signTransactionMessageWithSigners,
24+
} from '@solana/web3.js';
25+
import { getCreateInstruction } from '../src';
26+
27+
type Client = {
28+
rpc: Rpc<SolanaRpcApi>;
29+
rpcSubscriptions: RpcSubscriptions<SolanaRpcSubscriptionsApi>;
30+
};
31+
32+
export const createDefaultSolanaClient = (): Client => {
33+
const rpc = createSolanaRpc('http://127.0.0.1:8899');
34+
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
35+
return { rpc, rpcSubscriptions };
36+
};
37+
38+
export const generateKeyPairSignerWithSol = async (
39+
client: Client,
40+
putativeLamports: bigint = 1_000_000_000n
41+
) => {
42+
const signer = await generateKeyPairSigner();
43+
await airdropFactory(client)({
44+
recipientAddress: signer.address,
45+
lamports: lamports(putativeLamports),
46+
commitment: 'confirmed',
47+
});
48+
return signer;
49+
};
50+
51+
export const createDefaultTransaction = async (
52+
client: Client,
53+
feePayer: TransactionSigner
54+
) => {
55+
const { value: latestBlockhash } = await client.rpc
56+
.getLatestBlockhash()
57+
.send();
58+
return pipe(
59+
createTransactionMessage({ version: 0 }),
60+
(tx) => setTransactionMessageFeePayerSigner(feePayer, tx),
61+
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
62+
);
63+
};
64+
65+
export const signAndSendTransaction = async (
66+
client: Client,
67+
transactionMessage: CompilableTransactionMessage &
68+
TransactionMessageWithBlockhashLifetime,
69+
commitment: Commitment = 'confirmed'
70+
) => {
71+
const signedTransaction =
72+
await signTransactionMessageWithSigners(transactionMessage);
73+
const signature = getSignatureFromTransaction(signedTransaction);
74+
await sendAndConfirmTransactionFactory(client)(signedTransaction, {
75+
commitment,
76+
});
77+
return signature;
78+
};
79+
80+
export const getBalance = async (client: Client, address: Address) =>
81+
(await client.rpc.getBalance(address, { commitment: 'confirmed' }).send())
82+
.value;
83+
84+
export const createCounterForAuthority = async (
85+
client: Client,
86+
authority: TransactionSigner
87+
): Promise<Address> => {
88+
const [transaction, counter] = await Promise.all([
89+
createDefaultTransaction(client, authority),
90+
generateKeyPairSigner(),
91+
]);
92+
const createIx = getCreateInstruction({
93+
counter,
94+
payer: authority,
95+
authority: authority.address,
96+
});
97+
await pipe(
98+
transaction,
99+
(tx) => appendTransactionMessageInstruction(createIx, tx),
100+
(tx) => signAndSendTransaction(client, tx)
101+
);
102+
return counter.address;
103+
};

0 commit comments

Comments
 (0)