new docs (#1999)
|
@ -0,0 +1,3 @@
|
||||||
|
NEXT_PUBLIC_DOCSEARCH_APP_ID=
|
||||||
|
NEXT_PUBLIC_DOCSEARCH_API_KEY=
|
||||||
|
NEXT_PUBLIC_DOCSEARCH_INDEX_NAME=
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
|
@ -1,12 +1,35 @@
|
||||||
pids
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
logs
|
|
||||||
node_modules
|
# dependencies
|
||||||
npm-debug.log
|
/node_modules
|
||||||
coverage/
|
/.pnp
|
||||||
run
|
.pnp.js
|
||||||
dist
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.nyc_output
|
*.pem
|
||||||
.basement
|
|
||||||
config.local.js
|
# debug
|
||||||
basement_dist
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
.vscode/
|
||||||
|
.idea
|
|
@ -0,0 +1,20 @@
|
||||||
|
# Anchor Docs
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Next, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
|
||||||
|
|
||||||
|
## Customizing
|
||||||
|
|
||||||
|
You can start editing this template by modifying the files in the `/src` folder. The site will auto-update as you edit these files.
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Fence } from '@/components/Fence'
|
||||||
|
|
||||||
|
const nodes = {
|
||||||
|
document: {
|
||||||
|
render: undefined,
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
attributes: {
|
||||||
|
scope: {
|
||||||
|
type: String,
|
||||||
|
default: 'col',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: (props) => <th {...props} />,
|
||||||
|
},
|
||||||
|
fence: {
|
||||||
|
render: Fence,
|
||||||
|
attributes: {
|
||||||
|
language: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default nodes
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { Callout } from '@/components/Callout'
|
||||||
|
import { LinkGrid } from '@/components/LinkGrid'
|
||||||
|
|
||||||
|
const tags = {
|
||||||
|
callout: {
|
||||||
|
attributes: {
|
||||||
|
title: { type: String },
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
default: 'note',
|
||||||
|
matches: ['note', 'warning'],
|
||||||
|
errorLevel: 'critical',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
render: Callout,
|
||||||
|
},
|
||||||
|
figure: {
|
||||||
|
selfClosing: true,
|
||||||
|
attributes: {
|
||||||
|
src: { type: String },
|
||||||
|
alt: { type: String },
|
||||||
|
caption: { type: String },
|
||||||
|
},
|
||||||
|
render: ({ src, alt = '', caption }) => (
|
||||||
|
<figure>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img src={src} alt={alt} />
|
||||||
|
<figcaption>{caption}</figcaption>
|
||||||
|
</figure>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'link-grid': {
|
||||||
|
render: LinkGrid,
|
||||||
|
},
|
||||||
|
'link-grid-link': {
|
||||||
|
selfClosing: true,
|
||||||
|
render: LinkGrid.Link,
|
||||||
|
attributes: {
|
||||||
|
title: { type: String },
|
||||||
|
description: { type: String },
|
||||||
|
icon: { type: String },
|
||||||
|
href: { type: String },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default tags
|
|
@ -0,0 +1,16 @@
|
||||||
|
const withMarkdoc = require('@markdoc/next.js')
|
||||||
|
const { withPlausibleProxy } = require('next-plausible')
|
||||||
|
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = withMarkdoc()({
|
||||||
|
swcMinify: true,
|
||||||
|
reactStrictMode: true,
|
||||||
|
pageExtensions: ['js', 'jsx', 'md'],
|
||||||
|
experimental: {
|
||||||
|
newNextLinkBehavior: true,
|
||||||
|
scrollRestoration: true,
|
||||||
|
legacyBrowsers: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = withPlausibleProxy()(nextConfig)
|
|
@ -1,24 +1,38 @@
|
||||||
{
|
{
|
||||||
"name": "anchor",
|
"name": "anchor-docs",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"description": "",
|
"private": true,
|
||||||
"main": "index.js",
|
|
||||||
"authors": {
|
|
||||||
"name": "",
|
|
||||||
"email": ""
|
|
||||||
},
|
|
||||||
"repository": "/anchor",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"deploy": "gh-pages -d src/.vuepress/dist",
|
"dev": "next dev",
|
||||||
"dev": "vuepress dev src",
|
"build": "next build",
|
||||||
"build": "vuepress build src"
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"browserslist": "defaults, not ie <= 11",
|
||||||
|
"dependencies": {
|
||||||
|
"@docsearch/react": "^3.1.0",
|
||||||
|
"@headlessui/react": "^1.6.5",
|
||||||
|
"@markdoc/markdoc": "^0.1.3",
|
||||||
|
"@markdoc/next.js": "^0.1.4",
|
||||||
|
"@sindresorhus/slugify": "^2.1.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.2",
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"clsx": "^1.1.1",
|
||||||
|
"focus-visible": "^5.2.0",
|
||||||
|
"next": "12.1.6",
|
||||||
|
"next-plausible": "^3.2.0",
|
||||||
|
"postcss-focus-visible": "^6.0.4",
|
||||||
|
"postcss-import": "^14.1.0",
|
||||||
|
"prism-react-renderer": "^1.3.3",
|
||||||
|
"prismjs": "^1.28.0",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"tailwindcss": "^3.1.4"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@xiaopanda/vuepress-plugin-code-copy": "^1.0.3",
|
"eslint": "8.18.0",
|
||||||
"gh-pages": "^3.1.0",
|
"eslint-config-next": "12.1.6",
|
||||||
"vuepress": "^1.5.3",
|
"prettier": "^2.7.1",
|
||||||
"vuepress-plugin-dehydrate": "^1.1.5",
|
"prettier-plugin-tailwindcss": "^0.1.11"
|
||||||
"vuepress-theme-default-prefers-color-scheme": "^2.0.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
'postcss-import': {},
|
||||||
|
tailwindcss: {},
|
||||||
|
'postcss-focus-visible': {
|
||||||
|
replaceWith: '[data-focus-visible-added]',
|
||||||
|
},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
singleQuote: true,
|
||||||
|
semi: false,
|
||||||
|
plugins: [require('prettier-plugin-tailwindcss')],
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
|
||||||
|
.anchor
|
||||||
|
.DS_Store
|
||||||
|
target
|
||||||
|
**/*.rs.bk
|
||||||
|
node_modules
|
|
@ -0,0 +1,12 @@
|
||||||
|
[programs.localnet]
|
||||||
|
tic_tac_toe = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
|
||||||
|
|
||||||
|
[registry]
|
||||||
|
url = "https://anchor.projectserum.com"
|
||||||
|
|
||||||
|
[provider]
|
||||||
|
cluster = "localnet"
|
||||||
|
wallet = "~/.config/solana/id.json"
|
||||||
|
|
||||||
|
[scripts]
|
||||||
|
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
|
@ -0,0 +1,4 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"programs/*"
|
||||||
|
]
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Migrations are an early feature. Currently, they're nothing more than this
|
||||||
|
// single deploy script that's invoked from the CLI, injecting a provider
|
||||||
|
// configured from the workspace's Anchor.toml.
|
||||||
|
|
||||||
|
const anchor = require("@project-serum/anchor");
|
||||||
|
|
||||||
|
module.exports = async function (provider) {
|
||||||
|
// Configure client to use the provider.
|
||||||
|
anchor.setProvider(provider);
|
||||||
|
|
||||||
|
// Add your deploy script here.
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@project-serum/anchor": "0.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/mocha": "^9.0.0",
|
||||||
|
"@types/chai": "^4.3.0",
|
||||||
|
"chai": "^4.3.4",
|
||||||
|
"chai-as-promised": "^7.1.1",
|
||||||
|
"mocha": "^9.0.3",
|
||||||
|
"ts-mocha": "^8.0.0",
|
||||||
|
"typescript": "^4.3.5"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "tic-tac-toe"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Created with Anchor"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
||||||
|
name = "tic_tac_toe"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
no-entrypoint = []
|
||||||
|
no-idl = []
|
||||||
|
no-log-ix-name = []
|
||||||
|
cpi = ["no-entrypoint"]
|
||||||
|
default = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anchor-lang = "=0.24.1"
|
||||||
|
num-traits = "0.2"
|
||||||
|
num-derive = "0.3"
|
|
@ -0,0 +1,2 @@
|
||||||
|
[target.bpfel-unknown-unknown.dependencies.std]
|
||||||
|
features = []
|
|
@ -0,0 +1,10 @@
|
||||||
|
use anchor_lang::error_code;
|
||||||
|
|
||||||
|
#[error_code]
|
||||||
|
pub enum TicTacToeError {
|
||||||
|
TileOutOfBounds,
|
||||||
|
TileAlreadySet,
|
||||||
|
GameAlreadyOver,
|
||||||
|
NotPlayersTurn,
|
||||||
|
GameAlreadyStarted,
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub use play::*;
|
||||||
|
pub use setup_game::*;
|
||||||
|
|
||||||
|
pub mod play;
|
||||||
|
pub mod setup_game;
|
|
@ -0,0 +1,22 @@
|
||||||
|
use crate::errors::TicTacToeError;
|
||||||
|
use crate::state::game::*;
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
pub fn play(ctx: Context<Play>, tile: Tile) -> Result<()> {
|
||||||
|
let game = &mut ctx.accounts.game;
|
||||||
|
|
||||||
|
require_keys_eq!(
|
||||||
|
game.current_player(),
|
||||||
|
ctx.accounts.player.key(),
|
||||||
|
TicTacToeError::NotPlayersTurn
|
||||||
|
);
|
||||||
|
|
||||||
|
game.play(&tile)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Play<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub game: Account<'info, Game>,
|
||||||
|
pub player: Signer<'info>,
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
use crate::state::game::*;
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
pub fn setup_game(ctx: Context<SetupGame>, player_two: Pubkey) -> Result<()> {
|
||||||
|
ctx.accounts
|
||||||
|
.game
|
||||||
|
.start([ctx.accounts.player_one.key(), player_two])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetupGame<'info> {
|
||||||
|
#[account(init, payer = player_one, space = Game::MAXIMUM_SIZE + 8)]
|
||||||
|
pub game: Account<'info, Game>,
|
||||||
|
#[account(mut)]
|
||||||
|
pub player_one: Signer<'info>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use instructions::*;
|
||||||
|
use state::game::Tile;
|
||||||
|
|
||||||
|
pub mod errors;
|
||||||
|
pub mod instructions;
|
||||||
|
pub mod state;
|
||||||
|
|
||||||
|
// this key needs to be changed to whatever public key is returned by "anchor keys list"
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod tic_tac_toe {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub fn setup_game(ctx: Context<SetupGame>, player_two: Pubkey) -> Result<()> {
|
||||||
|
instructions::setup_game::setup_game(ctx, player_two)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(ctx: Context<Play>, tile: Tile) -> Result<()> {
|
||||||
|
instructions::play::play(ctx, tile)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
use crate::errors::TicTacToeError;
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use num_derive::*;
|
||||||
|
use num_traits::*;
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
pub struct Game {
|
||||||
|
players: [Pubkey; 2], // (32 * 2)
|
||||||
|
turn: u8, // 1
|
||||||
|
board: [[Option<Sign>; 3]; 3], // 9 * (1 + 1) = 18
|
||||||
|
state: GameState, // 32 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub const MAXIMUM_SIZE: usize = (32 * 2) + 1 + (9 * (1 + 1)) + (32 + 1);
|
||||||
|
|
||||||
|
pub fn start(&mut self, players: [Pubkey; 2]) -> Result<()> {
|
||||||
|
require_eq!(self.turn, 0, TicTacToeError::GameAlreadyStarted);
|
||||||
|
self.players = players;
|
||||||
|
self.turn = 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.state == GameState::Active
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_player_index(&self) -> usize {
|
||||||
|
((self.turn - 1) % 2) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_player(&self) -> Pubkey {
|
||||||
|
self.players[self.current_player_index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&mut self, tile: &Tile) -> Result<()> {
|
||||||
|
require!(self.is_active(), TicTacToeError::GameAlreadyOver);
|
||||||
|
|
||||||
|
match tile {
|
||||||
|
tile @ Tile {
|
||||||
|
row: 0..=2,
|
||||||
|
column: 0..=2,
|
||||||
|
} => match self.board[tile.row as usize][tile.column as usize] {
|
||||||
|
Some(_) => return Err(TicTacToeError::TileAlreadySet.into()),
|
||||||
|
None => {
|
||||||
|
self.board[tile.row as usize][tile.column as usize] =
|
||||||
|
Some(Sign::from_usize(self.current_player_index()).unwrap());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => return Err(TicTacToeError::TileOutOfBounds.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_state();
|
||||||
|
|
||||||
|
if GameState::Active == self.state {
|
||||||
|
self.turn += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool {
|
||||||
|
let [first, second, third] = trio;
|
||||||
|
self.board[first.0][first.1].is_some()
|
||||||
|
&& self.board[first.0][first.1] == self.board[second.0][second.1]
|
||||||
|
&& self.board[first.0][first.1] == self.board[third.0][third.1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(&mut self) {
|
||||||
|
for i in 0..=2 {
|
||||||
|
// three of the same in one row
|
||||||
|
if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) {
|
||||||
|
self.state = GameState::Won {
|
||||||
|
winner: self.current_player(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// three of the same in one column
|
||||||
|
if self.is_winning_trio([(0, i), (1, i), (2, i)]) {
|
||||||
|
self.state = GameState::Won {
|
||||||
|
winner: self.current_player(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// three of the same in one diagonal
|
||||||
|
if self.is_winning_trio([(0, 0), (1, 1), (2, 2)])
|
||||||
|
|| self.is_winning_trio([(0, 2), (1, 1), (2, 0)])
|
||||||
|
{
|
||||||
|
self.state = GameState::Won {
|
||||||
|
winner: self.current_player(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reaching this code means the game has not been won,
|
||||||
|
// so if there are unfilled tiles left, it's still active
|
||||||
|
for row in 0..=2 {
|
||||||
|
for column in 0..=2 {
|
||||||
|
if self.board[row][column].is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// game has not been won
|
||||||
|
// game has no more free tiles
|
||||||
|
// -> game ends in a tie
|
||||||
|
self.state = GameState::Tie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub enum GameState {
|
||||||
|
Active,
|
||||||
|
Tie,
|
||||||
|
Won { winner: Pubkey },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
AnchorSerialize, AnchorDeserialize, FromPrimitive, ToPrimitive, Copy, Clone, PartialEq, Eq,
|
||||||
|
)]
|
||||||
|
pub enum Sign {
|
||||||
|
X,
|
||||||
|
O,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||||
|
pub struct Tile {
|
||||||
|
row: u8,
|
||||||
|
column: u8,
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub use game::*;
|
||||||
|
|
||||||
|
pub mod game;
|
|
@ -0,0 +1,394 @@
|
||||||
|
import * as anchor from '@project-serum/anchor';
|
||||||
|
import { AnchorError, Program } from '@project-serum/anchor';
|
||||||
|
import { TicTacToe } from '../target/types/tic_tac_toe';
|
||||||
|
import chai from 'chai';
|
||||||
|
import chaiAsPromised from 'chai-as-promised';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
chai.use(chaiAsPromised);
|
||||||
|
|
||||||
|
async function play(program: Program<TicTacToe>, game, player, tile, expectedTurn, expectedGameState, expectedBoard) {
|
||||||
|
await program.methods
|
||||||
|
.play(tile)
|
||||||
|
.accounts({
|
||||||
|
player: player.publicKey,
|
||||||
|
game
|
||||||
|
})
|
||||||
|
.signers(player instanceof (anchor.Wallet as any) ? [] : [player])
|
||||||
|
.rpc();
|
||||||
|
|
||||||
|
const gameState = await program.account.game.fetch(game);
|
||||||
|
expect(gameState.turn).to.equal(expectedTurn);
|
||||||
|
expect(gameState.state).to.eql(expectedGameState);
|
||||||
|
expect(gameState.board)
|
||||||
|
.to
|
||||||
|
.eql(expectedBoard);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('tic-tac-toe', () => {
|
||||||
|
// Configure the client to use the local cluster.
|
||||||
|
anchor.setProvider(anchor.AnchorProvider.env());
|
||||||
|
|
||||||
|
const program = anchor.workspace.TicTacToe as Program<TicTacToe>;
|
||||||
|
const programProvider = program.provider as anchor.AnchorProvider;
|
||||||
|
|
||||||
|
it('setup game!', async() => {
|
||||||
|
const gameKeypair = anchor.web3.Keypair.generate();
|
||||||
|
const playerOne = programProvider.wallet;
|
||||||
|
const playerTwo = anchor.web3.Keypair.generate();
|
||||||
|
await program.methods
|
||||||
|
.setupGame(playerTwo.publicKey)
|
||||||
|
.accounts({
|
||||||
|
game: gameKeypair.publicKey,
|
||||||
|
playerOne: playerOne.publicKey,
|
||||||
|
})
|
||||||
|
.signers([gameKeypair])
|
||||||
|
.rpc();
|
||||||
|
|
||||||
|
let gameState = await program.account.game.fetch(gameKeypair.publicKey);
|
||||||
|
expect(gameState.turn).to.equal(1);
|
||||||
|
expect(gameState.players)
|
||||||
|
.to
|
||||||
|
.eql([playerOne.publicKey, playerTwo.publicKey]);
|
||||||
|
expect(gameState.state).to.eql({ active: {} });
|
||||||
|
expect(gameState.board)
|
||||||
|
.to
|
||||||
|
.eql([[null,null,null],[null,null,null],[null,null,null]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('player one wins!', async () => {
|
||||||
|
const gameKeypair = anchor.web3.Keypair.generate();
|
||||||
|
const playerOne = programProvider.wallet;
|
||||||
|
const playerTwo = anchor.web3.Keypair.generate();
|
||||||
|
await program.methods
|
||||||
|
.setupGame(playerTwo.publicKey)
|
||||||
|
.accounts({
|
||||||
|
game: gameKeypair.publicKey,
|
||||||
|
playerOne: playerOne.publicKey,
|
||||||
|
})
|
||||||
|
.signers([gameKeypair])
|
||||||
|
.rpc();
|
||||||
|
|
||||||
|
let gameState = await program.account.game.fetch(gameKeypair.publicKey);
|
||||||
|
expect(gameState.turn).to.equal(1);
|
||||||
|
expect(gameState.players)
|
||||||
|
.to
|
||||||
|
.eql([playerOne.publicKey, playerTwo.publicKey]);
|
||||||
|
expect(gameState.state).to.eql({ active: {} });
|
||||||
|
expect(gameState.board)
|
||||||
|
.to
|
||||||
|
.eql([[null,null,null],[null,null,null],[null,null,null]]);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 0},
|
||||||
|
2,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[null,null,null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne, // same player in subsequent turns
|
||||||
|
// change sth about the tx because
|
||||||
|
// duplicate tx that come in too fast
|
||||||
|
// after each other may get dropped
|
||||||
|
{row: 1, column: 0},
|
||||||
|
2,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[null,null,null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
chai.assert(false, "should've failed but didn't ");
|
||||||
|
} catch (_err) {
|
||||||
|
expect(_err).to.be.instanceOf(AnchorError);
|
||||||
|
const err: AnchorError = _err;
|
||||||
|
expect(err.error.errorCode.code).to.equal("NotPlayersTurn");
|
||||||
|
expect(err.error.errorCode.number).to.equal(6003);
|
||||||
|
expect(err.program.equals(program.programId)).is.true;
|
||||||
|
expect(err.error.comparedValues).to.deep.equal([playerTwo.publicKey, playerOne.publicKey]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 1, column: 0},
|
||||||
|
3,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[{o:{}},null,null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 1},
|
||||||
|
4,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{x: {}},null],
|
||||||
|
[{o:{}},null,null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 5, column: 1}, // out of bounds row
|
||||||
|
4,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{x: {}},null],
|
||||||
|
[{o:{}},null,null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
chai.assert(false, "should've failed but didn't ");
|
||||||
|
} catch (_err) {
|
||||||
|
expect(_err).to.be.instanceOf(AnchorError);
|
||||||
|
const err: AnchorError = _err;
|
||||||
|
expect(err.error.errorCode.number).to.equal(6000);
|
||||||
|
expect(err.error.errorCode.code).to.equal("TileOutOfBounds");
|
||||||
|
}
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 1, column: 1},
|
||||||
|
5,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{x: {}},null],
|
||||||
|
[{o:{}},{o:{}},null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 0},
|
||||||
|
5,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{x: {}},null],
|
||||||
|
[{o:{}},{o:{}},null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
chai.assert(false, "should've failed but didn't ");
|
||||||
|
} catch (_err) {
|
||||||
|
expect(_err).to.be.instanceOf(AnchorError);
|
||||||
|
const err: AnchorError = _err;
|
||||||
|
expect(err.error.errorCode.number).to.equal(6001);
|
||||||
|
}
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 2},
|
||||||
|
5,
|
||||||
|
{ won: { winner: playerOne.publicKey }, },
|
||||||
|
[
|
||||||
|
[{x:{}},{x: {}},{x: {}}],
|
||||||
|
[{o:{}},{o:{}},null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 2},
|
||||||
|
5,
|
||||||
|
{ won: { winner: playerOne.publicKey }, },
|
||||||
|
[
|
||||||
|
[{x:{}},{x: {}},{x: {}}],
|
||||||
|
[{o:{}},{o:{}},null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
chai.assert(false, "should've failed but didn't ");
|
||||||
|
} catch (_err) {
|
||||||
|
expect(_err).to.be.instanceOf(AnchorError);
|
||||||
|
const err: AnchorError = _err;
|
||||||
|
expect(err.error.errorCode.number).to.equal(6002);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('tie', async () => {
|
||||||
|
const gameKeypair = anchor.web3.Keypair.generate();
|
||||||
|
const playerOne = programProvider.wallet;
|
||||||
|
const playerTwo = anchor.web3.Keypair.generate();
|
||||||
|
await program.methods
|
||||||
|
.setupGame(playerTwo.publicKey)
|
||||||
|
.accounts({
|
||||||
|
game: gameKeypair.publicKey,
|
||||||
|
playerOne: playerOne.publicKey,
|
||||||
|
})
|
||||||
|
.signers([gameKeypair])
|
||||||
|
.rpc();
|
||||||
|
|
||||||
|
let gameState = await program.account.game.fetch(gameKeypair.publicKey);
|
||||||
|
expect(gameState.turn).to.equal(1);
|
||||||
|
expect(gameState.players)
|
||||||
|
.to
|
||||||
|
.eql([playerOne.publicKey, playerTwo.publicKey]);
|
||||||
|
expect(gameState.state).to.eql({ active: {} });
|
||||||
|
expect(gameState.board)
|
||||||
|
.to
|
||||||
|
.eql([[null,null,null],[null,null,null],[null,null,null]]);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 0},
|
||||||
|
2,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[null,null,null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 1, column: 1},
|
||||||
|
3,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[null,{o:{}},null],
|
||||||
|
[null,null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 2, column: 0},
|
||||||
|
4,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[null,{o:{}},null],
|
||||||
|
[{x:{}},null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 1, column: 0},
|
||||||
|
5,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[{o:{}},{o:{}},null],
|
||||||
|
[{x:{}},null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 1, column: 2},
|
||||||
|
6,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},null,null],
|
||||||
|
[{o:{}},{o:{}},{x:{}}],
|
||||||
|
[{x:{}},null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 0, column: 1},
|
||||||
|
7,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{o:{}},null],
|
||||||
|
[{o:{}},{o:{}},{x:{}}],
|
||||||
|
[{x:{}},null,null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 2, column: 1},
|
||||||
|
8,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{o:{}},null],
|
||||||
|
[{o:{}},{o:{}},{x:{}}],
|
||||||
|
[{x:{}},{x:{}},null]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{row: 2, column: 2},
|
||||||
|
9,
|
||||||
|
{ active: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{o:{}},null],
|
||||||
|
[{o:{}},{o:{}},{x:{}}],
|
||||||
|
[{x:{}},{x:{}},{o:{}}]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{row: 0, column: 2},
|
||||||
|
9,
|
||||||
|
{ tie: {}, },
|
||||||
|
[
|
||||||
|
[{x:{}},{o:{}},{x:{}}],
|
||||||
|
[{o:{}},{o:{}},{x:{}}],
|
||||||
|
[{x:{}},{x:{}},{o:{}}]
|
||||||
|
]
|
||||||
|
);
|
||||||
|
})
|
||||||
|
});
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"types": ["mocha", "chai"],
|
||||||
|
"typeRoots": ["./node_modules/@types"],
|
||||||
|
"lib": ["es2015"],
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6",
|
||||||
|
"esModuleInterop": true
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 266 KiB |
After Width: | Height: | Size: 108 KiB |
|
@ -0,0 +1,93 @@
|
||||||
|
Copyright 2018 The Lexend Project Authors (https://github.com/googlefonts/lexend), with Reserved Font Name “RevReading Lexend”.
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
https://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
After Width: | Height: | Size: 125 KiB |
After Width: | Height: | Size: 108 KiB |
After Width: | Height: | Size: 176 KiB |
|
@ -1,15 +0,0 @@
|
||||||
<template>
|
|
||||||
<p class="demo">
|
|
||||||
{{ msg }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
msg: 'Hello this is <Foo-Bar>'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<p class="demo">This is another component</p>
|
|
||||||
</template>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<template>
|
|
||||||
<p class="demo">
|
|
||||||
{{ msg }}
|
|
||||||
</p>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
msg: 'Hello this is <demo-component>'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,109 +0,0 @@
|
||||||
const { description } = require("../../package");
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
base: "/anchor/",
|
|
||||||
/**
|
|
||||||
* Ref:https://v1.vuepress.vuejs.org/config/#title
|
|
||||||
*/
|
|
||||||
title: "⚓ Anchor",
|
|
||||||
/**
|
|
||||||
* Ref:https://v1.vuepress.vuejs.org/config/#description
|
|
||||||
*/
|
|
||||||
description: description,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra tags to be injected to the page HTML `<head>`
|
|
||||||
*
|
|
||||||
* ref:https://v1.vuepress.vuejs.org/config/#head
|
|
||||||
*/
|
|
||||||
head: [
|
|
||||||
[
|
|
||||||
"link",
|
|
||||||
{
|
|
||||||
rel: "icon",
|
|
||||||
href: "data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>⚓ </text></svg>",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
["meta", { name: "theme-color", content: "#3eaf7c" }],
|
|
||||||
["meta", { name: "apple-mobile-web-app-capable", content: "yes" }],
|
|
||||||
[
|
|
||||||
"meta",
|
|
||||||
{ name: "apple-mobile-web-app-status-bar-style", content: "black" },
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
theme: "default-prefers-color-scheme",
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Theme configuration, here is the default theme configuration for VuePress.
|
|
||||||
*
|
|
||||||
* ref:https://v1.vuepress.vuejs.org/theme/default-theme-config.html
|
|
||||||
*/
|
|
||||||
themeConfig: {
|
|
||||||
repo: "",
|
|
||||||
editLinks: false,
|
|
||||||
docsDir: "",
|
|
||||||
editLinkText: "",
|
|
||||||
lastUpdated: false,
|
|
||||||
sidebarDepth: 2,
|
|
||||||
sidebar: [
|
|
||||||
{
|
|
||||||
collapsable: false,
|
|
||||||
title: "Getting Started",
|
|
||||||
children: [
|
|
||||||
"/getting-started/introduction",
|
|
||||||
"/getting-started/installation",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
collapsable: false,
|
|
||||||
title: "Teams",
|
|
||||||
children: ["/getting-started/projects"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
collapsable: false,
|
|
||||||
title: "Tutorials",
|
|
||||||
children: [
|
|
||||||
"/tutorials/tutorial-0",
|
|
||||||
"/tutorials/tutorial-1",
|
|
||||||
"/tutorials/tutorial-2",
|
|
||||||
"/tutorials/tutorial-3",
|
|
||||||
"/tutorials/tutorial-4",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
collapsable: false,
|
|
||||||
title: "CLI",
|
|
||||||
children: ["/cli/commands"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
collapsable: false,
|
|
||||||
title: "Source Verification",
|
|
||||||
children: [
|
|
||||||
"/getting-started/verification",
|
|
||||||
"/getting-started/publishing",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
nav: [
|
|
||||||
{ text: "The Anchor Book", link: "https://book.anchor-lang.com" },
|
|
||||||
{ text: "Rust", link: "https://docs.rs/anchor-lang/latest/anchor_lang/" },
|
|
||||||
{
|
|
||||||
text: "TypeScript",
|
|
||||||
link: "https://coral-xyz.github.io/anchor/ts/index.html",
|
|
||||||
},
|
|
||||||
{ text: "GitHub", link: "https://github.com/coral-xyz/anchor" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply plugins,ref:https://v1.vuepress.vuejs.org/zh/plugin/
|
|
||||||
*/
|
|
||||||
plugins: [
|
|
||||||
"dehydrate",
|
|
||||||
"@vuepress/plugin-back-to-top",
|
|
||||||
"@vuepress/plugin-medium-zoom",
|
|
||||||
"@xiaopanda/vuepress-plugin-code-copy",
|
|
||||||
],
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
/**
|
|
||||||
* Client app enhancement file.
|
|
||||||
*
|
|
||||||
* https://v1.vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements
|
|
||||||
*/
|
|
||||||
|
|
||||||
export default ({
|
|
||||||
Vue, // the version of Vue being used in the VuePress app
|
|
||||||
options, // the options for the root Vue instance
|
|
||||||
router, // the router instance for the app
|
|
||||||
siteData, // site metadata
|
|
||||||
}) => {
|
|
||||||
// ...apply enhancements for the site.
|
|
||||||
router.addRoutes([{ path: "/", redirect: "/getting-started/introduction" }]);
|
|
||||||
};
|
|
|
@ -1,8 +0,0 @@
|
||||||
/**
|
|
||||||
* Custom Styles here.
|
|
||||||
*
|
|
||||||
* ref:https://v1.vuepress.vuejs.org/config/#index-styl
|
|
||||||
*/
|
|
||||||
|
|
||||||
.home .hero img
|
|
||||||
max-width 450px!important
|
|
|
@ -1,10 +0,0 @@
|
||||||
/**
|
|
||||||
* Custom palette here.
|
|
||||||
*
|
|
||||||
* ref:https://v1.vuepress.vuejs.org/zh/config/#palette-styl
|
|
||||||
*/
|
|
||||||
|
|
||||||
$accentColor = #3eaf7c
|
|
||||||
$textColor = #2c3e50
|
|
||||||
$borderColor = #eaecef
|
|
||||||
$codeBgColor = #282c34
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
primary:
|
||||||
|
'rounded-full bg-sky-300 py-2 px-4 text-sm font-semibold text-slate-900 hover:bg-sky-200 active:bg-sky-500 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50',
|
||||||
|
secondary:
|
||||||
|
'rounded-full bg-slate-800 py-2 px-4 text-sm font-medium text-white hover:bg-slate-700 active:text-slate-400 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white/50',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({ variant = 'primary', className, ...props }) {
|
||||||
|
return <button className={clsx(styles[variant], className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ButtonLink({ variant = 'primary', className, href, ...props }) {
|
||||||
|
return (
|
||||||
|
<Link href={href}>
|
||||||
|
<a className={clsx(styles[variant], className)} {...props} />
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { Icon } from '@/components/Icon'
|
||||||
|
|
||||||
|
const styles = {
|
||||||
|
note: {
|
||||||
|
container:
|
||||||
|
'bg-sky-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10',
|
||||||
|
title: 'text-sky-900 dark:text-sky-400',
|
||||||
|
body: 'text-sky-800 prose-code:text-sky-900 dark:text-slate-300 dark:prose-code:text-slate-300 prose-a:text-sky-900 [--tw-prose-background:theme(colors.sky.50)]',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
container:
|
||||||
|
'bg-amber-50 dark:bg-slate-800/60 dark:ring-1 dark:ring-slate-300/10',
|
||||||
|
title: 'text-amber-900 dark:text-amber-500',
|
||||||
|
body: 'text-amber-800 prose-code:text-amber-900 prose-a:text-amber-900 [--tw-prose-underline:theme(colors.amber.400)] dark:[--tw-prose-underline:theme(colors.sky.700)] [--tw-prose-background:theme(colors.amber.50)] dark:text-slate-300 dark:prose-code:text-slate-300',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
note: (props) => <Icon icon="lightbulb" {...props} />,
|
||||||
|
warning: (props) => <Icon icon="warning" color="amber" {...props} />,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Callout({ type = 'note', title, children }) {
|
||||||
|
let IconComponent = icons[type]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={clsx('my-8 flex rounded-3xl p-6', styles[type].container)}>
|
||||||
|
<IconComponent className="h-8 w-8 flex-none" />
|
||||||
|
<div className="ml-4 flex-auto">
|
||||||
|
<p className={clsx('m-0 font-display text-xl', styles[type].title)}>
|
||||||
|
{title}
|
||||||
|
</p>
|
||||||
|
<div className={clsx('prose mt-2.5', styles[type].body)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import Highlight, { defaultProps } from 'prism-react-renderer'
|
||||||
|
|
||||||
|
export function Fence({ children, language }) {
|
||||||
|
return (
|
||||||
|
<Highlight
|
||||||
|
{...defaultProps}
|
||||||
|
code={children.trimEnd()}
|
||||||
|
language={language}
|
||||||
|
theme={undefined}
|
||||||
|
>
|
||||||
|
{({ className, style, tokens, getTokenProps }) => (
|
||||||
|
<pre className={className} style={style}>
|
||||||
|
<code>
|
||||||
|
{tokens.map((line, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{line.map((token, index) => (
|
||||||
|
<span key={index} {...getTokenProps({ token })} />
|
||||||
|
))}
|
||||||
|
{'\n'}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import Image from 'next/image'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import Highlight, { defaultProps } from 'prism-react-renderer'
|
||||||
|
|
||||||
|
import { ButtonLink } from '@/components/Button'
|
||||||
|
import { HeroBackground } from '@/components/HeroBackground'
|
||||||
|
import blurCyanImage from '@/images/blur-cyan.png'
|
||||||
|
import blurIndigoImage from '@/images/blur-indigo.png'
|
||||||
|
|
||||||
|
const codeLanguage = 'rust'
|
||||||
|
const code = `#[account(mut)]
|
||||||
|
pub payer: Signer<'info>,
|
||||||
|
pub publisher: Signer<'info>,
|
||||||
|
pub rent: Sysvar<'info, Rent>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
pub token_program: Program<'info, Token>,`
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ name: 'lib.rs', isActive: true },
|
||||||
|
{ name: 'Anchor.toml', isActive: false },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden bg-slate-900 dark:-mb-32 dark:-mt-[4.5rem] dark:pb-32 dark:pt-[4.5rem] dark:lg:-mt-[4.75rem] dark:lg:pt-[4.75rem]">
|
||||||
|
<div className="py-16 sm:px-2 lg:relative lg:py-20 lg:px-0">
|
||||||
|
<div className="mx-auto grid max-w-2xl grid-cols-1 items-center gap-y-16 gap-x-8 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12">
|
||||||
|
<div className="relative z-10 md:text-center lg:text-left">
|
||||||
|
<div className="absolute bottom-full right-full -mr-72 -mb-56 opacity-50">
|
||||||
|
<Image
|
||||||
|
src={blurCyanImage}
|
||||||
|
alt=""
|
||||||
|
layout="fixed"
|
||||||
|
width={530}
|
||||||
|
height={530}
|
||||||
|
unoptimized
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<p className="inline bg-gradient-to-r from-indigo-200 via-sky-400 to-indigo-200 bg-clip-text font-display text-5xl tracking-tight text-transparent">
|
||||||
|
Anchor
|
||||||
|
</p>
|
||||||
|
<p className="mt-3 text-2xl tracking-tight text-slate-400">
|
||||||
|
Solana's Sealevel runtime framework
|
||||||
|
</p>
|
||||||
|
<div className="mt-8 flex space-x-4 md:justify-center lg:justify-start">
|
||||||
|
<ButtonLink href="/">Get started</ButtonLink>
|
||||||
|
<ButtonLink
|
||||||
|
href="https://github.com/coral-xyz/anchor"
|
||||||
|
variant="secondary"
|
||||||
|
>
|
||||||
|
View on GitHub
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative lg:static xl:pl-10">
|
||||||
|
<div className="absolute inset-x-[-50vw] -top-32 -bottom-48 [mask-image:linear-gradient(transparent,white,white)] dark:[mask-image:linear-gradient(transparent,white,transparent)] lg:left-[calc(50%+14rem)] lg:right-0 lg:-top-32 lg:-bottom-32 lg:[mask-image:none] lg:dark:[mask-image:linear-gradient(white,white,transparent)]">
|
||||||
|
<HeroBackground className="absolute top-1/2 left-1/2 -translate-y-1/2 -translate-x-1/2 lg:left-0 lg:translate-x-0 lg:-translate-y-[60%]" />
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute -top-64 -right-64">
|
||||||
|
<Image
|
||||||
|
src={blurCyanImage}
|
||||||
|
alt=""
|
||||||
|
layout="fixed"
|
||||||
|
width={530}
|
||||||
|
height={530}
|
||||||
|
unoptimized
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute -bottom-40 -right-44">
|
||||||
|
<Image
|
||||||
|
src={blurIndigoImage}
|
||||||
|
alt=""
|
||||||
|
layout="fixed"
|
||||||
|
width={567}
|
||||||
|
height={567}
|
||||||
|
unoptimized
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-10 blur-lg" />
|
||||||
|
<div className="absolute inset-0 rounded-2xl bg-gradient-to-tr from-sky-300 via-sky-300/70 to-blue-300 opacity-10" />
|
||||||
|
<div className="relative rounded-2xl bg-[#0A101F]/80 ring-1 ring-white/10 backdrop-blur">
|
||||||
|
<div className="absolute -top-px left-20 right-11 h-px bg-gradient-to-r from-sky-300/0 via-sky-300/70 to-sky-300/0" />
|
||||||
|
<div className="absolute -bottom-px left-11 right-20 h-px bg-gradient-to-r from-blue-400/0 via-blue-400 to-blue-400/0" />
|
||||||
|
<div className="pl-4 pt-4">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-2.5 w-auto stroke-slate-500/30"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<circle cx="5" cy="5" r="4.5" />
|
||||||
|
<circle cx="21" cy="5" r="4.5" />
|
||||||
|
<circle cx="37" cy="5" r="4.5" />
|
||||||
|
</svg>
|
||||||
|
<div className="mt-4 flex space-x-2 text-xs">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab.name}
|
||||||
|
className={clsx('flex h-6 rounded-full', {
|
||||||
|
'bg-gradient-to-r from-sky-400/30 via-sky-400 to-sky-400/30 p-px font-medium text-sky-300':
|
||||||
|
tab.isActive,
|
||||||
|
'text-slate-500': !tab.isActive,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
'flex items-center rounded-full px-2.5',
|
||||||
|
{ 'bg-slate-800': tab.isActive }
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 flex items-start px-1 text-sm">
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="select-none border-r border-slate-300/5 pr-4 font-mono text-slate-600"
|
||||||
|
>
|
||||||
|
{Array.from({
|
||||||
|
length: code.split('\n').length,
|
||||||
|
}).map((_, index) => (
|
||||||
|
<Fragment key={index}>
|
||||||
|
{(index + 1).toString().padStart(2, '0')}
|
||||||
|
<br />
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<Highlight
|
||||||
|
{...defaultProps}
|
||||||
|
code={code}
|
||||||
|
language={codeLanguage}
|
||||||
|
theme={undefined}
|
||||||
|
>
|
||||||
|
{({
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
tokens,
|
||||||
|
getLineProps,
|
||||||
|
getTokenProps,
|
||||||
|
}) => (
|
||||||
|
<pre
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'flex overflow-x-auto pb-6'
|
||||||
|
)}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<code className="px-4">
|
||||||
|
{tokens.map((line, index) => (
|
||||||
|
<div key={index} {...getLineProps({ line })}>
|
||||||
|
{line.map((token, index) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
{...getTokenProps({ token })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</Highlight>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { useId } from 'react'
|
||||||
|
|
||||||
|
export function HeroBackground(props) {
|
||||||
|
let id = useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" width={668} height={1069} fill="none" {...props}>
|
||||||
|
<defs>
|
||||||
|
<clipPath id={`${id}-clip-path`}>
|
||||||
|
<path
|
||||||
|
fill="#fff"
|
||||||
|
transform="rotate(-180 334 534.4)"
|
||||||
|
d="M0 0h668v1068.8H0z"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
<g opacity=".4" clipPath={`url(#${id}-clip-path)`} strokeWidth={4}>
|
||||||
|
<path
|
||||||
|
opacity=".3"
|
||||||
|
d="M584.5 770.4v-474M484.5 770.4v-474M384.5 770.4v-474M283.5 769.4v-474M183.5 768.4v-474M83.5 767.4v-474"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M83.5 221.275v6.587a50.1 50.1 0 0 0 22.309 41.686l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M83.5 716.012v6.588a50.099 50.099 0 0 0 22.309 41.685l55.581 37.054a50.102 50.102 0 0 1 22.309 41.686v6.587M183.7 584.5v6.587a50.1 50.1 0 0 0 22.31 41.686l55.581 37.054a50.097 50.097 0 0 1 22.309 41.685v6.588M384.101 277.637v6.588a50.1 50.1 0 0 0 22.309 41.685l55.581 37.054a50.1 50.1 0 0 1 22.31 41.686v6.587M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M384.1 770.288v6.587a50.1 50.1 0 0 1-22.309 41.686l-55.581 37.054A50.099 50.099 0 0 0 283.9 897.3v6.588M484.3 594.937v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054A50.1 50.1 0 0 0 384.1 721.95v6.587M484.3 872.575v6.587a50.1 50.1 0 0 1-22.31 41.686l-55.581 37.054a50.098 50.098 0 0 0-22.309 41.686v6.582M584.501 663.824v39.988a50.099 50.099 0 0 1-22.31 41.685l-55.581 37.054a50.102 50.102 0 0 0-22.309 41.686v6.587M283.899 945.637v6.588a50.1 50.1 0 0 1-22.309 41.685l-55.581 37.05a50.12 50.12 0 0 0-22.31 41.69v6.59M384.1 277.637c0 19.946 12.763 37.655 31.686 43.962l137.028 45.676c18.923 6.308 31.686 24.016 31.686 43.962M183.7 463.425v30.69c0 21.564 13.799 40.709 34.257 47.529l134.457 44.819c18.922 6.307 31.686 24.016 31.686 43.962M83.5 102.288c0 19.515 13.554 36.412 32.604 40.645l235.391 52.309c19.05 4.234 32.605 21.13 32.605 40.646M83.5 463.425v-58.45M183.699 542.75V396.625M283.9 1068.8V945.637M83.5 363.225v-141.95M83.5 179.524v-77.237M83.5 60.537V0M384.1 630.425V277.637M484.301 830.824V594.937M584.5 1068.8V663.825M484.301 555.275V452.988M584.5 622.075V452.988M384.1 728.537v-56.362M384.1 1068.8v-20.88M384.1 1006.17V770.287M283.9 903.888V759.85M183.699 1066.71V891.362M83.5 1068.8V716.012M83.5 674.263V505.175"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="384.1"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 384.1)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="200.399"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 200.399)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="81.412"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 81.412)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="183.699"
|
||||||
|
cy="375.75"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 183.699 375.75)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="183.699"
|
||||||
|
cy="563.625"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 183.699 563.625)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="651.3"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 651.3)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="484.301"
|
||||||
|
cy="574.062"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 484.301 574.062)"
|
||||||
|
fill="#0EA5E9"
|
||||||
|
fillOpacity=".42"
|
||||||
|
stroke="#0EA5E9"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="749.412"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 749.412)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="1027.05"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 1027.05)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="283.9"
|
||||||
|
cy="924.763"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 283.9 924.763)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="183.699"
|
||||||
|
cy="870.487"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 183.699 870.487)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="283.9"
|
||||||
|
cy="738.975"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 283.9 738.975)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="695.138"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 695.138)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="83.5"
|
||||||
|
cy="484.3"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 83.5 484.3)"
|
||||||
|
fill="#0EA5E9"
|
||||||
|
fillOpacity=".42"
|
||||||
|
stroke="#0EA5E9"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="484.301"
|
||||||
|
cy="432.112"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 484.301 432.112)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="584.5"
|
||||||
|
cy="432.112"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 584.5 432.112)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="584.5"
|
||||||
|
cy="642.95"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 584.5 642.95)"
|
||||||
|
fill="#1E293B"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="484.301"
|
||||||
|
cy="851.699"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 484.301 851.699)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx="384.1"
|
||||||
|
cy="256.763"
|
||||||
|
r="10.438"
|
||||||
|
transform="rotate(-180 384.1 256.763)"
|
||||||
|
stroke="#334155"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { useId } from 'react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { InstallationIcon } from '@/components/icons/InstallationIcon'
|
||||||
|
import { LightbulbIcon } from '@/components/icons/LightbulbIcon'
|
||||||
|
import { PluginsIcon } from '@/components/icons/PluginsIcon'
|
||||||
|
import { PresetsIcon } from '@/components/icons/PresetsIcon'
|
||||||
|
import { ThemingIcon } from '@/components/icons/ThemingIcon'
|
||||||
|
import { WarningIcon } from '@/components/icons/WarningIcon'
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
installation: InstallationIcon,
|
||||||
|
presets: PresetsIcon,
|
||||||
|
plugins: PluginsIcon,
|
||||||
|
theming: ThemingIcon,
|
||||||
|
lightbulb: LightbulbIcon,
|
||||||
|
warning: WarningIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconStyles = {
|
||||||
|
blue: '[--icon-foreground:theme(colors.slate.900)] [--icon-background:theme(colors.white)]',
|
||||||
|
amber:
|
||||||
|
'[--icon-foreground:theme(colors.amber.900)] [--icon-background:theme(colors.amber.100)]',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Icon({ color = 'blue', icon, className, ...props }) {
|
||||||
|
let id = useId()
|
||||||
|
let IconComponent = icons[icon]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
fill="none"
|
||||||
|
className={clsx(className, iconStyles[color])}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<IconComponent id={id} color={color} />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const gradients = {
|
||||||
|
blue: [
|
||||||
|
{ stopColor: '#0EA5E9' },
|
||||||
|
{ stopColor: '#22D3EE', offset: '.527' },
|
||||||
|
{ stopColor: '#818CF8', offset: 1 },
|
||||||
|
],
|
||||||
|
amber: [
|
||||||
|
{ stopColor: '#FDE68A', offset: '.08' },
|
||||||
|
{ stopColor: '#F59E0B', offset: '.837' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Gradient({ color = 'blue', ...props }) {
|
||||||
|
return (
|
||||||
|
<radialGradient
|
||||||
|
cx={0}
|
||||||
|
cy={0}
|
||||||
|
r={1}
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{gradients[color].map((stop, index) => (
|
||||||
|
<stop key={index} {...stop} />
|
||||||
|
))}
|
||||||
|
</radialGradient>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LightMode({ className, ...props }) {
|
||||||
|
return <g className={clsx('dark:hidden', className)} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DarkMode({ className, ...props }) {
|
||||||
|
return <g className={clsx('hidden dark:inline', className)} {...props} />
|
||||||
|
}
|
|
@ -0,0 +1,275 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
import { Hero } from '@/components/Hero'
|
||||||
|
import { Logo } from '@/components/Logo'
|
||||||
|
import { MobileNavigation } from '@/components/MobileNavigation'
|
||||||
|
import { Navigation } from '@/components/Navigation'
|
||||||
|
import { Prose } from '@/components/Prose'
|
||||||
|
import { Search } from '@/components/Search'
|
||||||
|
import { ThemeSelector } from '@/components/ThemeSelector'
|
||||||
|
|
||||||
|
function Header({ navigation }) {
|
||||||
|
let [isScrolled, setIsScrolled] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onScroll() {
|
||||||
|
setIsScrolled(window.scrollY > 0)
|
||||||
|
}
|
||||||
|
onScroll()
|
||||||
|
window.addEventListener('scroll', onScroll)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', onScroll)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={clsx(
|
||||||
|
'sticky top-0 z-50 flex flex-wrap items-center justify-between bg-white px-4 py-5 shadow-md shadow-slate-900/5 transition duration-500 dark:shadow-none sm:px-6 lg:px-8',
|
||||||
|
{
|
||||||
|
'dark:bg-slate-900/95 dark:backdrop-blur dark:[@supports(backdrop-filter:blur(0))]:bg-slate-900/75':
|
||||||
|
isScrolled,
|
||||||
|
'dark:bg-transparent': !isScrolled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="mr-6 lg:hidden">
|
||||||
|
<MobileNavigation navigation={navigation} />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex flex-grow basis-0 items-center">
|
||||||
|
<Link href="/">
|
||||||
|
<a className="block w-10 overflow-hidden lg:w-auto">
|
||||||
|
<span className="sr-only">Home page</span>
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="-my-5 mr-6 sm:mr-8 md:mr-0">
|
||||||
|
<Search />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex basis-0 justify-end space-x-6 sm:space-x-8 md:flex-grow">
|
||||||
|
<ThemeSelector className="relative z-10" />
|
||||||
|
<Link href="https://github.com/coral-xyz/anchor">
|
||||||
|
<a className="group">
|
||||||
|
<span className="sr-only">GitHub</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
className="h-6 w-6 fill-slate-400 group-hover:fill-slate-500 dark:group-hover:fill-slate-300"
|
||||||
|
>
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Layout({ children, title, navigation, tableOfContents }) {
|
||||||
|
let router = useRouter()
|
||||||
|
let isHomePage = router.pathname === '/'
|
||||||
|
let allLinks = navigation.flatMap((section) => section.links)
|
||||||
|
let linkIndex = allLinks.findIndex((link) => link.href === router.pathname)
|
||||||
|
let previousPage = allLinks[linkIndex - 1]
|
||||||
|
let nextPage = allLinks[linkIndex + 1]
|
||||||
|
let section = navigation.find((section) =>
|
||||||
|
section.links.find((link) => link.href === router.pathname)
|
||||||
|
)
|
||||||
|
let currentSection = useTableOfContents(tableOfContents)
|
||||||
|
|
||||||
|
function isActive(section) {
|
||||||
|
if (section.id === currentSection) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!section.children) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return section.children.findIndex(isActive) > -1
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Header navigation={navigation} />
|
||||||
|
|
||||||
|
{isHomePage && <Hero />}
|
||||||
|
|
||||||
|
<div className="relative mx-auto flex max-w-8xl justify-center sm:px-2 lg:px-8 xl:px-12">
|
||||||
|
<div className="hidden lg:relative lg:block lg:flex-none">
|
||||||
|
<div className="absolute inset-y-0 right-0 w-[50vw] bg-slate-50 dark:hidden" />
|
||||||
|
<div className="sticky top-[4.5rem] -ml-0.5 h-[calc(100vh-4.5rem)] overflow-y-auto py-16 pl-0.5">
|
||||||
|
<div className="absolute top-16 bottom-0 right-0 hidden h-12 w-px bg-gradient-to-t from-slate-800 dark:block" />
|
||||||
|
<div className="absolute top-28 bottom-0 right-0 hidden w-px bg-slate-800 dark:block" />
|
||||||
|
<Navigation
|
||||||
|
navigation={navigation}
|
||||||
|
className="w-64 pr-8 xl:w-72 xl:pr-16"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 max-w-2xl flex-auto px-4 py-16 lg:max-w-none lg:pr-0 lg:pl-8 xl:px-16">
|
||||||
|
<article>
|
||||||
|
{(title || section) && (
|
||||||
|
<header className="mb-9 space-y-1">
|
||||||
|
{section && (
|
||||||
|
<p className="font-display text-sm font-medium text-sky-500">
|
||||||
|
{section.title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<h1 className="font-display text-3xl tracking-tight text-slate-900 dark:text-white">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
</header>
|
||||||
|
)}
|
||||||
|
<Prose>{children}</Prose>
|
||||||
|
</article>
|
||||||
|
<dl className="mt-12 flex border-t border-slate-200 pt-6 dark:border-slate-800">
|
||||||
|
{previousPage && (
|
||||||
|
<div>
|
||||||
|
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||||
|
Previous
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1">
|
||||||
|
<Link href={previousPage.href}>
|
||||||
|
<a className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
|
||||||
|
← {previousPage.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{nextPage && (
|
||||||
|
<div className="ml-auto text-right">
|
||||||
|
<dt className="font-display text-sm font-medium text-slate-900 dark:text-white">
|
||||||
|
Next
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1">
|
||||||
|
<Link href={nextPage.href}>
|
||||||
|
<a className="text-base font-semibold text-slate-500 hover:text-slate-600 dark:text-slate-400 dark:hover:text-slate-300">
|
||||||
|
{nextPage.title} →
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div className="hidden xl:sticky xl:top-[4.5rem] xl:-mr-6 xl:block xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6">
|
||||||
|
<nav aria-labelledby="on-this-page-title" className="w-56">
|
||||||
|
{tableOfContents.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2
|
||||||
|
id="on-this-page-title"
|
||||||
|
className="font-display text-sm font-medium text-slate-900 dark:text-white"
|
||||||
|
>
|
||||||
|
On this page
|
||||||
|
</h2>
|
||||||
|
<ul className="mt-4 space-y-3 text-sm">
|
||||||
|
{tableOfContents.map((section) => (
|
||||||
|
<li key={section.id}>
|
||||||
|
<h3>
|
||||||
|
<Link href={`#${section.id}`}>
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
isActive(section)
|
||||||
|
? 'text-sky-500'
|
||||||
|
: 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{section.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</h3>
|
||||||
|
{section.children.length > 0 && (
|
||||||
|
<ul className="mt-2 space-y-3 pl-5 text-slate-500 dark:text-slate-400">
|
||||||
|
{section.children.map((subSection) => (
|
||||||
|
<li key={subSection.id}>
|
||||||
|
<Link href={`#${subSection.id}`}>
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
isActive(subSection)
|
||||||
|
? 'text-sky-500'
|
||||||
|
: 'hover:text-slate-600 dark:hover:text-slate-300'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{subSection.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTableOfContents(tableOfContents) {
|
||||||
|
let [currentSection, setCurrentSection] = useState(tableOfContents[0]?.id)
|
||||||
|
|
||||||
|
let getHeadings = useCallback(() => {
|
||||||
|
function* traverse(node) {
|
||||||
|
if (Array.isArray(node)) {
|
||||||
|
for (let child of node) {
|
||||||
|
yield* traverse(child)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let el = document.getElementById(node.id)
|
||||||
|
if (!el) return
|
||||||
|
|
||||||
|
let style = window.getComputedStyle(el)
|
||||||
|
let scrollMt = parseFloat(style.scrollMarginTop)
|
||||||
|
|
||||||
|
let top = window.scrollY + el.getBoundingClientRect().top - scrollMt
|
||||||
|
yield { id: node.id, top }
|
||||||
|
|
||||||
|
for (let child of node.children ?? []) {
|
||||||
|
yield* traverse(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(traverse(tableOfContents))
|
||||||
|
}, [tableOfContents])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let headings = getHeadings()
|
||||||
|
if (tableOfContents.length === 0 || headings.length === 0) return
|
||||||
|
function onScroll() {
|
||||||
|
let sortedHeadings = headings.concat([]).sort((a, b) => a.top - b.top)
|
||||||
|
|
||||||
|
let top = window.pageYOffset
|
||||||
|
let current = sortedHeadings[0].id
|
||||||
|
for (let i = 0; i < sortedHeadings.length; i++) {
|
||||||
|
if (top >= sortedHeadings[i].top) {
|
||||||
|
current = sortedHeadings[i].id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setCurrentSection(current)
|
||||||
|
}
|
||||||
|
window.addEventListener('scroll', onScroll, {
|
||||||
|
capture: true,
|
||||||
|
passive: true,
|
||||||
|
})
|
||||||
|
onScroll()
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('scroll', onScroll, {
|
||||||
|
capture: true,
|
||||||
|
passive: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [getHeadings, tableOfContents])
|
||||||
|
|
||||||
|
return currentSection
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import NextLink from 'next/link'
|
||||||
|
|
||||||
|
import { Icon } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function LinkGrid({ children }) {
|
||||||
|
return (
|
||||||
|
<div className="not-prose my-12 grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LinkGrid.Link = function Link({ title, description, href, icon }) {
|
||||||
|
return (
|
||||||
|
<div className="group relative rounded-xl border border-slate-200 dark:border-slate-800">
|
||||||
|
<div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--link-grid-hover-bg,theme(colors.sky.50)),var(--link-grid-hover-bg,theme(colors.sky.50)))_padding-box,linear-gradient(to_top,theme(colors.indigo.400),theme(colors.cyan.400),theme(colors.sky.500))_border-box] group-hover:opacity-100 dark:[--link-grid-hover-bg:theme(colors.slate.800)]" />
|
||||||
|
<div className="relative overflow-hidden rounded-xl p-6">
|
||||||
|
<Icon icon={icon} className="h-8 w-8" />
|
||||||
|
<h2 className="mt-4 font-display text-base text-slate-900 dark:text-white">
|
||||||
|
<NextLink href={href}>
|
||||||
|
<a>
|
||||||
|
<span className="absolute -inset-px rounded-xl" />
|
||||||
|
{title}
|
||||||
|
</a>
|
||||||
|
</NextLink>
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-slate-700 dark:text-slate-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import Image from 'next/image'
|
||||||
|
|
||||||
|
export function Logo() {
|
||||||
|
return (
|
||||||
|
<div className="hidden min-w-full items-center gap-2 lg:flex">
|
||||||
|
<Image src="/logo.png" alt="Logo" width={30} height={30} />
|
||||||
|
<span className="text-xl font-semibold">Anchor</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { Dialog } from '@headlessui/react'
|
||||||
|
|
||||||
|
import { Logo } from '@/components/Logo'
|
||||||
|
import { Navigation } from '@/components/Navigation'
|
||||||
|
|
||||||
|
export function MobileNavigation({ navigation }) {
|
||||||
|
let router = useRouter()
|
||||||
|
let [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) return
|
||||||
|
|
||||||
|
function onRouteChange() {
|
||||||
|
setIsOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
router.events.on('routeChangeComplete', onRouteChange)
|
||||||
|
router.events.on('routeChangeError', onRouteChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeComplete', onRouteChange)
|
||||||
|
router.events.off('routeChangeError', onRouteChange)
|
||||||
|
}
|
||||||
|
}, [router, isOpen])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Open navigation</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 stroke-slate-500"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
|
<path d="M4 7h16M4 12h16M4 17h16" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Dialog
|
||||||
|
open={isOpen}
|
||||||
|
onClose={setIsOpen}
|
||||||
|
className="fixed inset-0 z-50 flex items-start overflow-y-auto bg-slate-900/50 pr-10 backdrop-blur lg:hidden"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="min-h-full w-full max-w-xs bg-white px-4 pt-5 pb-12 dark:bg-slate-900 sm:px-6">
|
||||||
|
<Dialog.Title className="sr-only">Navigation</Dialog.Title>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button type="button" onClick={() => setIsOpen(false)}>
|
||||||
|
<span className="sr-only">Close navigation</span>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-6 w-6 stroke-slate-500"
|
||||||
|
fill="none"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
|
<path d="M5 5l14 14M19 5l-14 14" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<Link href="/">
|
||||||
|
<a className="ml-6 block w-10 overflow-hidden lg:w-auto">
|
||||||
|
<span className="sr-only">Home page</span>
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Navigation navigation={navigation} className="mt-5 px-1" />
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export function Navigation({ navigation, className }) {
|
||||||
|
let router = useRouter()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={clsx('text-base lg:text-sm', className)}>
|
||||||
|
<ul className="space-y-9">
|
||||||
|
{navigation.map((section) => (
|
||||||
|
<li key={section.title}>
|
||||||
|
<h2 className="font-display font-medium text-slate-900 dark:text-white">
|
||||||
|
{section.title}
|
||||||
|
</h2>
|
||||||
|
<ul className="mt-2 space-y-2 border-l-2 border-slate-100 dark:border-slate-800 lg:mt-4 lg:space-y-4 lg:border-slate-200">
|
||||||
|
{section.links.map((link) => (
|
||||||
|
<li key={link.href} className="relative">
|
||||||
|
<Link href={link.href}>
|
||||||
|
<a
|
||||||
|
className={clsx(
|
||||||
|
'block w-full pl-3.5 before:pointer-events-none before:absolute before:-left-1 before:top-1/2 before:h-1.5 before:w-1.5 before:-translate-y-1/2 before:rounded-full',
|
||||||
|
{
|
||||||
|
'font-semibold text-sky-500 before:bg-sky-500':
|
||||||
|
link.href === router.pathname,
|
||||||
|
'text-slate-500 before:hidden before:bg-slate-300 hover:text-slate-600 hover:before:block dark:text-slate-400 dark:before:bg-slate-700 dark:hover:text-slate-300':
|
||||||
|
link.href !== router.pathname,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export function Prose({ as: Component = 'div', className, ...props }) {
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'prose prose-slate max-w-none dark:prose-invert dark:text-slate-400',
|
||||||
|
// headings
|
||||||
|
'prose-headings:scroll-mt-28 prose-headings:font-display prose-headings:font-normal lg:prose-headings:scroll-mt-[8.5rem]',
|
||||||
|
// lead
|
||||||
|
'prose-lead:text-slate-500 dark:prose-lead:text-slate-400',
|
||||||
|
// links
|
||||||
|
'prose-a:font-semibold dark:prose-a:text-sky-400',
|
||||||
|
// link underline
|
||||||
|
'prose-a:no-underline prose-a:shadow-[inset_0_-2px_0_0_var(--tw-prose-background,#fff),inset_0_calc(-1*(var(--tw-prose-underline-size,4px)+2px))_0_0_var(--tw-prose-underline,theme(colors.sky.300))] hover:prose-a:[--tw-prose-underline-size:6px] dark:[--tw-prose-background:theme(colors.slate.900)] dark:prose-a:shadow-[inset_0_calc(-1*var(--tw-prose-underline-size,2px))_0_0_var(--tw-prose-underline,theme(colors.sky.800))] dark:hover:prose-a:[--tw-prose-underline-size:6px]',
|
||||||
|
// pre
|
||||||
|
'prose-pre:rounded-xl prose-pre:bg-slate-900 prose-pre:shadow-lg dark:prose-pre:bg-slate-800/60 dark:prose-pre:shadow-none dark:prose-pre:ring-1 dark:prose-pre:ring-slate-300/10',
|
||||||
|
// hr
|
||||||
|
'dark:prose-hr:border-slate-800'
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { createPortal } from 'react-dom'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import { DocSearchModal, useDocSearchKeyboardEvents } from '@docsearch/react'
|
||||||
|
|
||||||
|
const docSearchConfig = {
|
||||||
|
appId: process.env.NEXT_PUBLIC_DOCSEARCH_APP_ID,
|
||||||
|
apiKey: process.env.NEXT_PUBLIC_DOCSEARCH_API_KEY,
|
||||||
|
indexName: process.env.NEXT_PUBLIC_DOCSEARCH_INDEX_NAME,
|
||||||
|
}
|
||||||
|
|
||||||
|
function Hit({ hit, children }) {
|
||||||
|
return (
|
||||||
|
<Link href={hit.url}>
|
||||||
|
<a>{children}</a>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Search() {
|
||||||
|
let [isOpen, setIsOpen] = useState(false)
|
||||||
|
let [modifierKey, setModifierKey] = useState()
|
||||||
|
|
||||||
|
const onOpen = useCallback(() => {
|
||||||
|
setIsOpen(true)
|
||||||
|
}, [setIsOpen])
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
setIsOpen(false)
|
||||||
|
}, [setIsOpen])
|
||||||
|
|
||||||
|
useDocSearchKeyboardEvents({ isOpen, onOpen, onClose })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setModifierKey(
|
||||||
|
/(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) ? '⌘' : 'Ctrl '
|
||||||
|
)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="group flex h-6 w-6 items-center justify-center sm:justify-start md:h-auto md:w-80 md:flex-none md:rounded-lg md:py-2.5 md:pl-4 md:pr-3.5 md:text-sm md:ring-1 md:ring-slate-200 md:hover:ring-slate-300 dark:md:bg-slate-800/75 dark:md:ring-inset dark:md:ring-white/5 dark:md:hover:bg-slate-700/40 dark:md:hover:ring-slate-500 lg:w-96"
|
||||||
|
onClick={onOpen}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="h-5 w-5 flex-none fill-slate-400 group-hover:fill-slate-500 dark:fill-slate-500 md:group-hover:fill-slate-400"
|
||||||
|
>
|
||||||
|
<path d="M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z" />
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only md:not-sr-only md:ml-2 md:text-slate-500 md:dark:text-slate-400">
|
||||||
|
Search docs
|
||||||
|
</span>
|
||||||
|
{modifierKey && (
|
||||||
|
<kbd className="ml-auto hidden font-medium text-slate-400 dark:text-slate-500 md:block">
|
||||||
|
<kbd className="font-sans">{modifierKey}</kbd>
|
||||||
|
<kbd className="font-sans">K</kbd>
|
||||||
|
</kbd>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{isOpen &&
|
||||||
|
createPortal(
|
||||||
|
<DocSearchModal
|
||||||
|
{...docSearchConfig}
|
||||||
|
initialScrollY={window.scrollY}
|
||||||
|
onClose={onClose}
|
||||||
|
hitComponent={Hit}
|
||||||
|
navigator={{
|
||||||
|
navigate({ itemUrl }) {
|
||||||
|
Router.push(itemUrl)
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Listbox } from '@headlessui/react'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
const themes = [
|
||||||
|
{ name: 'Light', value: 'light', icon: LightIcon },
|
||||||
|
{ name: 'Dark', value: 'dark', icon: DarkIcon },
|
||||||
|
{ name: 'System', value: 'system', icon: SystemIcon },
|
||||||
|
]
|
||||||
|
|
||||||
|
function IconBase({ children, ...props }) {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 16 16" {...props}>
|
||||||
|
{children}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LightIcon(props) {
|
||||||
|
return (
|
||||||
|
<IconBase {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M7 1a1 1 0 0 1 2 0v1a1 1 0 1 1-2 0V1Zm4 7a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm2.657-5.657a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm-1.415 11.313-.707-.707a1 1 0 0 1 1.415-1.415l.707.708a1 1 0 0 1-1.415 1.414ZM16 7.999a1 1 0 0 0-1-1h-1a1 1 0 1 0 0 2h1a1 1 0 0 0 1-1ZM7 14a1 1 0 1 1 2 0v1a1 1 0 1 1-2 0v-1Zm-2.536-2.464a1 1 0 0 0-1.414 0l-.707.707a1 1 0 0 0 1.414 1.414l.707-.707a1 1 0 0 0 0-1.414Zm0-8.486A1 1 0 0 1 3.05 4.464l-.707-.707a1 1 0 0 1 1.414-1.414l.707.707ZM3 8a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h1a1 1 0 0 0 1-1Z"
|
||||||
|
/>
|
||||||
|
</IconBase>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DarkIcon(props) {
|
||||||
|
return (
|
||||||
|
<IconBase {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M7.23 3.333C7.757 2.905 7.68 2 7 2a6 6 0 1 0 0 12c.68 0 .758-.905.23-1.332A5.989 5.989 0 0 1 5 8c0-1.885.87-3.568 2.23-4.668ZM12 5a1 1 0 0 1 1 1 1 1 0 0 0 1 1 1 1 0 1 1 0 2 1 1 0 0 0-1 1 1 1 0 1 1-2 0 1 1 0 0 0-1-1 1 1 0 1 1 0-2 1 1 0 0 0 1-1 1 1 0 0 1 1-1Z"
|
||||||
|
/>
|
||||||
|
</IconBase>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SystemIcon(props) {
|
||||||
|
return (
|
||||||
|
<IconBase {...props}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M1 4a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3h-1.5l.31 1.242c.084.333.36.573.63.808.091.08.182.158.264.24A1 1 0 0 1 11 15H5a1 1 0 0 1-.704-1.71c.082-.082.173-.16.264-.24.27-.235.546-.475.63-.808L5.5 11H4a3 3 0 0 1-3-3V4Zm3-1a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4Z"
|
||||||
|
/>
|
||||||
|
</IconBase>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ThemeSelector(props) {
|
||||||
|
let [selectedTheme, setSelectedTheme] = useState()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedTheme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', selectedTheme.value)
|
||||||
|
} else {
|
||||||
|
setSelectedTheme(
|
||||||
|
themes.find(
|
||||||
|
(theme) =>
|
||||||
|
theme.value === document.documentElement.getAttribute('data-theme')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [selectedTheme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Listbox
|
||||||
|
as="div"
|
||||||
|
value={selectedTheme}
|
||||||
|
onChange={setSelectedTheme}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Listbox.Label className="sr-only">Theme</Listbox.Label>
|
||||||
|
<Listbox.Button className="flex h-6 w-6 items-center justify-center rounded-lg shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
||||||
|
<span className="sr-only">{selectedTheme?.name}</span>
|
||||||
|
<LightIcon className="hidden h-4 w-4 fill-sky-400 [[data-theme=light]_&]:block" />
|
||||||
|
<DarkIcon className="hidden h-4 w-4 fill-sky-400 [[data-theme=dark]_&]:block" />
|
||||||
|
<LightIcon className="hidden h-4 w-4 fill-slate-400 [:not(.dark)[data-theme=system]_&]:block" />
|
||||||
|
<DarkIcon className="hidden h-4 w-4 fill-slate-400 [.dark[data-theme=system]_&]:block" />
|
||||||
|
</Listbox.Button>
|
||||||
|
<Listbox.Options className="absolute top-full left-1/2 mt-3 w-36 -translate-x-1/2 space-y-1 rounded-xl bg-white p-3 text-sm font-medium shadow-md shadow-black/5 ring-1 ring-black/5 dark:bg-slate-800 dark:ring-white/5">
|
||||||
|
{themes.map((theme) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={theme.value}
|
||||||
|
value={theme}
|
||||||
|
className={({ active, selected }) =>
|
||||||
|
clsx(
|
||||||
|
'flex cursor-pointer select-none items-center rounded-[0.625rem] p-1',
|
||||||
|
{
|
||||||
|
'text-sky-500': selected,
|
||||||
|
'text-slate-900 dark:text-white': active && !selected,
|
||||||
|
'text-slate-700 dark:text-slate-400': !active && !selected,
|
||||||
|
'bg-slate-100 dark:bg-slate-900/40': active,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ selected }) => (
|
||||||
|
<>
|
||||||
|
<div className="rounded-md bg-white p-1 shadow ring-1 ring-slate-900/5 dark:bg-slate-700 dark:ring-inset dark:ring-white/5">
|
||||||
|
<theme.icon
|
||||||
|
className={clsx('h-4 w-4', {
|
||||||
|
'fill-sky-400 dark:fill-sky-400': selected,
|
||||||
|
'fill-slate-400': !selected,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">{theme.name}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Listbox>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { DarkMode, Gradient, LightMode } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function InstallationIcon({ id, color }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 12 3)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 16 7)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={12} cy={12} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="m8 8 9 21 2-10 10-2L8 8Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
d="m4 4 10.286 24 2.285-11.429L28 14.286 4 4Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
stroke={`url(#${id}-gradient-dark)`}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { DarkMode, Gradient, LightMode } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function LightbulbIcon({ id, color }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 20 11)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 24.5001 -19.2498 0 16 5.5)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M20 24.995c0-1.855 1.094-3.501 2.427-4.792C24.61 18.087 26 15.07 26 12.231 26 7.133 21.523 3 16 3S6 7.133 6 12.23c0 2.84 1.389 5.857 3.573 7.973C10.906 21.494 12 23.14 12 24.995V27a2 2 0 0 0 2 2h4a2 2 0 0 0 2-2v-2.005Z"
|
||||||
|
className="fill-[var(--icon-background)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M25 12.23c0 2.536-1.254 5.303-3.269 7.255l1.391 1.436c2.354-2.28 3.878-5.547 3.878-8.69h-2ZM16 4c5.047 0 9 3.759 9 8.23h2C27 6.508 21.998 2 16 2v2Zm-9 8.23C7 7.76 10.953 4 16 4V2C10.002 2 5 6.507 5 12.23h2Zm3.269 7.255C8.254 17.533 7 14.766 7 12.23H5c0 3.143 1.523 6.41 3.877 8.69l1.392-1.436ZM13 27v-2.005h-2V27h2Zm1 1a1 1 0 0 1-1-1h-2a3 3 0 0 0 3 3v-2Zm4 0h-4v2h4v-2Zm1-1a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2Zm0-2.005V27h2v-2.005h-2ZM8.877 20.921C10.132 22.136 11 23.538 11 24.995h2c0-2.253-1.32-4.143-2.731-5.51L8.877 20.92Zm12.854-1.436C20.32 20.852 19 22.742 19 24.995h2c0-1.457.869-2.859 2.122-4.074l-1.391-1.436Z"
|
||||||
|
className="fill-[var(--icon-foreground)]"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M20 26a1 1 0 1 0 0-2v2Zm-8-2a1 1 0 1 0 0 2v-2Zm2 0h-2v2h2v-2Zm1 1V13.5h-2V25h2Zm-5-11.5v1h2v-1h-2Zm3.5 4.5h5v-2h-5v2Zm8.5-3.5v-1h-2v1h2ZM20 24h-2v2h2v-2Zm-2 0h-4v2h4v-2Zm-1-10.5V25h2V13.5h-2Zm2.5-2.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2ZM18.5 18a3.5 3.5 0 0 0 3.5-3.5h-2a1.5 1.5 0 0 1-1.5 1.5v2ZM10 14.5a3.5 3.5 0 0 0 3.5 3.5v-2a1.5 1.5 0 0 1-1.5-1.5h-2Zm2.5-3.5a2.5 2.5 0 0 0-2.5 2.5h2a.5.5 0 0 1 .5-.5v-2Zm2.5 2.5a2.5 2.5 0 0 0-2.5-2.5v2a.5.5 0 0 1 .5.5h2Z"
|
||||||
|
className="fill-[var(--icon-foreground)]"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M16 2C10.002 2 5 6.507 5 12.23c0 3.144 1.523 6.411 3.877 8.691.75.727 1.363 1.52 1.734 2.353.185.415.574.726 1.028.726H12a1 1 0 0 0 1-1v-4.5a.5.5 0 0 0-.5-.5A3.5 3.5 0 0 1 9 14.5V14a3 3 0 1 1 6 0v9a1 1 0 1 0 2 0v-9a3 3 0 1 1 6 0v.5a3.5 3.5 0 0 1-3.5 3.5.5.5 0 0 0-.5.5V23a1 1 0 0 0 1 1h.36c.455 0 .844-.311 1.03-.726.37-.833.982-1.626 1.732-2.353 2.354-2.28 3.878-5.547 3.878-8.69C27 6.507 21.998 2 16 2Zm5 25a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1 3 3 0 0 0 3 3h4a3 3 0 0 0 3-3Zm-8-13v1.5a.5.5 0 0 1-.5.5 1.5 1.5 0 0 1-1.5-1.5V14a1 1 0 1 1 2 0Zm6.5 2a.5.5 0 0 1-.5-.5V14a1 1 0 1 1 2 0v.5a1.5 1.5 0 0 1-1.5 1.5Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { DarkMode, Gradient, LightMode } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function PluginsIcon({ id, color }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 20 11)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark-1`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark-2`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 14 -14 0 16 10)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<g
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 9v14l12 6V15L3 9Z" />
|
||||||
|
<path d="M27 9v14l-12 6V15l12-6Z" />
|
||||||
|
</g>
|
||||||
|
<path
|
||||||
|
d="M11 4h8v2l6 3-10 6L5 9l6-3V4Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)]"
|
||||||
|
/>
|
||||||
|
<g
|
||||||
|
className="stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M20 5.5 27 9l-12 6L3 9l7-3.5" />
|
||||||
|
<path d="M20 5c0 1.105-2.239 2-5 2s-5-.895-5-2m10 0c0-1.105-2.239-2-5-2s-5 .895-5 2m10 0v3c0 1.105-2.239 2-5 2s-5-.895-5-2V5" />
|
||||||
|
</g>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path
|
||||||
|
d="M17.676 3.38a3.887 3.887 0 0 0-3.352 0l-9 4.288C3.907 8.342 3 9.806 3 11.416v9.168c0 1.61.907 3.073 2.324 3.748l9 4.288a3.887 3.887 0 0 0 3.352 0l9-4.288C28.093 23.657 29 22.194 29 20.584v-9.168c0-1.61-.907-3.074-2.324-3.748l-9-4.288Z"
|
||||||
|
stroke={`url(#${id}-gradient-dark-1)`}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16.406 8.087a.989.989 0 0 0-.812 0l-7 3.598A1.012 1.012 0 0 0 8 12.61v6.78c0 .4.233.762.594.925l7 3.598a.989.989 0 0 0 .812 0l7-3.598c.361-.163.594-.525.594-.925v-6.78c0-.4-.233-.762-.594-.925l-7-3.598Z"
|
||||||
|
fill={`url(#${id}-gradient-dark-2)`}
|
||||||
|
stroke={`url(#${id}-gradient-dark-2)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { DarkMode, Gradient, LightMode } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function PresetsIcon({ id, color }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 20 3)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 22.75 -22.75 0 16 6.25)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={12} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<g
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M3 5v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
|
||||||
|
<path d="M18 17v10a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V17a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
|
||||||
|
<path d="M18 5v4a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2h-7a2 2 0 0 0-2 2Z" />
|
||||||
|
<path d="M3 25v2a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2v-2a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2Z" />
|
||||||
|
</g>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode fill={`url(#${id}-gradient-dark)`}>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 17V4a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Zm16 10v-9a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-6a2 2 0 0 1-2-2Zm0-23v5a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1h-8a1 1 0 0 0-1 1ZM3 28v-3a1 1 0 0 1 1-1h9a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1Z"
|
||||||
|
/>
|
||||||
|
<path d="M2 4v13h2V4H2Zm2-2a2 2 0 0 0-2 2h2V2Zm8 0H4v2h8V2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 13V4h-2v13h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-8 0h8v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Zm16 1v9h2v-9h-2Zm3-3a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1v-2Zm6 0h-6v2h6v-2Zm3 3a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2Zm0 9v-9h-2v9h2Zm-3 3a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2Zm-6 0h6v-2h-6v2Zm-3-3a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1h-2Zm2-18V4h-2v5h2Zm0 0h-2a2 2 0 0 0 2 2V9Zm8 0h-8v2h8V9Zm0 0v2a2 2 0 0 0 2-2h-2Zm0-5v5h2V4h-2Zm0 0h2a2 2 0 0 0-2-2v2Zm-8 0h8V2h-8v2Zm0 0V2a2 2 0 0 0-2 2h2ZM2 25v3h2v-3H2Zm2-2a2 2 0 0 0-2 2h2v-2Zm9 0H4v2h9v-2Zm2 2a2 2 0 0 0-2-2v2h2Zm0 3v-3h-2v3h2Zm-2 2a2 2 0 0 0 2-2h-2v2Zm-9 0h9v-2H4v2Zm-2-2a2 2 0 0 0 2 2v-2H2Z" />
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { DarkMode, Gradient, LightMode } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function ThemingIcon({ id, color }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 21 -21 0 12 11)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={12} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="M27 12.13 19.87 5 13 11.87v14.26l14-14Z"
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 3h10v22a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V3Z"
|
||||||
|
className="fill-[var(--icon-background)]"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M3 9v16a4 4 0 0 0 4 4h2a4 4 0 0 0 4-4V9M3 9V3h10v6M3 9h10M3 15h10M3 21h10"
|
||||||
|
className="stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M29 29V19h-8.5L13 26c0 1.5-2.5 3-5 3h21Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M3 2a1 1 0 0 0-1 1v21a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H3Zm16.752 3.293a1 1 0 0 0-1.593.244l-1.045 2A1 1 0 0 0 17 8v13a1 1 0 0 0 1.71.705l7.999-8.045a1 1 0 0 0-.002-1.412l-6.955-6.955ZM26 18a1 1 0 0 0-.707.293l-10 10A1 1 0 0 0 16 30h13a1 1 0 0 0 1-1V19a1 1 0 0 0-1-1h-3ZM5 18a1 1 0 1 0 0 2h6a1 1 0 1 0 0-2H5Zm-1-5a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1Zm1-7a1 1 0 0 0 0 2h6a1 1 0 1 0 0-2H5Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { DarkMode, Gradient, LightMode } from '@/components/Icon'
|
||||||
|
|
||||||
|
export function WarningIcon({ id, color }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<defs>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="rotate(65.924 1.519 20.92) scale(25.7391)"
|
||||||
|
/>
|
||||||
|
<Gradient
|
||||||
|
id={`${id}-gradient-dark`}
|
||||||
|
color={color}
|
||||||
|
gradientTransform="matrix(0 24.5 -24.5 0 16 5.5)"
|
||||||
|
/>
|
||||||
|
</defs>
|
||||||
|
<LightMode>
|
||||||
|
<circle cx={20} cy={20} r={12} fill={`url(#${id}-gradient)`} />
|
||||||
|
<path
|
||||||
|
d="M3 16c0 7.18 5.82 13 13 13s13-5.82 13-13S23.18 3 16 3 3 8.82 3 16Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="m15.408 16.509-1.04-5.543a1.66 1.66 0 1 1 3.263 0l-1.039 5.543a.602.602 0 0 1-1.184 0Z"
|
||||||
|
className="fill-[var(--icon-foreground)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M16 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
fillOpacity={0.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="fill-[var(--icon-background)] stroke-[color:var(--icon-foreground)]"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
</LightMode>
|
||||||
|
<DarkMode>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M2 16C2 8.268 8.268 2 16 2s14 6.268 14 14-6.268 14-14 14S2 23.732 2 16Zm11.386-4.85a2.66 2.66 0 1 1 5.228 0l-1.039 5.543a1.602 1.602 0 0 1-3.15 0l-1.04-5.543ZM16 20a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"
|
||||||
|
fill={`url(#${id}-gradient-dark)`}
|
||||||
|
/>
|
||||||
|
</DarkMode>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
# Installing Dependencies
|
|
||||||
|
|
||||||
To get started, make sure to setup all the prerequisite tools on your local machine
|
|
||||||
(an installer has not yet been developed).
|
|
||||||
|
|
||||||
## Install Rust
|
|
||||||
|
|
||||||
For an introduction to Rust, see the excellent Rust [book](https://doc.rust-lang.org/book/).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
||||||
source $HOME/.cargo/env
|
|
||||||
rustup component add rustfmt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install Solana
|
|
||||||
|
|
||||||
See the solana [docs](https://docs.solana.com/cli/install-solana-cli-tools) for installation instructions. On macOS and Linux,
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sh -c "$(curl -sSfL https://release.solana.com/v1.9.1/install)"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install Yarn
|
|
||||||
|
|
||||||
[Yarn](https://yarnpkg.com/) is recommended for JavaScript package management.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g yarn
|
|
||||||
```
|
|
||||||
|
|
||||||
## Install Anchor
|
|
||||||
|
|
||||||
### Install using pre-build binary on x86_64 Linux
|
|
||||||
|
|
||||||
Anchor binaries are available via an NPM package [`@project-serum/anchor-cli`](https://www.npmjs.com/package/@project-serum/anchor-cli). Only x86_64 Linux is supported currently, you must build from source for other OS'.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm i -g @project-serum/anchor-cli
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build from source for other operating systems
|
|
||||||
|
|
||||||
For now, we can use Cargo to install the CLI.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo install --git https://github.com/coral-xyz/anchor --tag v0.24.2 anchor-cli --locked
|
|
||||||
```
|
|
||||||
|
|
||||||
On Linux systems you may need to install additional dependencies if `cargo install` fails. On Ubuntu,
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Now verify the CLI is installed properly.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
anchor --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## Start a Project
|
|
||||||
|
|
||||||
To initialize a new project, simply run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
anchor init <new-project-name>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Minimum version requirements
|
|
||||||
|
|
||||||
| Build tool | Version |
|
|
||||||
|:------------|:---------------|
|
|
||||||
| Node.js | v11.0.0 |
|
|
|
@ -1,18 +0,0 @@
|
||||||
# Introduction
|
|
||||||
|
|
||||||
<div style="border: 2px solid red; text-align: center; padding: 10px 10px 10px 10px; box-sizing: border-box"> This documentation is being sunset in favor of <a href="https://book.anchor-lang.com" rel="noopener noreferrer" target="_blank">The Anchor Book</a>. At this point in time, either documentation may contain information that the other does not.</div>
|
|
||||||
|
|
||||||
Anchor is a framework for Solana's [Sealevel](https://medium.com/solana-labs/sealevel-parallel-processing-thousands-of-smart-contracts-d814b378192) runtime providing several convenient developer tools.
|
|
||||||
|
|
||||||
- Rust crates and eDSL for writing Solana programs
|
|
||||||
- [IDL](https://en.wikipedia.org/wiki/Interface_description_language) specification
|
|
||||||
- TypeScript package for generating clients from IDL
|
|
||||||
- CLI and workspace management for developing complete applications
|
|
||||||
|
|
||||||
If you're familiar with developing in Ethereum's [Solidity](https://docs.soliditylang.org/en/v0.7.4/), [Truffle](https://www.trufflesuite.com/), [web3.js](https://github.com/ethereum/web3.js) or Parity's [Ink!](https://github.com/paritytech/ink), then the experience will be familiar. Although the DSL syntax and semantics are targeted at Solana, the high level flow of writing RPC request handlers, emitting an IDL, and generating clients from IDL is the same.
|
|
||||||
|
|
||||||
Here, we'll walk through several tutorials demonstrating how to use Anchor. To skip the tutorials and jump straight to examples, go [here](https://github.com/coral-xyz/anchor/blob/master/examples). For an introduction to Solana, see the [docs](https://docs.solana.com/developing/programming-model/overview).
|
|
||||||
|
|
||||||
::: tip NOTE
|
|
||||||
Anchor is in active development, so all APIs are subject to change. If you are one of the early developers to try it out and have feedback, please reach out by [filing an issue](https://github.com/coral-xyz/anchor/issues/new). This documentation is a work in progress and is expected to change dramatically as features continue to be built out. If you have any problems, consult the [source](https://github.com/coral-xyz/anchor) or feel free to ask questions on the [Discord](https://discord.gg/JgVgQ82erk).
|
|
||||||
:::
|
|
|
@ -1,24 +0,0 @@
|
||||||
# Projects
|
|
||||||
|
|
||||||
Open a pull request to add your project to the [list](https://github.com/coral-xyz/anchor/blob/master/docs/src/getting-started/projects.md).
|
|
||||||
|
|
||||||
* [Serum](https://github.com/project-serum)
|
|
||||||
* [Synthetify](https://github.com/Synthetify)
|
|
||||||
* [SolFarm](https://solfarm.io/)
|
|
||||||
* [Zeta Markets](https://zeta.markets/)
|
|
||||||
* [Saber](https://saber.so)
|
|
||||||
* [01](https://01protocol.com/)
|
|
||||||
* [Parrot Finance](https://parrot.fi/)
|
|
||||||
* [Marinade Finance](https://marinade.finance/)
|
|
||||||
* [Aldrin](https://dex.aldrin.com/)
|
|
||||||
* [Cyclos](https://cyclos.io/)
|
|
||||||
* [Solend](https://solend.fi)
|
|
||||||
* [Drift](https://www.drift.trade/)
|
|
||||||
* [Fabric](https://stake.fsynth.io/)
|
|
||||||
* [Jet Protocol](https://jetprotocol.io/)
|
|
||||||
* [Quarry](https://quarry.so/)
|
|
||||||
* [PsyOptions](https://psyoptions.io/)
|
|
||||||
* [sosol](https://sosol.app/)
|
|
||||||
* [Arrow Protocol](https://arrowprotocol.com/)
|
|
||||||
* [Hubble Protocol](https://hubbleprotocol.io/)
|
|
||||||
|
|
After Width: | Height: | Size: 214 KiB |
After Width: | Height: | Size: 219 KiB |
|
@ -1,23 +0,0 @@
|
||||||
---
|
|
||||||
home: true
|
|
||||||
heroText: Anchor
|
|
||||||
tagline: A framework for building Solana programs
|
|
||||||
actionText: Get Started →
|
|
||||||
actionLink: /getting-started/introduction
|
|
||||||
features:
|
|
||||||
- title: Security
|
|
||||||
details: Anchor eliminates many footguns of raw Solana programs by default and allows you to add more security checks without disrupting your business logic.
|
|
||||||
- title: Code Generation
|
|
||||||
details: (De)Serialization, cross-program invocations, account initialization, and more.
|
|
||||||
- title: IDL & Client Generation
|
|
||||||
details: Anchor generates an IDL based on your program and automatically creates a typescript client with it.
|
|
||||||
- title: Verifiability
|
|
||||||
details: Anchor programs can be built verifiably, so users know that the on-chain program matches the code base.
|
|
||||||
- title: Workspace Management
|
|
||||||
details: The CLI helps you manage workspaces with multiple programs, e2e tests, and more.
|
|
||||||
- title: Compatibility
|
|
||||||
details: Anchor programs can interact with all non-anchor programs on Solana.
|
|
||||||
|
|
||||||
footer: Apache License 2.0
|
|
||||||
---
|
|
||||||
<div style="border: 2px solid red; text-align: center; padding: 10px 10px 10px 10px; box-sizing: border-box"> This documentation is being sunset in favor of <a href="https://book.anchor-lang.com" rel="noopener noreferrer" target="_blank">The Anchor Book</a>. At this point in time, either documentation may contain information that the other does not.</div>
|
|
|
@ -0,0 +1,149 @@
|
||||||
|
import Head from 'next/head'
|
||||||
|
import { slugifyWithCounter } from '@sindresorhus/slugify'
|
||||||
|
import PlausibleProvider from 'next-plausible'
|
||||||
|
|
||||||
|
import Prism from 'prism-react-renderer/prism'
|
||||||
|
;(typeof global !== 'undefined' ? global : window).Prism = Prism
|
||||||
|
|
||||||
|
require('prismjs/components/prism-rust')
|
||||||
|
require('prismjs/components/prism-toml')
|
||||||
|
|
||||||
|
import { Layout } from '@/components/Layout'
|
||||||
|
|
||||||
|
import 'focus-visible'
|
||||||
|
import '@/styles/tailwind.css'
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{
|
||||||
|
title: 'Introduction',
|
||||||
|
links: [
|
||||||
|
{ title: 'Getting started', href: '/' },
|
||||||
|
{ title: 'Installation', href: '/docs/installation' },
|
||||||
|
{ title: 'Hello World', href: '/docs/hello-world' },
|
||||||
|
{ title: 'Intro to Solana', href: '/docs/intro-to-solana' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Core concepts',
|
||||||
|
links: [
|
||||||
|
{ title: 'High-level Overview', href: '/docs/high-level-overview' },
|
||||||
|
{
|
||||||
|
title: 'The Accounts Struct',
|
||||||
|
href: '/docs/the-accounts-struct',
|
||||||
|
},
|
||||||
|
{ title: 'The Program Module', href: '/docs/the-program-module' },
|
||||||
|
{
|
||||||
|
title: 'Errors',
|
||||||
|
href: '/docs/errors',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Cross-Program Invocations',
|
||||||
|
href: '/docs/cross-program-invocations',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Program Derived Addresses',
|
||||||
|
href: '/docs/pdas',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Guides',
|
||||||
|
links: [
|
||||||
|
{ title: 'Publishing Source', href: '/docs/publishing-source' },
|
||||||
|
{
|
||||||
|
title: 'Verifiable Builds',
|
||||||
|
href: '/docs/verifiable-builds',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'References',
|
||||||
|
links: [
|
||||||
|
{ title: 'Space', href: '/docs/space' },
|
||||||
|
{
|
||||||
|
title: 'JavaScript Anchor Types',
|
||||||
|
href: '/docs/javascript-anchor-types',
|
||||||
|
},
|
||||||
|
{ title: 'CLI', href: '/docs/cli' },
|
||||||
|
{ title: 'AVM', href: '/docs/avm' },
|
||||||
|
{ title: 'Anchor.toml', href: '/docs/manifest' },
|
||||||
|
{
|
||||||
|
title: 'Important Links',
|
||||||
|
href: '/docs/important-links',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Projects',
|
||||||
|
links: [{ title: 'Tic-Tac-Toe', href: '/docs/tic-tac-toe' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function getNodeText(node) {
|
||||||
|
let text = ''
|
||||||
|
for (let child of node.children ?? []) {
|
||||||
|
if (typeof child === 'string') {
|
||||||
|
text += child
|
||||||
|
}
|
||||||
|
text += getNodeText(child)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectHeadings(nodes, slugify = slugifyWithCounter()) {
|
||||||
|
let sections = []
|
||||||
|
|
||||||
|
for (let node of nodes) {
|
||||||
|
if (/^h[23]$/.test(node.name)) {
|
||||||
|
let title = getNodeText(node)
|
||||||
|
if (title) {
|
||||||
|
let id = slugify(title)
|
||||||
|
node.attributes.id = id
|
||||||
|
if (node.name === 'h3') {
|
||||||
|
sections[sections.length - 1].children.push({
|
||||||
|
...node.attributes,
|
||||||
|
title,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
sections.push({ ...node.attributes, title, children: [] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push(...collectHeadings(node.children ?? [], slugify))
|
||||||
|
}
|
||||||
|
|
||||||
|
return sections
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }) {
|
||||||
|
let title = pageProps.markdoc?.frontmatter.title
|
||||||
|
|
||||||
|
let pageTitle =
|
||||||
|
pageProps.markdoc?.frontmatter.pageTitle ||
|
||||||
|
`${pageProps.markdoc?.frontmatter.title} - Docs`
|
||||||
|
|
||||||
|
let description = pageProps.markdoc?.frontmatter.description
|
||||||
|
|
||||||
|
let tableOfContents = pageProps.markdoc?.content
|
||||||
|
? collectHeadings(pageProps.markdoc.content)
|
||||||
|
: []
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PlausibleProvider domain="anchor-lang.com" trackOutboundLinks={true}>
|
||||||
|
<Head>
|
||||||
|
<title>{pageTitle}</title>
|
||||||
|
{description && <meta name="description" content={description} />}
|
||||||
|
</Head>
|
||||||
|
<Layout
|
||||||
|
navigation={navigation}
|
||||||
|
title={title}
|
||||||
|
tableOfContents={tableOfContents}
|
||||||
|
>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</Layout>
|
||||||
|
</PlausibleProvider>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Head, Html, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
const themeScript = `
|
||||||
|
let mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
|
||||||
|
function updateTheme(savedTheme) {
|
||||||
|
let theme = 'system'
|
||||||
|
try {
|
||||||
|
if (!savedTheme) {
|
||||||
|
savedTheme = window.localStorage.theme
|
||||||
|
}
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
theme = 'dark'
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else if (savedTheme === 'light') {
|
||||||
|
theme = 'light'
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
} else if (mediaQuery.matches) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
theme = 'light'
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
return theme
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateThemeWithoutTransitions(savedTheme) {
|
||||||
|
updateTheme(savedTheme)
|
||||||
|
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||||
|
window.setTimeout(() => {
|
||||||
|
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('data-theme', updateTheme())
|
||||||
|
|
||||||
|
new MutationObserver(([{ oldValue }]) => {
|
||||||
|
let newValue = document.documentElement.getAttribute('data-theme')
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem('theme', newValue)
|
||||||
|
} catch {}
|
||||||
|
updateThemeWithoutTransitions(newValue)
|
||||||
|
}
|
||||||
|
}).observe(document.documentElement, { attributeFilter: ['data-theme'], attributeOldValue: true })
|
||||||
|
|
||||||
|
mediaQuery.addEventListener('change', updateThemeWithoutTransitions)
|
||||||
|
window.addEventListener('storage', updateThemeWithoutTransitions)
|
||||||
|
`
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html className="antialiased [font-feature-settings:'ss01']" lang="en">
|
||||||
|
<Head>
|
||||||
|
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
|
||||||
|
</Head>
|
||||||
|
<body className="bg-white dark:bg-slate-900">
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
---
|
||||||
|
title: Anchor Version Manager
|
||||||
|
description: Anchor - Anchor Version Manager
|
||||||
|
---
|
||||||
|
|
||||||
|
Anchor Version Manager (avm) is provided to manage multiple installations of the anchor-cli binary. This may be required to produce verifiable builds, or if you'd prefer to work with an alternate version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```shell
|
||||||
|
Anchor version manager
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
avm <SUBCOMMAND>
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-h, --help Print help information
|
||||||
|
-V, --version Print version information
|
||||||
|
|
||||||
|
SUBCOMMANDS:
|
||||||
|
help Print this message or the help of the given subcommand(s)
|
||||||
|
install Install a version of Anchor
|
||||||
|
list List available versions of Anchor
|
||||||
|
uninstall Uninstall a version of Anchor
|
||||||
|
use Use a specific version of Anchor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```shell
|
||||||
|
avm install <version>
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the specified version of anchor-cli. The version argument should follow semver versioning. It is also possible to use `latest` as the version argument to install the latest version.
|
||||||
|
|
||||||
|
## List
|
||||||
|
|
||||||
|
```shell
|
||||||
|
avm list
|
||||||
|
```
|
||||||
|
|
||||||
|
Lists available versions of anchor-cli.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
0.3.0
|
||||||
|
0.4.0
|
||||||
|
0.4.1
|
||||||
|
0.4.2
|
||||||
|
0.4.3
|
||||||
|
0.4.4
|
||||||
|
0.4.5
|
||||||
|
0.5.0
|
||||||
|
0.6.0
|
||||||
|
0.7.0
|
||||||
|
0.8.0
|
||||||
|
0.9.0
|
||||||
|
0.10.0
|
||||||
|
0.11.0
|
||||||
|
0.11.1
|
||||||
|
0.12.0
|
||||||
|
0.13.0
|
||||||
|
0.13.1
|
||||||
|
0.13.2
|
||||||
|
0.14.0
|
||||||
|
0.15.0
|
||||||
|
0.16.0
|
||||||
|
0.16.1
|
||||||
|
0.16.2
|
||||||
|
0.17.0
|
||||||
|
0.18.0
|
||||||
|
0.18.2
|
||||||
|
0.19.0
|
||||||
|
0.20.0 (installed)
|
||||||
|
0.20.1 (latest, installed, current)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uninstall
|
||||||
|
|
||||||
|
```shell
|
||||||
|
avm uninstall <version>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
```shell
|
||||||
|
avm use <version>
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a specific version. This version will remain in use until you change it by calling the same command again. Similarly to `avm install`, you can also use `latest` for the version.
|
|
@ -1,10 +1,15 @@
|
||||||
# Commands
|
---
|
||||||
|
title: CLI
|
||||||
|
description: Anchor - CLI
|
||||||
|
---
|
||||||
|
|
||||||
A CLI is provided to support building and managing an Anchor workspace.
|
A CLI is provided to support building and managing an Anchor workspace.
|
||||||
For a comprehensive list of commands and options, run `anchor -h` on any
|
For a comprehensive list of commands and options, run `anchor -h` on any
|
||||||
of the following subcommands.
|
of the following subcommands.
|
||||||
|
|
||||||
```
|
---
|
||||||
|
|
||||||
|
```shell
|
||||||
anchor-cli
|
anchor-cli
|
||||||
|
|
||||||
USAGE:
|
USAGE:
|
||||||
|
@ -24,6 +29,7 @@ SUBCOMMANDS:
|
||||||
init Initializes a workspace
|
init Initializes a workspace
|
||||||
migrate Runs the deploy migration script
|
migrate Runs the deploy migration script
|
||||||
new Creates a new program
|
new Creates a new program
|
||||||
|
shell Starts a node shell with an Anchor client setup according to the local config
|
||||||
test Runs integration tests against a localnetwork
|
test Runs integration tests against a localnetwork
|
||||||
upgrade Upgrades a single program. The configured wallet must be the upgrade authority
|
upgrade Upgrades a single program. The configured wallet must be the upgrade authority
|
||||||
verify Verifies the on-chain bytecode matches the locally compiled artifact. Run this
|
verify Verifies the on-chain bytecode matches the locally compiled artifact. Run this
|
||||||
|
@ -31,16 +37,15 @@ SUBCOMMANDS:
|
||||||
Cargo.toml
|
Cargo.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor build
|
anchor build
|
||||||
```
|
```
|
||||||
|
|
||||||
Builds programs in the workspace targeting Solana's BPF runtime and emitting IDLs in the `target/idl` directory.
|
Builds programs in the workspace targeting Solana's BPF runtime and emitting IDLs in the `target/idl` directory.
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor build --verifiable
|
anchor build --verifiable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -50,13 +55,13 @@ Runs the build inside a docker image so that the output binary is deterministic
|
||||||
|
|
||||||
### Cluster list
|
### Cluster list
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor cluster list
|
anchor cluster list
|
||||||
```
|
```
|
||||||
|
|
||||||
This lists cluster endpoints:
|
This lists cluster endpoints:
|
||||||
|
|
||||||
```
|
```shell
|
||||||
Cluster Endpoints:
|
Cluster Endpoints:
|
||||||
|
|
||||||
* Mainnet - https://solana-api.projectserum.com
|
* Mainnet - https://solana-api.projectserum.com
|
||||||
|
@ -67,20 +72,20 @@ Cluster Endpoints:
|
||||||
|
|
||||||
## Deploy
|
## Deploy
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor deploy
|
anchor deploy
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploys all programs in the workspace to the configured cluster.
|
Deploys all programs in the workspace to the configured cluster.
|
||||||
|
|
||||||
::: tip Note
|
{% callout title="Tip" %}
|
||||||
This is different from the `solana program deploy` command, because everytime it's run
|
This is different from the `solana program deploy` command, because everytime it's run
|
||||||
it will generate a *new* program address.
|
it will generate a _new_ program address.
|
||||||
:::
|
{% /callout %}
|
||||||
|
|
||||||
## Expand
|
## Expand
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor expand
|
anchor expand
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -99,7 +104,7 @@ allows us to generate clients for a program using nothing but the program ID.
|
||||||
|
|
||||||
### Idl Init
|
### Idl Init
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl init -f <target/idl/program.json> <program-id>
|
anchor idl init -f <target/idl/program.json> <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -108,20 +113,20 @@ allowing room for growth in case the idl needs to be upgraded in the future.
|
||||||
|
|
||||||
### Idl Fetch
|
### Idl Fetch
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl fetch -o <out-file.json> <program-id>
|
anchor idl fetch -o <out-file.json> <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
Fetches an IDL from the configured blockchain. For example, make sure
|
Fetches an IDL from the configured blockchain. For example, make sure
|
||||||
your `Anchor.toml` is pointing to the `mainnet` cluster and run
|
your `Anchor.toml` is pointing to the `mainnet` cluster and run
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl fetch GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv
|
anchor idl fetch GrAkKfEpTKQuVHG2Y97Y2FF4i7y7Q5AHLK94JBy7Y5yv
|
||||||
```
|
```
|
||||||
|
|
||||||
### Idl Authority
|
### Idl Authority
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl authority <program-id>
|
anchor idl authority <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -130,7 +135,7 @@ update the IDL.
|
||||||
|
|
||||||
### Idl Erase Authority
|
### Idl Erase Authority
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl erase-authority -p <program-id>
|
anchor idl erase-authority -p <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -139,14 +144,14 @@ configured wallet must be the current authority.
|
||||||
|
|
||||||
### Idl Upgrade
|
### Idl Upgrade
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl upgrade <program-id> -f <target/idl/program.json>
|
anchor idl upgrade <program-id> -f <target/idl/program.json>
|
||||||
```
|
```
|
||||||
|
|
||||||
Upgrades the IDL file on chain to the new `target/idl/program.json` idl.
|
Upgrades the IDL file on chain to the new `target/idl/program.json` idl.
|
||||||
The configured wallet must be the current authority.
|
The configured wallet must be the current authority.
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor idl set-authority -n <new-authority> -p <program-id>
|
anchor idl set-authority -n <new-authority> -p <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -155,23 +160,23 @@ must be encoded in base 58.
|
||||||
|
|
||||||
## Init
|
## Init
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor init
|
anchor init
|
||||||
```
|
```
|
||||||
|
|
||||||
Initializes a project workspace with the following structure.
|
Initializes a project workspace with the following structure.
|
||||||
|
|
||||||
* `Anchor.toml`: Anchor configuration file.
|
- `Anchor.toml`: Anchor configuration file.
|
||||||
* `Cargo.toml`: Rust workspace configuration file.
|
- `Cargo.toml`: Rust workspace configuration file.
|
||||||
* `package.json`: JavaScript dependencies file.
|
- `package.json`: JavaScript dependencies file.
|
||||||
* `programs/`: Directory for Solana program crates.
|
- `programs/`: Directory for Solana program crates.
|
||||||
* `app/`: Directory for your application frontend.
|
- `app/`: Directory for your application frontend.
|
||||||
* `tests/`: Directory for JavaScript integration tests.
|
- `tests/`: Directory for JavaScript integration tests.
|
||||||
* `migrations/deploy.js`: Deploy script.
|
- `migrations/deploy.js`: Deploy script.
|
||||||
|
|
||||||
## Migrate
|
## Migrate
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor migrate
|
anchor migrate
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -181,10 +186,10 @@ from the workspace's `Anchor.toml`. For example,
|
||||||
```javascript
|
```javascript
|
||||||
// File: migrations/deploys.js
|
// File: migrations/deploys.js
|
||||||
|
|
||||||
const anchor = require("@project-serum/anchor");
|
const anchor = require('@project-serum/anchor')
|
||||||
|
|
||||||
module.exports = async function (provider) {
|
module.exports = async function (provider) {
|
||||||
anchor.setProvider(provider);
|
anchor.setProvider(provider)
|
||||||
|
|
||||||
// Add your deploy script here.
|
// Add your deploy script here.
|
||||||
}
|
}
|
||||||
|
@ -195,15 +200,23 @@ and only support this simple deploy script at the moment.
|
||||||
|
|
||||||
## New
|
## New
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor new <program-name>
|
anchor new <program-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
Creates a new program in the workspace's `programs/` directory initialized with boilerplate.
|
Creates a new program in the workspace's `programs/` directory initialized with boilerplate.
|
||||||
|
|
||||||
|
## Shell
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Starts a node js shell with an Anchor client setup according to the local config. This client can be used to interact with deployed Solana programs in the workspace.
|
||||||
|
|
||||||
## Test
|
## Test
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor test
|
anchor test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -213,24 +226,21 @@ of all workspace programs before running them.
|
||||||
If the configured network is a localnet, then automatically starts the localnetwork and runs
|
If the configured network is a localnet, then automatically starts the localnetwork and runs
|
||||||
the test.
|
the test.
|
||||||
|
|
||||||
::: tip Note
|
{% callout title="Note" %}
|
||||||
Be sure to shutdown any other local validators, otherwise `anchor test` will fail to run.
|
Be sure to shutdown any other local validators, otherwise `anchor test` will fail to run.
|
||||||
|
|
||||||
If you'd prefer to run the program against your local validator use `anchor test --skip-local-validator`.
|
If you'd prefer to run the program against your local validator use `anchor test --skip-local-validator`.
|
||||||
:::
|
{% /callout %}
|
||||||
|
|
||||||
When running tests we stream program logs to `.anchor/program-logs/<address>.<program-name>.log`
|
When running tests we stream program logs to `.anchor/program-logs/<address>.<program-name>.log`
|
||||||
|
|
||||||
::: tip Note
|
{% callout title="Note" %}
|
||||||
The Anchor workflow [recommends](https://www.parity.io/paritys-checklist-for-secure-smart-contract-development/)
|
The Anchor workflow [recommends](https://www.parity.io/paritys-checklist-for-secure-smart-contract-development/) to test your program using integration tests in a language other than Rust to make sure that bugs related to syntax misunderstandings are coverable with tests and not just replicated in tests.
|
||||||
to test your program using integration tests in a language other
|
{% /callout %}
|
||||||
than Rust to make sure that bugs related to syntax misunderstandings
|
|
||||||
are coverable with tests and not just replicated in tests.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Upgrade
|
## Upgrade
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor upgrade <target/deploy/program.so> --program-id <program-id>
|
anchor upgrade <target/deploy/program.so> --program-id <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -238,7 +248,7 @@ Uses Solana's upgradeable BPF loader to upgrade the on chain program code.
|
||||||
|
|
||||||
## Verify
|
## Verify
|
||||||
|
|
||||||
```
|
```shell
|
||||||
anchor verify <program-id>
|
anchor verify <program-id>
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,447 @@
|
||||||
|
---
|
||||||
|
title: Cross-Program Invocations
|
||||||
|
description: Anchor - Cross-Program Invocations
|
||||||
|
---
|
||||||
|
|
||||||
|
Often it's useful for programs to interact with each other. In Solana this is achieved via Cross-Program Invocations (CPIs).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Consider the following example of a puppet and a puppet master. Admittedly, it is not very realistic but it allows us to show you the many nuances of CPIs. The milestone project of the intermediate section covers a more realistic program with multiple CPIs.
|
||||||
|
|
||||||
|
## Setting up basic CPI functionality
|
||||||
|
|
||||||
|
Create a new workspace
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor init puppet
|
||||||
|
```
|
||||||
|
|
||||||
|
and copy the following code.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod puppet {
|
||||||
|
use super::*;
|
||||||
|
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
|
||||||
|
let puppet = &mut ctx.accounts.puppet;
|
||||||
|
puppet.data = data;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize<'info> {
|
||||||
|
#[account(init, payer = user, space = 8 + 8)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
#[account(mut)]
|
||||||
|
pub user: Signer<'info>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetData<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
pub struct Data {
|
||||||
|
pub data: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There's nothing special happening here. It's a pretty simple program! The interesting part is how it interacts with the next program we are going to create.
|
||||||
|
|
||||||
|
Run
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor new puppet-master
|
||||||
|
```
|
||||||
|
|
||||||
|
inside the workspace and copy the following code:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use puppet::cpi::accounts::SetData;
|
||||||
|
use puppet::program::Puppet;
|
||||||
|
use puppet::{self, Data};
|
||||||
|
|
||||||
|
declare_id!("HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
mod puppet_master {
|
||||||
|
use super::*;
|
||||||
|
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
|
||||||
|
let cpi_program = ctx.accounts.puppet_program.to_account_info();
|
||||||
|
let cpi_accounts = SetData {
|
||||||
|
puppet: ctx.accounts.puppet.to_account_info(),
|
||||||
|
};
|
||||||
|
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||||
|
puppet::cpi::set_data(cpi_ctx, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct PullStrings<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
pub puppet_program: Program<'info, Puppet>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also add the line `puppet_master = "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"` in the `[programs.localnet]` section of your `Anchor.toml`. Finally, import the puppet program into the puppet-master program by adding the following line to the `[dependencies]` section of the `Cargo.toml` file inside the `puppet-master` program folder:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
puppet = { path = "../puppet", features = ["cpi"]}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `features = ["cpi"]` is used so we can not only use puppet's types but also its instruction builders and cpi functions. Without those, we would have to use low level solana syscalls. Fortunately, anchor provides abstractions on top of those. By enabling the `cpi` feature, the puppet-master program gets access to the `puppet::cpi` module. Anchor generates this module automatically and it contains tailor-made instructions builders and cpi helpers for the program.
|
||||||
|
|
||||||
|
In the case of the puppet program, the puppet-master uses the `SetData` instruction builder struct provided by the `puppet::cpi::accounts` module to submit the accounts the `SetData` instruction of the puppet program expects. Then, the puppet-master creates a new cpi context and passes it to the `puppet::cpi::set_data` cpi function. This function has the exact same function as the `set_data` function in the puppet program with the exception that it expects a `CpiContext` instead of a `Context`.
|
||||||
|
|
||||||
|
Setting up a CPI can distract from the business logic of the program so it's recommended to move the CPI setup into the `impl` block of the instruction. The puppet-master program then looks like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use puppet::cpi::accounts::SetData;
|
||||||
|
use puppet::program::Puppet;
|
||||||
|
use puppet::{self, Data};
|
||||||
|
|
||||||
|
declare_id!("HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
mod puppet_master {
|
||||||
|
use super::*;
|
||||||
|
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
|
||||||
|
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct PullStrings<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
pub puppet_program: Program<'info, Puppet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'info> PullStrings<'info> {
|
||||||
|
pub fn set_data_ctx(&self) -> CpiContext<'_, '_, '_, 'info, SetData<'info>> {
|
||||||
|
let cpi_program = self.puppet_program.to_account_info();
|
||||||
|
let cpi_accounts = SetData {
|
||||||
|
puppet: self.puppet.to_account_info()
|
||||||
|
};
|
||||||
|
CpiContext::new(cpi_program, cpi_accounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We can verify that everything works as expected by replacing the contents of the `puppet.ts` file with:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import * as anchor from '@project-serum/anchor'
|
||||||
|
import { Program } from '@project-serum/anchor'
|
||||||
|
import { Keypair } from '@solana/web3.js'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
import { Puppet } from '../target/types/puppet'
|
||||||
|
import { PuppetMaster } from '../target/types/puppet_master'
|
||||||
|
|
||||||
|
describe('puppet', () => {
|
||||||
|
const provider = anchor.AnchorProvider.env()
|
||||||
|
anchor.setProvider(provider)
|
||||||
|
|
||||||
|
const puppetProgram = anchor.workspace.Puppet as Program<Puppet>
|
||||||
|
const puppetMasterProgram = anchor.workspace
|
||||||
|
.PuppetMaster as Program<PuppetMaster>
|
||||||
|
|
||||||
|
const puppetKeypair = Keypair.generate()
|
||||||
|
|
||||||
|
it('Does CPI!', async () => {
|
||||||
|
await puppetProgram.methods
|
||||||
|
.initialize()
|
||||||
|
.accounts({
|
||||||
|
puppet: puppetKeypair.publicKey,
|
||||||
|
user: provider.wallet.publicKey,
|
||||||
|
})
|
||||||
|
.signers([puppetKeypair])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
await puppetMasterProgram.methods
|
||||||
|
.pullStrings(new anchor.BN(42))
|
||||||
|
.accounts({
|
||||||
|
puppetProgram: puppetProgram.programId,
|
||||||
|
puppet: puppetKeypair.publicKey,
|
||||||
|
})
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await puppetProgram.account.data.fetch(puppetKeypair.publicKey)
|
||||||
|
).data.toNumber()
|
||||||
|
).to.equal(42)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
and running `anchor test`.
|
||||||
|
|
||||||
|
## Privilege Extension
|
||||||
|
|
||||||
|
CPIs extend the privileges of the caller to the callee. The puppet account was passed as a mutable account to the puppet-master but it was still mutable in the puppet program as well (otherwise the `expect` in the test would've failed). The same applies to signatures.
|
||||||
|
|
||||||
|
If you want to prove this for yourself, add an `authority` field to the `Data` struct in the puppet program.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[account]
|
||||||
|
pub struct Data {
|
||||||
|
pub data: u64,
|
||||||
|
pub authority: Pubkey
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and adjust the `initialize` function:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn initialize(ctx: Context<Initialize>, authority: Pubkey) -> Result<()> {
|
||||||
|
ctx.accounts.puppet.authority = authority;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `32` to the `space` constraint of the `puppet` field for the `Pubkey` field in the `Data` struct.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize<'info> {
|
||||||
|
#[account(init, payer = user, space = 8 + 8 + 32)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
#[account(mut)]
|
||||||
|
pub user: Signer<'info>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, adjust the `SetData` validation struct:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetData<'info> {
|
||||||
|
#[account(mut, has_one = authority)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
pub authority: Signer<'info>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `has_one` constraint checks that `puppet.authority = authority.key()`.
|
||||||
|
|
||||||
|
The puppet-master program now also needs adjusting:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use puppet::cpi::accounts::SetData;
|
||||||
|
use puppet::program::Puppet;
|
||||||
|
use puppet::{self, Data};
|
||||||
|
|
||||||
|
declare_id!("HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
mod puppet_master {
|
||||||
|
use super::*;
|
||||||
|
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
|
||||||
|
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct PullStrings<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
pub puppet_program: Program<'info, Puppet>,
|
||||||
|
// Even though the puppet program already checks that authority is a signer
|
||||||
|
// using the Signer type here is still required because the anchor ts client
|
||||||
|
// can not infer signers from programs called via CPIs
|
||||||
|
pub authority: Signer<'info>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'info> PullStrings<'info> {
|
||||||
|
pub fn set_data_ctx(&self) -> CpiContext<'_, '_, '_, 'info, SetData<'info>> {
|
||||||
|
let cpi_program = self.puppet_program.to_account_info();
|
||||||
|
let cpi_accounts = SetData {
|
||||||
|
puppet: self.puppet.to_account_info(),
|
||||||
|
authority: self.authority.to_account_info()
|
||||||
|
};
|
||||||
|
CpiContext::new(cpi_program, cpi_accounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, change the test:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import * as anchor from '@project-serum/anchor'
|
||||||
|
import { Program } from '@project-serum/anchor'
|
||||||
|
import { Keypair } from '@solana/web3.js'
|
||||||
|
import { Puppet } from '../target/types/puppet'
|
||||||
|
import { PuppetMaster } from '../target/types/puppet_master'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
describe('puppet', () => {
|
||||||
|
const provider = anchor.AnchorProvider.env()
|
||||||
|
anchor.setProvider(provider)
|
||||||
|
|
||||||
|
const puppetProgram = anchor.workspace.Puppet as Program<Puppet>
|
||||||
|
const puppetMasterProgram = anchor.workspace
|
||||||
|
.PuppetMaster as Program<PuppetMaster>
|
||||||
|
|
||||||
|
const puppetKeypair = Keypair.generate()
|
||||||
|
const authorityKeypair = Keypair.generate()
|
||||||
|
|
||||||
|
it('Does CPI!', async () => {
|
||||||
|
await puppetProgram.methods
|
||||||
|
.initialize(authorityKeypair.publicKey)
|
||||||
|
.accounts({
|
||||||
|
puppet: puppetKeypair.publicKey,
|
||||||
|
user: provider.wallet.publicKey,
|
||||||
|
})
|
||||||
|
.signers([puppetKeypair])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
await puppetMasterProgram.methods
|
||||||
|
.pullStrings(new anchor.BN(42))
|
||||||
|
.accounts({
|
||||||
|
puppetProgram: puppetProgram.programId,
|
||||||
|
puppet: puppetKeypair.publicKey,
|
||||||
|
authority: authorityKeypair.publicKey,
|
||||||
|
})
|
||||||
|
.signers([authorityKeypair])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await puppetProgram.account.data.fetch(puppetKeypair.publicKey)
|
||||||
|
).data.toNumber()
|
||||||
|
).to.equal(42)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The test passes because the signature that was given to the puppet-master by the authority was then extended to the puppet program which used it to check that the authority for the puppet account had signed the transaction.
|
||||||
|
|
||||||
|
> Privilege extension is convenient but also dangerous. If a CPI is unintentionally made to a malicious program,
|
||||||
|
> this program has the same privileges as the caller.
|
||||||
|
> Anchor protects you from CPIs to malicious programs with two measures.
|
||||||
|
> First, the `Program<'info, T>` type checks that the given account is the expected program `T`.
|
||||||
|
> Should you ever forget to use the `Program` type, the automatically generated cpi function
|
||||||
|
> (in the previous example this was `puppet::cpi::set_data`)
|
||||||
|
> also checks that the `cpi_program` argument equals the expected program.
|
||||||
|
|
||||||
|
## Reloading an Account
|
||||||
|
|
||||||
|
In the puppet program, the `Account<'info, T>` type is used for the `puppet` account. If a CPI edits an account of that type,
|
||||||
|
the caller's account does not change during the instruction.
|
||||||
|
|
||||||
|
You can easily see this for yourself by adding the following right after the `puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)` cpi call.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)?;
|
||||||
|
if ctx.accounts.puppet.data != 42 {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
```
|
||||||
|
|
||||||
|
{% callout type="warning" title="Note" %}
|
||||||
|
Your test will fail. But why? After all the test used to pass, so the cpi definitely did change the `data` field to `42`.
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
The reason the `data` field has not been updated to `42` in the caller is that at the beginning of the instruction the `Account<'info, T>` type deserializes the incoming bytes into a new struct. This struct is no longer connected to the underlying data in the account. The CPI changes the data in the underlying account but since the struct in the caller has no connection to the underlying account the struct in the caller remains unchanged.
|
||||||
|
|
||||||
|
If you need to read the value of an account that has just been changed by a CPI, you can call its `reload` method which will re-deserialize the account. If you put `ctx.accounts.puppet.reload()?;` right after the cpi call, the test will pass again.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
puppet::cpi::set_data(ctx.accounts.set_data_ctx(), data)?;
|
||||||
|
ctx.accounts.puppet.reload()?;
|
||||||
|
if ctx.accounts.puppet.data != 42 {
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Returning values from handler functions
|
||||||
|
|
||||||
|
The Anchor handler functions are capable of returning data using the Solana `set_return_data` and `get_return_data` syscalls. This data can be used in CPI callers and clients.
|
||||||
|
|
||||||
|
Instead of returning a `Result<()>`, consider this version of the `set_data` function from above which has been modified to return `Result<u64>`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<u64> {
|
||||||
|
let puppet = &mut ctx.accounts.puppet;
|
||||||
|
puppet.data = data;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Defining a return type that isn't the unit type `()` will cause Anchor to transparently call `set_return_data` with the given type (`u64` in this example) when this function is called. The return from the CPI call is wrapped in a struct to allow for lazy retrieval of this return data. E.g.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn pull_strings(ctx: Context<PullStrings>, data: u64) -> Result<()> {
|
||||||
|
let cpi_program = ctx.accounts.puppet_program.to_account_info();
|
||||||
|
let cpi_accounts = SetData {
|
||||||
|
puppet: ctx.accounts.puppet.to_account_info(),
|
||||||
|
};
|
||||||
|
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||||
|
let result = puppet::cpi::set_data(cpi_ctx, data)?;
|
||||||
|
// The below statement calls sol_get_return and deserializes the result.
|
||||||
|
// `return_data` contains the return from `set_data`,
|
||||||
|
// which in this example is just `data`.
|
||||||
|
let return_data = result.get();
|
||||||
|
// ... do something with the `return_data` ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
{% callout type="warning" title="Note" %}
|
||||||
|
The type being returned must implement the `AnchorSerialize` and `AnchorDeserialize` traits, for example:
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||||
|
pub struct StructReturn {
|
||||||
|
pub value: u64,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reading return data in the clients
|
||||||
|
|
||||||
|
It's even possible to use return values without CPIs. This may be useful if you're using a function to calculate a value that you need on the frontend without rewriting the code in the frontend.
|
||||||
|
|
||||||
|
Whether you're using a CPI or not, you can use the `view` function to read whatever was set last as return data in the transaction (`view` simulates the transaction and reads the `Program return` log).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const returnData = await program.methods
|
||||||
|
.calculate(someVariable)
|
||||||
|
.accounts({
|
||||||
|
acc: somePubkey,
|
||||||
|
anotherAcc: someOtherPubkey,
|
||||||
|
})
|
||||||
|
.view()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Return Data Size Limit Workarounds
|
||||||
|
|
||||||
|
The `set_return_data` and `get_return_data` syscalls are limited to 1024 bytes so it's worth briefly explaining the old workaround for CPI return values.
|
||||||
|
|
||||||
|
By using a CPI together with `reload` it's possible to simulate return values. One could imagine that instead of just setting the `data` field to `42` the puppet program did some calculation with the `42` and saved the result in `data`. The puppet-master can then call `reload` after the cpi and use the result of the puppet program's calculation.
|
||||||
|
|
||||||
|
## Programs as Signers
|
||||||
|
|
||||||
|
There's one more thing that can be done with CPIs. But for that, you need to first learn what PDAs are. We'll cover those in the next chapter.
|
|
@ -0,0 +1,83 @@
|
||||||
|
---
|
||||||
|
title: Errors
|
||||||
|
description: Anchor - Errors
|
||||||
|
---
|
||||||
|
|
||||||
|
> [`AnchorError` Rust Reference](https://docs.rs/anchor-lang/latest/anchor_lang/error/struct.AnchorError.html)
|
||||||
|
|
||||||
|
> [`AnchorError` Typescript Reference](https://project-serum.github.io/anchor/ts/classes/AnchorError.html)
|
||||||
|
|
||||||
|
There are two types of errors in anchor programs. AnchorErrors and non-anchor errors.
|
||||||
|
AnchorErrors can be divided into Anchor Internal Errors that the framework returns from inside its own code or
|
||||||
|
custom errors which the user (you!) can return.
|
||||||
|
|
||||||
|
- AnchorErrors
|
||||||
|
- Anchor Internal Errors
|
||||||
|
- Custom Errors
|
||||||
|
- Non-anchor errors.
|
||||||
|
|
||||||
|
[AnchorErrors](https://docs.rs/anchor-lang/latest/anchor_lang/error/struct.AnchorError.html) provide a range of information like the error name and number or the location in the code where the anchor was thrown, or the account that violated a constraint (e.g. a `mut` constraint). Once thrown inside the program, [you can access the error information](https://project-serum.github.io/anchor/ts/classes/AnchorError.html) in the anchor clients like the typescript client. The typescript client also enriches the error with additional information about which program the error was thrown in and the CPI calls (which are explained [here](./cross-program-invocations) in the book) that led to the program from which the error was thrown from. [The milestone chapter](./milestone_project_tic-tac-toe.md) explores how all of this works together in practice. For now, let's look at how different errors can be returned from inside a program.
|
||||||
|
|
||||||
|
## Anchor Internal Errors
|
||||||
|
|
||||||
|
> [Anchor Internal Error Code Reference](https://docs.rs/anchor-lang/latest/anchor_lang/error/enum.ErrorCode.html)
|
||||||
|
|
||||||
|
Anchor has many different internal error codes. These are not meant to be used by users, but it's useful to study the reference to learn about the mappings between codes and their causes. They are, for example, thrown when a constraint has been violated, e.g. when an account is marked with `mut` but its `is_writable` property is `false`.
|
||||||
|
|
||||||
|
## Custom Errors
|
||||||
|
|
||||||
|
You can add errors that are unique to your program by using the `error_code` attribute.
|
||||||
|
|
||||||
|
Simply add it to an enum with a name of your choice. You can then use the variants of the enum as errors in your program. Additionally, you can add a message attribute to the individual variants. Clients will then display this error message if the error occurs. Custom Error code numbers start at the [custom error offset](https://docs.rs/anchor-lang/latest/anchor_lang/error/constant.ERROR_CODE_OFFSET.html).
|
||||||
|
|
||||||
|
To actually throw an error use the [`err!`](https://docs.rs/anchor-lang/latest/anchor_lang/macro.err.html) or the [`error!`](https://docs.rs/anchor-lang/latest/anchor_lang/prelude/macro.error.html) macro. These add file and line information to the error that is then logged by anchor.
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
|
||||||
|
if data.data >= 100 {
|
||||||
|
return err!(MyError::DataTooLarge);
|
||||||
|
}
|
||||||
|
ctx.accounts.my_account.set_inner(data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[error_code]
|
||||||
|
pub enum MyError {
|
||||||
|
#[msg("MyAccount may only hold data below 100")]
|
||||||
|
DataTooLarge
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### require!
|
||||||
|
|
||||||
|
You can use the [`require`](https://docs.rs/anchor-lang/latest/anchor_lang/macro.require.html) macro to simplify writing errors. The code above can be simplified to this (Note that the `>=` flips to `<`):
|
||||||
|
|
||||||
|
```rust,ignore
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
|
||||||
|
require!(data.data < 100, MyError::DataTooLarge);
|
||||||
|
ctx.accounts.my_account.set_inner(data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[error_code]
|
||||||
|
pub enum MyError {
|
||||||
|
#[msg("MyAccount may only hold data below 100")]
|
||||||
|
DataTooLarge
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are a couple of `require` macros to choose from ([search for require in the docs](https://docs.rs/anchor-lang/latest/anchor_lang/?search=require)). When comparing public keys, it's important to use the `keys` variants of the require statements like `require_keys_eq` instead of `require_eq` because comparing public keys with `require_eq` is very expensive.
|
||||||
|
|
||||||
|
> (Ultimately, all programs return the same Error: The [`ProgramError`](https://docs.rs/solana-program/latest/solana_program/program_error/enum.ProgramError.html).
|
||||||
|
|
||||||
|
This Error has a field for a custom error number. This is where Anchor puts its internal and custom error codes. However, this is just a single number and a single number is only so useful. So in addition, in the case of AnchorErrors, Anchor logs the returned AnchorError and the Anchor clients parse these logs to provide as much information as possible. This is not always possible. For example, there is currently no easy way to get the logs of a `processed` transaction with preflight checks turned off. In addition, non-anchor or old anchor programs might not log AnchorErrors. In these cases, Anchor will fall back to checking whether the returned error number by the transaction matches an error number defined in the `IDL` or an Anchor internal error code. If so, Anchor will at least enrich the error with the error message. Also, if there are logs available, Anchor will always try to parse the program error stack and return that so you know which program the error was returned from.
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
title: Hello World
|
||||||
|
description: Helle World
|
||||||
|
---
|
||||||
|
|
||||||
|
To initialize a new project, simply run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor init <new-workspace-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
This creates a new anchor workspace you can move into. The following are some of the important files in the folder:
|
||||||
|
|
||||||
|
- The `.anchor` folder: It includes the most recent program logs and a local ledger that is used for testing
|
||||||
|
- The `app` folder: An empty folder that you can use to hold your frontend if you use a monorepo
|
||||||
|
- The `programs` folder: This folder contains your programs. It can contain multiple but initially only contains a program with the same name as `<new-workspace-name>`. This program already contains a `lib.rs` file with some sample code.
|
||||||
|
- The `tests` folder: The folder that contains your E2E tests. It will already include a file that tests the sample code in the `programs/<new-workspace-name>`.
|
||||||
|
- The `migrations` folder: In this folder you can save your deploy and migration scripts for your programs.
|
||||||
|
- The `Anchor.toml` file: This file configures workspace wide settings for your programs. Initially, it configures
|
||||||
|
- The addresses of your programs on localnet (`[programs.localnet]`)
|
||||||
|
- A registry your program can be pushed to (`[registry]`)
|
||||||
|
- A provider which can be used in your tests (`[provider]`)
|
||||||
|
- Scripts that Anchor executes for you (`[scripts]`). The `test` script is run when running `anchor test`. You can run your own scripts with `anchor run <script_name>`.
|
|
@ -0,0 +1,31 @@
|
||||||
|
---
|
||||||
|
title: High-level Overview
|
||||||
|
description: Anchor High-level Overview
|
||||||
|
---
|
||||||
|
|
||||||
|
An Anchor program consists of three parts. The `program` module, the Accounts structs which are marked with `#[derive(Accounts)]`, and the `declare_id` macro. The `program` module is where you write your business logic. The Accounts structs is where you validate accounts. The`declare_id` macro creates an `ID` field that stores the address of your program. Anchor uses this hardcoded `ID` for security checks and it also allows other crates to access your program's address.
|
||||||
|
|
||||||
|
When you start up a new Anchor project, you'll see the following:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// use this import to gain access to common anchor features
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
// declare an id for your program
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
// write your business logic here
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn initialize(_ctx: Context<Initialize>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate incoming accounts here
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize {}
|
||||||
|
```
|
||||||
|
|
||||||
|
We'll go into more detail in the next sections but for now, note that the way an endpoint is connected to its corresponding Accounts struct is the `ctx` argument in the endpoint. The argument is of type `Context` which is generic over an Accounts struct, i.e. this is where you put the name of your account validation struct. In this example, it's `Initialize`.
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
title: Important Links
|
||||||
|
description: Anchor - Important Links
|
||||||
|
---
|
||||||
|
|
||||||
|
- [Accounts Reference](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/index.html)
|
||||||
|
- [Constraints Reference](https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html)
|
||||||
|
- [Error Codes](https://docs.rs/anchor-lang/latest/anchor_lang/error/enum.ErrorCode.html)
|
|
@ -0,0 +1,79 @@
|
||||||
|
---
|
||||||
|
title: Installation
|
||||||
|
description: Quidem magni aut exercitationem maxime rerum eos.
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rust
|
||||||
|
|
||||||
|
Go [here](https://www.rust-lang.org/tools/install) to install Rust.
|
||||||
|
|
||||||
|
{% callout title="You should know!" %}
|
||||||
|
We recommend reading chapters 1-9 of the [Rust book](https://doc.rust-lang.org/book/title-page.html) which cover the basics of using Rust (Most of the time you don't need advanced Rust to write anchor programs).
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
## Solana
|
||||||
|
|
||||||
|
Go [here](https://docs.solana.com/cli/install-solana-cli-tools) to install Solana and then run `solana-keygen new` to create a keypair at the default location. Anchor uses this keypair to run your program tests.
|
||||||
|
|
||||||
|
{% callout title="You should know!" %}
|
||||||
|
We also recommend checking out the the official [Solana developers page](https://solana.com/developers).
|
||||||
|
{% /callout %}
|
||||||
|
|
||||||
|
## Yarn
|
||||||
|
|
||||||
|
Go [here](https://yarnpkg.com/getting-started/install) to install Yarn.
|
||||||
|
|
||||||
|
## Anchor
|
||||||
|
|
||||||
|
### Installing using Anchor version manager (avm) (recommended)
|
||||||
|
|
||||||
|
Anchor version manager is a tool for using multiple versions of the anchor-cli. It will require the same dependencies as building from source. It is recommended you uninstall the NPM package if you have it installed.
|
||||||
|
|
||||||
|
Install `avm` using Cargo. Note this will replace your `anchor` binary if you had one installed.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install --git https://github.com/project-serum/anchor avm --locked --force
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux systems you may need to install additional dependencies if cargo install fails. E.g. on Ubuntu:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the latest version of the CLI using `avm`, and then set it to be the version to use.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
avm install latest
|
||||||
|
avm use latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the installation.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor --version
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install using pre-build binary on x86_64 Linux
|
||||||
|
|
||||||
|
Anchor binaries are available via an NPM package [`@project-serum/anchor-cli`](https://www.npmjs.com/package/@project-serum/anchor-cli). Only `x86_64` Linux is supported currently, you must build from source for other OS'.
|
||||||
|
|
||||||
|
### Build from source for other operating systems without avm
|
||||||
|
|
||||||
|
We can also use Cargo to install the CLI directly. Make sure that the `--tag` argument uses the version you want (the version here is just an example).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cargo install --git https://github.com/project-serum/anchor --tag v0.24.1 anchor-cli --locked
|
||||||
|
```
|
||||||
|
|
||||||
|
On Linux systems you may need to install additional dependencies if cargo install fails. On Ubuntu,
|
||||||
|
|
||||||
|
```shell
|
||||||
|
sudo apt-get update && sudo apt-get upgrade && sudo apt-get install -y pkg-config build-essential libudev-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Now verify the CLI is installed properly.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor --version
|
||||||
|
```
|
|
@ -0,0 +1,197 @@
|
||||||
|
---
|
||||||
|
title: Intro to Programming on Solana
|
||||||
|
description: Quidem magni aut exercitationem maxime rerum eos.
|
||||||
|
---
|
||||||
|
|
||||||
|
This is a brief intro to programming on Solana that explains the most important topics.
|
||||||
|
It aims to provide everything you need to understand the following chapters in the book.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Memory on Solana
|
||||||
|
|
||||||
|
On a high level, memory inside a Solana cluster can be thought of as a monolithic heap of data. Smart contracts on Solana ("programs" in Solana jargon) each have access to their own part of that heap.
|
||||||
|
|
||||||
|
While a program may read any part of the global heap, if a program tries to write to a part of the heap that is not theirs, the Solana runtime makes the transaction fail (there is one exception to this which is increasing the balance of an account).
|
||||||
|
|
||||||
|
All state lives in this heap. Your SOL accounts, smart contracts, and memory used by smart contracts. And each memory region has a program that manages it (sometimes called the “owner”). The solana term for a memory region is "account". Some programs own thousands of independent accounts. As shown in the figure, these accounts (even when owned by the same program) do not have to be equal in size.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Since all state lives in the heap, even programs themselves live there. Accounts that store programs are owned by the `BPFLoader`. This is a program that can be used to deploy and upgrade other programs. The `BPFLoader` is owned by the `Native Loader` and that is where the recursion ends.
|
||||||
|
|
||||||
|
## Transactions and Accounts
|
||||||
|
|
||||||
|
You can make a program read and write data by sending transactions. Programs provide endpoints that can be called via transactions (In reality it's a bit more complex than that but frameworks like Anchor abstract away this complexity). A function signature usually takes the following arguments:
|
||||||
|
|
||||||
|
- the accounts that the program may read from and write to during this transaction.
|
||||||
|
- additional data specific to the function
|
||||||
|
|
||||||
|
The first point means that even if in theory the program may read and write to a large part of the global heap, in the context of a transaction, it may only read from and write to the specific regions specified in the arguments of the transaction.
|
||||||
|
|
||||||
|
> This design is partly responsible for Solana’s high throughput. The runtime can look at all the incoming transactions of a program (and even across programs) and can check whether the memory regions in the first argument of the transactions overlap. If they don’t, the runtime can run these transactions in parallel because they don’t conflict with each other. Even better, if the runtime sees that two transactions access overlapping memory regions but only read and don’t write, it can also parallelize those transactions because they do not conflict with each other.
|
||||||
|
|
||||||
|
How exactly can a transaction specify a memory region/account? To answer that, we need to look deeper into what properties an account has ([docs here](https://docs.rs/solana-program/latest/solana_program/account_info/struct.AccountInfo.html). This is the data structure for an account in a transaction. The `is_signer` and `is_writable` fields are set per transaction (e.g. `is_signed` is set if the corresponding private key of the account's `key` field signed the transaction) and are not part of the metadata that is saved in the heap). In front of the user data that the account can store (in the `data` field) , there is some metadata connected to each account. First, it has a key property which is a ed25519 public key and serves as the address of the account. This is how the transaction can specify which accounts the program may access in the transaction.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
An account also has a lamports field (a lamport is SOL’s smallest unit). Since all state lives in the heap, normal SOL accounts are on the heap too. They're accounts with a `data` field of length 0 (they still have metadata though!) and some amount of lamports. The System Program owns all regular SOL accounts.
|
||||||
|
|
||||||
|
## Rent
|
||||||
|
|
||||||
|
Because validators don’t have infinite storage and providing storage costs money, accounts need to pay rent for their existence. This rent is subtracted from their lamports regularly. However, if an account's lamports balance is above the rent-exemption threshold, it is rent-exempt and does not lose its lamports. This threshold depends on the size of the account. In 99% of cases, you will create rent-exempt accounts. It's even being considered to disable non-rent-exempt accounts.
|
||||||
|
|
||||||
|
## Program Example: The System Program
|
||||||
|
|
||||||
|
Let’s now look at an example of a program: The System Program. The System Program is a smart contract with some additional privileges.
|
||||||
|
|
||||||
|
All "normal" SOL accounts are owned by the System Program. One of the system program’s responsibilities is handling transfers between the accounts it owns. This is worth repeating: Even normal SOL transfers on Solana are handled by a smart contract.
|
||||||
|
|
||||||
|
To provide transfer functionality, the system program has a “transfer” endpoint. This endpoint takes 2 accounts - from and to - and a “lamports” argument. The system program checks whether `from` signed the transaction via the `is_signer` field on the `from` account. The runtime will set this flag to `true` if the private key of the keypair that the account’s public key belongs to signed the transaction. If “from” signed the transaction, the system program removes lamports from `from`’s account and adds them to `to`’s account.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// simplified system program code
|
||||||
|
|
||||||
|
fn transfer(accounts, lamports) {
|
||||||
|
if !accounts.from.is_signer {
|
||||||
|
error();
|
||||||
|
}
|
||||||
|
accounts.from.lamports -= lamports;
|
||||||
|
accounts.to.lamports += lamports;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Take a moment to guess would happen if the user passed in a `from` account that was not owned by the system program!
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
The transaction would fail! A program may not write to any accounts that it doesn't own. There's one exception to this rule though.
|
||||||
|
If the `to` account was owned by a different program, the transaction would still succeed. This is because programs may increase the lamports of an account even if they do not own it.
|
||||||
|
|
||||||
|
Next to transferring lamports, the system program is used to create accounts for other programs. An account is created with a specific size and a specific amount of lamports. Let's now look at program composition to see how creating accounts works in practice.
|
||||||
|
|
||||||
|
## Program Composition
|
||||||
|
|
||||||
|
There are two ways for developers to make programs interact with each other. To explain these, we'll use a common flow on Solana: Create & Initialize.
|
||||||
|
|
||||||
|
Consider a counter program with two endpoints. One to initialize the counter and one to increment it. To create a new counter, we call the system program's `create_account` to create the account in memory and then the counter's `initialize` function.
|
||||||
|
|
||||||
|
### Program Composition via multiple instructions in a transaction
|
||||||
|
|
||||||
|
The first way to create and initialize the counter is by using multiple instructions in a transaction.
|
||||||
|
While a transaction can be used to execute a single call to a program like it was done above with `transfer`,
|
||||||
|
a single transaction can also include multiple calls to different programs.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If we went with this approach, our counter data structure would look like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct Counter {
|
||||||
|
pub count: u64,
|
||||||
|
pub is_initialized: bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and our `initialize` function would look like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// pseudo code
|
||||||
|
fn initialize(accounts) {
|
||||||
|
let counter = deserialize(accounts.counter);
|
||||||
|
if counter.is_initialized {
|
||||||
|
error("already initialized");
|
||||||
|
}
|
||||||
|
counter.count = 0;
|
||||||
|
counter.is_initialized = true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach could also be called the "implicit" approach. This is because the programs do not explicitly communicate with each other. They are glued together by the user on the client side.
|
||||||
|
|
||||||
|
This also means that the counter needs to have an `is_initialized` variable so `initialize` can only be called once per counter account.
|
||||||
|
|
||||||
|
### Program Composition via Cross-Program Invocations
|
||||||
|
|
||||||
|
Cross-Program Invocations (CPIs) are the explicit tool to compose programs. A CPI is a direct call from one program into another within the same instruction.
|
||||||
|
|
||||||
|
Using CPIs the create & initialize flow can be executed inside the `initialize` function of the counter:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// pseudo code
|
||||||
|
fn initialize(accounts) {
|
||||||
|
accounts.system_program.create_account(accounts.payer, accounts.counter);
|
||||||
|
let counter = deserialize(accounts.counter);
|
||||||
|
counter.count = 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, no `is_initialized` is needed. This is because the CPI to the system program will fail if the counter exists already.
|
||||||
|
|
||||||
|
Anchor recommends CPIs to create and initialize accounts when possible (Accounts that are created by CPI can only be created with a maximum size of `10` kibibytes. This is large enough for most use cases though.). This is because creating an account inside your own instruction means that you can be certain about its properties. Any account that you don't create yourself is passed in by some other program or user that cannot be trusted. This brings us to the next section.
|
||||||
|
|
||||||
|
### Validating Inputs
|
||||||
|
|
||||||
|
On Solana it is crucial to validate program inputs. Clients pass accounts and program inputs to programs which means that malicious clients can pass malicious accounts and inputs. Programs need to be written in a way that handles those malicious inputs.
|
||||||
|
|
||||||
|
Consider the transfer function in the system program for example. It checks that `from` has signed the transaction.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// simplified system program code
|
||||||
|
|
||||||
|
fn transfer(accounts, lamports) {
|
||||||
|
if !accounts.from.is_signer {
|
||||||
|
error();
|
||||||
|
}
|
||||||
|
accounts.from.lamports -= lamports;
|
||||||
|
accounts.to.lamports += lamports;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If it didn't do that, anyone could call the endpoint with your account and make the system program transfer the lamports from your account into theirs.
|
||||||
|
|
||||||
|
The book will eventually have a chapter explaining all the different types of attacks and how anchor prevents them but for now here's one more example. Consider the counter program from earlier. Now imagine that next to the counter struct, there's another struct that is a singleton which is used to count how many counters there are.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
struct CounterCounter {
|
||||||
|
count: u64
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every time a new counter is created, the `count` variable of the counter counter should be incremented by one.
|
||||||
|
|
||||||
|
Consider the following `increment` instruction that increases the value of a counter account:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// pseudo code
|
||||||
|
fn increment(accounts) {
|
||||||
|
let counter = deserialize(accounts.counter);
|
||||||
|
counter.count += 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This function is insecure. But why? It's not possible to pass in an account owned by a different program because the function writes to the account so the runtime would make the transaction fail. But it is possible to pass in the counter counter singleton account because both the counter and the counter counter struct have the same structure (they're a rust struct with a single `u64` variable). This would then increase the counter counter's count and it would no longer track how many counters there are.
|
||||||
|
|
||||||
|
The fix is simple:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// pseudo code
|
||||||
|
|
||||||
|
// a better approach than hardcoding the address is using a PDA.
|
||||||
|
// We will cover those later in the book.
|
||||||
|
let HARDCODED_COUNTER_COUNTER_ADDRESS = SOME_ADDRESS;
|
||||||
|
|
||||||
|
fn increment(accounts) {
|
||||||
|
if accounts.counter.key == HARDCODED_COUNTER_COUNTER_ADDRESS {
|
||||||
|
error("Wrong account type");
|
||||||
|
}
|
||||||
|
let counter = deserialize(accounts.counter);
|
||||||
|
counter.count += 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There are many types of attacks possible on Solana that all revolve around passing in one account where another was expected but it wasn't checked that the actual one is really the expected one. This brings us from Solana to Anchor. A big part of Anchor's raison d'être is making input validation easier or even doing it for you when possible (e.g. with idiomatic anchor, this account type confusion cannot happen thanks to anchor's discriminator which we'll cover later in the book).
|
||||||
|
|
||||||
|
Let's dive in.
|
|
@ -0,0 +1,112 @@
|
||||||
|
---
|
||||||
|
title: Javascript Anchor Types Reference
|
||||||
|
description: Anchor - Javascript Anchor Types Reference
|
||||||
|
---
|
||||||
|
|
||||||
|
This reference shows you how anchor maps rust types to javascript/typescript types in the client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{% table %}
|
||||||
|
* Rust Type
|
||||||
|
* Javascript Type
|
||||||
|
* Example
|
||||||
|
* Note
|
||||||
|
---
|
||||||
|
* `bool`
|
||||||
|
* `bool`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init(true)
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `u64/u128/i64/i128`
|
||||||
|
* `anchor.BN`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init(new anchor.BN(99))
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
* [https://github.com/indutny/bn.js/](https://github.com/indutny/bn.js/ )
|
||||||
|
---
|
||||||
|
* `u8/u16/u32/i8/i16/i32`
|
||||||
|
* `number`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init(99)
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `f32/f64`
|
||||||
|
* `number`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init(1.0)
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `Enum`
|
||||||
|
* `{ variantName: {} }`
|
||||||
|
* ```rust
|
||||||
|
enum MyEnum { One, Two };
|
||||||
|
```
|
||||||
|
```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init({ one: {} })
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
```rust
|
||||||
|
enum MyEnum { One: { val: u64 }, Two };
|
||||||
|
```
|
||||||
|
```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init({ one: { val: 99 } })
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `Struct`
|
||||||
|
* `{ val: {} }`
|
||||||
|
* ```rust
|
||||||
|
struct MyStruct { val: u64 };
|
||||||
|
```
|
||||||
|
```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init({ val: 99 })
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `[T; N]`
|
||||||
|
* `[ T ]`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init([1,2,3])
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `String`
|
||||||
|
* `string`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init("hello")
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
---
|
||||||
|
* `Vec<T>`
|
||||||
|
* `[ T ]`
|
||||||
|
* ```javascript
|
||||||
|
await program
|
||||||
|
.methods
|
||||||
|
.init([1,2,3])
|
||||||
|
.rpc();
|
||||||
|
```
|
||||||
|
{% /table %}
|
|
@ -0,0 +1,136 @@
|
||||||
|
---
|
||||||
|
title: Anchor.toml Reference
|
||||||
|
description: Anchor - Anchor.toml Reference
|
||||||
|
---
|
||||||
|
|
||||||
|
## provider (required)
|
||||||
|
|
||||||
|
A wallet and cluster that are used for all commands.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[provider]
|
||||||
|
cluster = "localnet" # The cluster used for all commands.
|
||||||
|
wallet = "~/.config/solana/id.json" # The keypair used for all commands.
|
||||||
|
```
|
||||||
|
|
||||||
|
## scripts (required for testing)
|
||||||
|
|
||||||
|
Scripts that can be run with `anchor run <script>`. The `test` script is executed by `anchor test`.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[scripts]
|
||||||
|
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
## registry
|
||||||
|
|
||||||
|
The registry that is used in commands related to verifiable builds (e.g. when pushing a verifiable build with `anchor publish`).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
[registry]
|
||||||
|
url = "https://anchor.projectserum.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
## programs
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[programs.localnet]
|
||||||
|
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
|
||||||
|
```
|
||||||
|
|
||||||
|
The addresses of the programs in the workspace.
|
||||||
|
|
||||||
|
`programs.localnet` is used during testing on localnet where it's possible to load a program at genesis with the `--bpf-program` option on `solana-test-validator`.
|
||||||
|
|
||||||
|
## test
|
||||||
|
|
||||||
|
#### startup_wait
|
||||||
|
|
||||||
|
Increases the time anchor waits for the `solana-test-validator` to start up. This is, for example, useful if you're cloning (see `test.validator.clone`) many accounts which increases the validator's startup time.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[test]
|
||||||
|
startup_wait = 10000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### genesis
|
||||||
|
|
||||||
|
Makes commands like `anchor test` start `solana-test-validator` with a given program already loaded.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[test.genesis]]
|
||||||
|
address = "9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
|
||||||
|
program = "dex.so"
|
||||||
|
|
||||||
|
[[test.genesis]]
|
||||||
|
address = "22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD"
|
||||||
|
program = "swap.so"
|
||||||
|
```
|
||||||
|
|
||||||
|
## test.validator
|
||||||
|
|
||||||
|
These options are passed into the options with the same name in the `solana-test-validator` cli (see `solana-test-validator --help`) in commands like `anchor test`.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[test.validator]
|
||||||
|
url = "https://api.mainnet-beta.solana.com" # This is the url of the cluster that accounts are cloned from (See `test.validator.clone`).
|
||||||
|
warp_slot = 1337 # Warp the ledger to `warp_slot` after starting the validator.
|
||||||
|
slots_per_epoch = 5 # Override the number of slots in an epoch.
|
||||||
|
rpc_port = 1337 # Set JSON RPC on this port, and the next port for the RPC websocket.
|
||||||
|
limit_ledger_size = 1337 # Keep this amount of shreds in root slots.
|
||||||
|
ledger = "test-ledger" # Set ledger location.
|
||||||
|
gossip_port = 1337 # Gossip port number for the validator.
|
||||||
|
gossip_host = "127.0.0.1" # Gossip DNS name or IP address for the validator to advertise in gossip.
|
||||||
|
faucet_sol = 1337 # Give the faucet address this much SOL in genesis.
|
||||||
|
faucet_port = 1337 # Enable the faucet on this port.
|
||||||
|
dynamic_port_range = "1337 - 13337" # Range to use for dynamically assigned ports.
|
||||||
|
bind_address = "0.0.0.0" # IP address to bind the validator ports.
|
||||||
|
```
|
||||||
|
|
||||||
|
#### test.validator.clone
|
||||||
|
|
||||||
|
Use this to clone an account from the `test.validator.clone.url` cluster to the cluster of your test.
|
||||||
|
If `address` points to a program owned by the "BPF upgradeable loader", anchor (`>= 0.23.0`) will clone the
|
||||||
|
program data account of the program for you automatically.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[test.validator]
|
||||||
|
url = "https://api.mainnet-beta.solana.com"
|
||||||
|
|
||||||
|
[[test.validator.clone]]
|
||||||
|
address = "7NL2qWArf2BbEBBH1vTRZCsoNqFATTddH6h8GkVvrLpG"
|
||||||
|
[[test.validator.clone]]
|
||||||
|
address = "2RaN5auQwMdg5efgCaVqpETBV8sacWGR8tkK4m9kjo5r"
|
||||||
|
[[test.validator.clone]]
|
||||||
|
address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" # implicitly also clones PwDiXFxQsGra4sFFTT8r1QWRMd4vfumiWC1jfWNfdYT
|
||||||
|
```
|
||||||
|
|
||||||
|
#### test.validator.account
|
||||||
|
|
||||||
|
Use this to upload an account from a `.json` file.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[test.validator.account]]
|
||||||
|
address = "Ev8WSPQsGb4wfjybqff5eZNcS3n6HaMsBkMk9suAiuM"
|
||||||
|
filename = "some_account.json"
|
||||||
|
|
||||||
|
[[test.validator.account]]
|
||||||
|
address = "Ev8WSPQsGb4wfjybqff5eZNcS3n6HaMsBkMk9suAiuM"
|
||||||
|
filename = "some_other_account.json"
|
||||||
|
```
|
|
@ -0,0 +1,377 @@
|
||||||
|
---
|
||||||
|
title: Program Derived Addresses
|
||||||
|
description: Anchor - Program Derived Addresses
|
||||||
|
---
|
||||||
|
|
||||||
|
Knowing how to use PDAs is one of the most important skills for Solana Programming.
|
||||||
|
They simplify the programming model and make programs more secure. So what are they?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
PDAs (program derived addresses) are addresses with special properties.
|
||||||
|
|
||||||
|
Unlike normal addresses, PDAs are not public keys and therefore do not have an associated private key. There are two use cases for PDAs. They provide a mechanism to build hashmap-like structures on-chain and they allow programs to sign instructions.
|
||||||
|
|
||||||
|
## Creation of a PDA
|
||||||
|
|
||||||
|
Before we dive into how to use PDAs in anchor, here's a short explainer on what PDAs are.
|
||||||
|
|
||||||
|
PDAs are created by hashing a number of seeds the user can choose and the id of a program:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// pseudo code
|
||||||
|
let pda = hash(seeds, program_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
The seeds can be anything. A pubkey, a string, an array of numbers etc.
|
||||||
|
|
||||||
|
There's a 50% chance that this hash function results in a public key (but PDAs are not public keys), so a bump has to be searched for so that we get a PDA:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// pseudo code
|
||||||
|
fn find_pda(seeds, program_id) {
|
||||||
|
for bump in 0..256 {
|
||||||
|
let potential_pda = hash(seeds, bump, program_id);
|
||||||
|
if is_pubkey(potential_pda) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return (potential_pda, bump);
|
||||||
|
}
|
||||||
|
panic!("Could not find pda after 256 tries.");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It is technically possible that no bump is found within 256 tries but this probability is negligible.
|
||||||
|
If you're interested in the exact calculation of a PDA, check out the [`solana_program` source code](https://docs.rs/solana-program/latest/solana_program/pubkey/struct.Pubkey.html#method.find_program_address).
|
||||||
|
|
||||||
|
The first bump that results in a PDA is commonly called the "canonical bump". Other bumps may also result in a PDA but it's recommended to only use the canonical bump to avoid confusion.
|
||||||
|
|
||||||
|
## Using PDAs
|
||||||
|
|
||||||
|
We are now going to show you what you can do with PDAs and how to do it in Anchor!
|
||||||
|
|
||||||
|
### Hashmap-like structures using PDAs
|
||||||
|
|
||||||
|
Before we dive into the specifics of creating hashmaps in anchor, let's look at how to create a hashmap with PDAs in general.
|
||||||
|
|
||||||
|
#### Building hashmaps with PDAs
|
||||||
|
|
||||||
|
PDAs are hashed from the bump, a program id, but also a number of seeds which can be freely chosen by the user.
|
||||||
|
These seeds can be used to build hashmap-like structures on-chain.
|
||||||
|
|
||||||
|
For instance, imagine you're building an in-browser game and want to store some user stats. Maybe their level and their in-game name. You could create an account with a layout that looks like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct UserStats {
|
||||||
|
level: u16,
|
||||||
|
name: String,
|
||||||
|
authority: Pubkey
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `authority` would be the user the accounts belongs to.
|
||||||
|
|
||||||
|
This approach creates the following problem. It's easy to go from the user stats account to the user account address (just read the `authority` field) but if you just have the user account address (which is more likely), how do you find the user stats account? You can't. This is a problem because your game probably has instructions that require both the user stats account and its authority which means the client needs to pass those accounts into the instruction (for example, a `ChangeName` instruction). So maybe the frontend could store a mapping between a user's account address and a user's info address in local storage. This works until the user accidentally wipes their local storage.
|
||||||
|
|
||||||
|
With PDAs you can have a layout like this:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub struct UserStats {
|
||||||
|
level: u16,
|
||||||
|
name: String,
|
||||||
|
bump: u8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and encode the information about the relationship between the user and the user stats account in the address of the user stats account itself.
|
||||||
|
|
||||||
|
Reusing the pseudo code from above:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// pseudo code
|
||||||
|
let seeds = [b"user-stats", authority];
|
||||||
|
let (pda, bump) = find_pda(seeds, game_program_id);
|
||||||
|
```
|
||||||
|
|
||||||
|
When a user connects to your website, this pda calculation can be done client-side using their user account address as the `authority`. The resulting pda then serves as the address of the user's stats account. The `b"user-stats"` is added in case there are other account types that are also PDAs. If there were an inventory account, it could be inferred using these seeds:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let seeds = [b"inventory", authority];
|
||||||
|
```
|
||||||
|
|
||||||
|
To summarize, we have used PDAs to create a mapping between a user and their user stats account. There is no single hashmap object that exposes a `get` function. Instead, each value (the user stats address) can be found by using certain seeds ("user-stats" and the user account address) as inputs to the `find_pda` function.
|
||||||
|
|
||||||
|
#### How to build PDA hashmaps in Anchor
|
||||||
|
|
||||||
|
Continuing with the example from the previous sections, create a new workspace
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor init game
|
||||||
|
```
|
||||||
|
|
||||||
|
and copy the following code
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
pub mod game {
|
||||||
|
use super::*;
|
||||||
|
// handler function
|
||||||
|
pub fn create_user_stats(ctx: Context<CreateUserStats>, name: String) -> Result<()> {
|
||||||
|
let user_stats = &mut ctx.accounts.user_stats;
|
||||||
|
user_stats.level = 0;
|
||||||
|
if name.as_bytes().len() > 200 {
|
||||||
|
// proper error handling omitted for brevity
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
user_stats.name = name;
|
||||||
|
user_stats.bump = *ctx.bumps.get("user_stats").unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
pub struct UserStats {
|
||||||
|
level: u16,
|
||||||
|
name: String,
|
||||||
|
bump: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
// validation struct
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct CreateUserStats<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub user: Signer<'info>,
|
||||||
|
// space: 8 discriminator + 2 level + 4 name length + 200 name + 1 bump
|
||||||
|
#[account(
|
||||||
|
init,
|
||||||
|
payer = user,
|
||||||
|
space = 8 + 2 + 4 + 200 + 1, seeds = [b"user-stats", user.key().as_ref()], bump
|
||||||
|
)]
|
||||||
|
pub user_stats: Account<'info, UserStats>,
|
||||||
|
pub system_program: Program<'info, System>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the account validation struct we use `seeds` together with `init` to create a PDA with the desired seeds.
|
||||||
|
Additionally, we add an empty `bump` constraint to signal to anchor that it should find the canonical bump itself.
|
||||||
|
Then, in the handler, we call `ctx.bumps.get("user_stats")` to get the bump anchor found and save it to the user stats
|
||||||
|
account as an extra property.
|
||||||
|
|
||||||
|
If we then want to use the created pda in a different instruction, we can add a new validation struct (This will check that the `user_stats` account is the pda created by running `hash(seeds, user_stats.bump, game_program_id)`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// validation struct
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct ChangeUserName<'info> {
|
||||||
|
pub user: Signer<'info>,
|
||||||
|
#[account(mut, seeds = [b"user-stats", user.key().as_ref()], bump = user_stats.bump)]
|
||||||
|
pub user_stats: Account<'info, UserStats>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and another handler function:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// handler function (add this next to the create_user_stats function in the game module)
|
||||||
|
pub fn change_user_name(ctx: Context<ChangeUserName>, new_name: String) -> Result<()> {
|
||||||
|
if new_name.as_bytes().len() > 200 {
|
||||||
|
// proper error handling omitted for brevity
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
ctx.accounts.user_stats.name = new_name;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, let's add a test. Copy this into `game.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import * as anchor from '@project-serum/anchor'
|
||||||
|
import { Program } from '@project-serum/anchor'
|
||||||
|
import { PublicKey } from '@solana/web3.js'
|
||||||
|
import { Game } from '../target/types/game'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
describe('game', async () => {
|
||||||
|
const provider = anchor.AnchorProvider.env()
|
||||||
|
anchor.setProvider(provider)
|
||||||
|
|
||||||
|
const program = anchor.workspace.Game as Program<Game>
|
||||||
|
|
||||||
|
it('Sets and changes name!', async () => {
|
||||||
|
const [userStatsPDA, _] = await PublicKey.findProgramAddress(
|
||||||
|
[
|
||||||
|
anchor.utils.bytes.utf8.encode('user-stats'),
|
||||||
|
provider.wallet.publicKey.toBuffer(),
|
||||||
|
],
|
||||||
|
program.programId
|
||||||
|
)
|
||||||
|
|
||||||
|
await program.methods
|
||||||
|
.createUserStats('brian')
|
||||||
|
.accounts({
|
||||||
|
user: provider.wallet.publicKey,
|
||||||
|
userStats: userStatsPDA,
|
||||||
|
})
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
expect((await program.account.userStats.fetch(userStatsPDA)).name).to.equal(
|
||||||
|
'brian'
|
||||||
|
)
|
||||||
|
|
||||||
|
await program.methods
|
||||||
|
.changeUserName('tom')
|
||||||
|
.accounts({
|
||||||
|
user: provider.wallet.publicKey,
|
||||||
|
userStats: userStatsPDA,
|
||||||
|
})
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
expect((await program.account.userStats.fetch(userStatsPDA)).name).to.equal(
|
||||||
|
'tom'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Exactly as described in the subchapter before this one, we use a `find` function to find the PDA. We can then use it just like a normal address. Well, almost. When we call `createUserStats`, we don't have to add the PDA to the `[signers]` array even though account creation requires a signature. This is because it is impossible to sign the transaction from outside the program as the PDA (it's not a public key so there is no private key to sign with). Instead, the signature is added when the CPI to the system program is made. We're going to explain how this works in the [Programs as Signers](#programs-as-signers) section.
|
||||||
|
|
||||||
|
#### Enforcing uniqueness
|
||||||
|
|
||||||
|
A subtle result of this hashmap structure is enforced uniqueness. When `init` is used with `seeds` and `bump`, it will always search for the canonical bump. This means that it can only be called once (because the 2nd time it's called the PDA will already be initialized). To illustrate how powerful enforced uniqueness is, consider a decentralized exchange program. In this program, anyone can create a new market for two assets. However, the program creators want liquidity to be concentrated so there should only be one market for every combination of two assets. This could be done without PDAs but would require a global account that saves all the different markets. Then upon market creation, the program would check whether the asset combination exists in the global market list. With PDAs this can be done in a much more straightforward way. Any market would simply be the PDA of the mint addresses of the two assets. The program would then check whether either of the two possible PDAs (because the market could've been created with the assets in reverse order) already exists.
|
||||||
|
|
||||||
|
### Programs as Signers
|
||||||
|
|
||||||
|
Creating PDAs requires them to sign the `createAccount` CPI of the system program. How does that work?
|
||||||
|
|
||||||
|
PDAs are not public keys so it's impossible for them to sign anything. However, PDAs can still pseudo sign CPIs.
|
||||||
|
In anchor, to sign with a pda you have to change `CpiContext::new(cpi_program, cpi_accounts)` to `CpiContext::new_with_signer(cpi_program, cpi_accounts, seeds)` where the `seeds` argument are the seeds _and_ the bump the PDA was created with.
|
||||||
|
When the CPI is invoked, for each account in `cpi_accounts` the Solana runtime will check whether`hash(seeds, current_program_id) == account address` is true. If yes, that account's `is_signer` flag will be turned to true.
|
||||||
|
This means a PDA derived from some program X, may only be used to sign CPIs that originate from that program X. This means that on a high level, PDA signatures can be considered program signatures.
|
||||||
|
|
||||||
|
This is great news because for many programs it is necessary that the program itself takes the authority over some assets.
|
||||||
|
For instance, lending protocol programs need to manage deposited collateral and automated market maker programs need to manage the tokens put into their liquidity pools.
|
||||||
|
|
||||||
|
Let's revisit the puppet workspace and add a PDA signature.
|
||||||
|
|
||||||
|
First, adjust the puppet-master code:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use puppet::cpi::accounts::SetData;
|
||||||
|
use puppet::program::Puppet;
|
||||||
|
use puppet::{self, Data};
|
||||||
|
|
||||||
|
declare_id!("HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
mod puppet_master {
|
||||||
|
use super::*;
|
||||||
|
pub fn pull_strings(ctx: Context<PullStrings>, bump: u8, data: u64) -> Result<()> {
|
||||||
|
let bump = &[bump][..];
|
||||||
|
puppet::cpi::set_data(
|
||||||
|
ctx.accounts.set_data_ctx().with_signer(&[&[bump][..]]),
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct PullStrings<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub puppet: Account<'info, Data>,
|
||||||
|
pub puppet_program: Program<'info, Puppet>,
|
||||||
|
/// CHECK: only used as a signing PDA
|
||||||
|
pub authority: UncheckedAccount<'info>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'info> PullStrings<'info> {
|
||||||
|
pub fn set_data_ctx(&self) -> CpiContext<'_, '_, '_, 'info, SetData<'info>> {
|
||||||
|
let cpi_program = self.puppet_program.to_account_info();
|
||||||
|
let cpi_accounts = SetData {
|
||||||
|
puppet: self.puppet.to_account_info(),
|
||||||
|
authority: self.authority.to_account_info(),
|
||||||
|
};
|
||||||
|
CpiContext::new(cpi_program, cpi_accounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `authority` account is now an `UncheckedAccount` instead of a `Signer`. When the puppet-master is invoked, the `authority` pda is not a signer yet so we mustn't add a check for it. We just care about the puppet-master being able to sign so we don't add any additional seeds. Just a bump that is calculated off-chain and then passed to the function.
|
||||||
|
|
||||||
|
Finally, this is the new `puppet.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import * as anchor from '@project-serum/anchor'
|
||||||
|
import { Program } from '@project-serum/anchor'
|
||||||
|
import { Keypair, PublicKey } from '@solana/web3.js'
|
||||||
|
import { Puppet } from '../target/types/puppet'
|
||||||
|
import { PuppetMaster } from '../target/types/puppet_master'
|
||||||
|
import { expect } from 'chai'
|
||||||
|
|
||||||
|
describe('puppet', () => {
|
||||||
|
const provider = anchor.AnchorProvider.env()
|
||||||
|
anchor.setProvider(provider)
|
||||||
|
|
||||||
|
const puppetProgram = anchor.workspace.Puppet as Program<Puppet>
|
||||||
|
const puppetMasterProgram = anchor.workspace
|
||||||
|
.PuppetMaster as Program<PuppetMaster>
|
||||||
|
|
||||||
|
const puppetKeypair = Keypair.generate()
|
||||||
|
|
||||||
|
it('Does CPI!', async () => {
|
||||||
|
const [puppetMasterPDA, puppetMasterBump] =
|
||||||
|
await PublicKey.findProgramAddress([], puppetMasterProgram.programId)
|
||||||
|
|
||||||
|
await puppetProgram.methods
|
||||||
|
.initialize(puppetMasterPDA)
|
||||||
|
.accounts({
|
||||||
|
puppet: puppetKeypair.publicKey,
|
||||||
|
user: provider.wallet.publicKey,
|
||||||
|
})
|
||||||
|
.signers([puppetKeypair])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
await puppetMasterProgram.methods
|
||||||
|
.pullStrings(puppetMasterBump, new anchor.BN(42))
|
||||||
|
.accounts({
|
||||||
|
puppetProgram: puppetProgram.programId,
|
||||||
|
puppet: puppetKeypair.publicKey,
|
||||||
|
authority: puppetMasterPDA,
|
||||||
|
})
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
expect(
|
||||||
|
(
|
||||||
|
await puppetProgram.account.data.fetch(puppetKeypair.publicKey)
|
||||||
|
).data.toNumber()
|
||||||
|
).to.equal(42)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `authority` is no longer a randomly generated keypair but a PDA derived from the puppet-master program. This means the puppet-master can sign with it which it does inside `pullStrings`. It's worth noting that our implementation also allows non-canonical bumps but again because we are only interested in being able to sign we don't care which bump is used.
|
||||||
|
|
||||||
|
> In some cases it's possible to reduce the number of accounts you need by making a PDA storing state also sign a CPI instead of defining a separate PDA to do that.
|
||||||
|
|
||||||
|
## PDAs: Conclusion
|
||||||
|
|
||||||
|
This section serves as a brief recap of the different things you can do with PDAs.
|
||||||
|
|
||||||
|
First, you can create hashmaps with them. We created a user stats PDA which was derived from the user address. This derivation linked the user address and the user stats account, allowing the latter to be easily found given the former.
|
||||||
|
Hashmaps also result in enforced uniqueness which can be used in many different ways, e.g. for only allowing one market per two assets in a decentralized exchange.
|
||||||
|
|
||||||
|
Secondly, PDAs can be used to allow programs to sign CPIs. This means that programs can be given control over assets which they then manage according to the rules defined in their code.
|
||||||
|
|
||||||
|
You can even combine these two use cases and use a PDA that's used in an instruction as a state account to also sign a CPI.
|
||||||
|
|
||||||
|
Admittedly, working with PDAs is one of the most challenging parts of working with Solana.
|
||||||
|
This is why in addition to our explanations here, we want to provide you with some further resources.
|
||||||
|
|
||||||
|
- [Pencilflips's twitter thread on PDAs](https://twitter.com/pencilflip/status/1455948263853600768?s=20&t=J2JXCwv395D7MNkX7a9LGw)
|
||||||
|
- [jarry xiao's talk on PDAs and CPIs](https://www.youtube.com/watch?v=iMWaQRyjpl4)
|
||||||
|
- [paulx's guide on everything Solana (covers much more than PDAs)](https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/)
|
|
@ -1,24 +1,28 @@
|
||||||
# Publishing Source
|
---
|
||||||
|
title: Publishing Source
|
||||||
|
description: Anchor - Publishing Source
|
||||||
|
---
|
||||||
|
|
||||||
The Anchor Program Registry at [anchor.projectserum.com](https://anchor.projectserum.com)
|
The Anchor Program Registry at [apr.dev](https://apr.dev)
|
||||||
hosts a catalog of verified programs on Solana both written with and without Anchor. It is recommended
|
hosts a catalog of verified programs on Solana both written with and without Anchor. It is recommended
|
||||||
that authors of smart contracts publish their source to promote best
|
that authors of smart contracts publish their source to promote best
|
||||||
practices for security and transparency.
|
practices for security and transparency.
|
||||||
|
|
||||||
::: tip note
|
---
|
||||||
|
|
||||||
|
{% callout title="Note" %}
|
||||||
The Anchor Program Registry is currently in alpha testing. For access to publishing
|
The Anchor Program Registry is currently in alpha testing. For access to publishing
|
||||||
please ask on [Discord](https://discord.gg/rg5ZZPmmTm).
|
please ask on [Discord](http://discord.gg/ZCHmqvXgDw).
|
||||||
:::
|
{% /callout %}
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
The process for publishing is mostly identical to `crates.io`.
|
The process for publishing is mostly identical to `crates.io`.
|
||||||
|
|
||||||
* Signup for an account [here](https://anchor.projectserum.com/signup).
|
- Signup for an account [here](https://apr.dev).
|
||||||
* Confirm your email by clicking the link sent to your address.
|
- Navigate to your Profile on the top navbar.
|
||||||
* Navigate to your Username -> Account Settings on the top navbar.
|
- Click "Generate New Access Token".
|
||||||
* Click "New Token" in the **API Access** section.
|
- Run `anchor login <token>` at the command line.
|
||||||
* Run `anchor login <token>` at the command line.
|
|
||||||
|
|
||||||
And you're ready to interact with the registry.
|
And you're ready to interact with the registry.
|
||||||
|
|
||||||
|
@ -53,34 +57,34 @@ Here there are four sections.
|
||||||
to all programs in the local
|
to all programs in the local
|
||||||
workspace, i.e., the path to the `Cargo.toml` manifest associated with each
|
workspace, i.e., the path to the `Cargo.toml` manifest associated with each
|
||||||
program that can be compiled by the `anchor` CLI. For programs using the
|
program that can be compiled by the `anchor` CLI. For programs using the
|
||||||
standard Anchor workflow, this can be ommitted. For programs not written in Anchor
|
standard Anchor workflow, this can be ommitted. For programs not written in Anchor
|
||||||
but still want to publish, this should be added.
|
but still want to publish, this should be added.
|
||||||
3. `[provider]` - configures the wallet and cluster settings. Here, `mainnet` is used because the registry only supports `mainnet` binary verification at the moment.
|
3. `[provider]` - configures the wallet and cluster settings. Here, `mainnet` is used because the registry only supports `mainnet` binary verification at the moment.
|
||||||
3. `[programs.mainnet]` - configures each program in the workpace, providing
|
4. `[programs.mainnet]` - configures each program in the workpace, providing
|
||||||
the `address` of the program to verify.
|
the `address` of the program to verify.
|
||||||
|
|
||||||
::: tip
|
{% callout title="Note" %}
|
||||||
When defining program in `[programs.mainnet]`, make sure the name provided
|
When defining program in `[programs.mainnet]`, make sure the name provided
|
||||||
matches the **lib** name for your program, which is defined
|
matches the **lib** name for your program, which is defined
|
||||||
by your program's Cargo.toml.
|
by your program's Cargo.toml.
|
||||||
:::
|
{% /callout %}
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
#### Anchor Program
|
#### Anchor Program
|
||||||
|
|
||||||
An example of a toml file for an Anchor program can be found [here](https://anchor.projectserum.com/build/2).
|
An example of a toml file for an Anchor program can be found [here](https://www.apr.dev/program/22Y43yTVxuUkoRKdm9thyRhQ3SdgQS7c7kB6UNCiaczD/build/2).
|
||||||
|
|
||||||
#### Non Anchor Program
|
#### Non Anchor Program
|
||||||
|
|
||||||
An example of a toml file for a non-anchor program can be found [here](https://anchor.projectserum.com/build/1).
|
An example of a toml file for a non-anchor program can be found [here](https://www.apr.dev/program/9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin/build/1).
|
||||||
|
|
||||||
## Publishing
|
## Publishing
|
||||||
|
|
||||||
To publish to the Anchor Program Registry, change directories to the `Anchor.toml`
|
To publish to the Anchor Program Registry, change directories to the `Anchor.toml`
|
||||||
defined root and run
|
defined root and run
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
anchor publish <program-name>
|
anchor publish <program-name>
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
title: Space Reference
|
||||||
|
description: Anchor - Space Reference
|
||||||
|
---
|
||||||
|
|
||||||
|
This reference tells you how much space you should allocate for an account.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This only applies to accounts that don't use `zero-copy`. `zero-copy` uses `repr(C)` with a pointer cast,
|
||||||
|
so there the `C` layout applies.
|
||||||
|
|
||||||
|
In addition to the space for the account data, you have to add `8` to the `space` constraint for Anchor's internal discriminator (see the example).
|
||||||
|
|
||||||
|
| Types | Space in bytes | Details/Example |
|
||||||
|
| ---------- | ----------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||||
|
| bool | 1 | would only require 1 bit but still uses 1 byte |
|
||||||
|
| u8/i8 | 1 |
|
||||||
|
| u16/i16 | 2 |
|
||||||
|
| u32/i32 | 4 |
|
||||||
|
| u64/i64 | 8 |
|
||||||
|
| u128/i128 | 16 |
|
||||||
|
| [T;amount] | space(T) \* amount | e.g. space([u16;32]) = 2 \* 32 = 64 |
|
||||||
|
| Pubkey | 32 |
|
||||||
|
| Vec\<T> | 4 + (space(T) \* amount) | Account size is fixed so account should be initialized with sufficient space from the beginning |
|
||||||
|
| String | 4 + length of string in bytes | Account size is fixed so account should be initialized with sufficient space from the beginning |
|
||||||
|
| Option\<T> | 1 + (space(T)) |
|
||||||
|
| Enum | 1 + Largest Variant Size | e.g. Enum { A, B { val: u8 }, C { val: u16 } } -> 1 + space(u16) = 3 |
|
||||||
|
| f32 | 4 | serialization will fail for NaN |
|
||||||
|
| f64 | 8 | serialization will fail for NaN |
|
||||||
|
|
||||||
|
# Example
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[account]
|
||||||
|
pub struct MyData {
|
||||||
|
pub val: u16,
|
||||||
|
pub state: GameState,
|
||||||
|
pub players: Vec<Pubkey> // we want to support up to 10 players
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MyData {
|
||||||
|
pub const MAX_SIZE: usize = 2 + (1 + 32) + (4 + 10 * 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub enum GameState {
|
||||||
|
Active,
|
||||||
|
Tie,
|
||||||
|
Won { winner: Pubkey },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct InitializeMyData<'info> {
|
||||||
|
// Note that we have to add 8 to the space for the internal anchor
|
||||||
|
#[account(init, payer = signer, space = 8 + MyData::MAX_SIZE)]
|
||||||
|
pub acc: Account<'info, MyData>,
|
||||||
|
pub signer: Signer<'info>,
|
||||||
|
pub system_program: Program<'info, System>
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,176 @@
|
||||||
|
---
|
||||||
|
title: The Accounts Struct
|
||||||
|
description: Anchor - The Accounts Struct
|
||||||
|
---
|
||||||
|
|
||||||
|
The Accounts struct is where you define which accounts your instruction expects and which constraints these accounts should adhere to. You do this via two constructs: Types and constraints.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Types
|
||||||
|
|
||||||
|
> [Account Types Reference](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/index.html)
|
||||||
|
|
||||||
|
Each type has a specific use case in mind. Detailed explanations for the types can be found in the [reference](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/index.html). We will briefly explain the most important type here, the `Account` type.
|
||||||
|
|
||||||
|
### The Account Type
|
||||||
|
|
||||||
|
> [Account Reference](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html)
|
||||||
|
|
||||||
|
The `Account` type is used when an instruction is interested in the deserialized data of the account. Consider the following example where we set some data in an account:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
|
||||||
|
ctx.accounts.my_account.data = data;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MyAccount {
|
||||||
|
data: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetData<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub my_account: Account<'info, MyAccount>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`Account` is generic over `T`. This `T` is a type you can create yourself to store data. In this example, we have created a struct `MyAccount` with a single `data` field to store a `u64`. Account requires `T` to implement certain functions (e.g. functions that (de)serialize `T`). Most of the time, you can use the `#[account]` attribute to add these functions to your data, as is done in the example.
|
||||||
|
|
||||||
|
Most importantly, the `#[account]` attribute sets the owner of that data to the `ID` (the one we created earlier with `declare_id`) of the crate `#[account]` is used in. The Account type can then check for you that the `AccountInfo` passed into your instruction has its `owner` field set to the correct program. In this example, `MyAccount` is declared in our own crate so `Account` will verify that the owner of `my_account` equals the address we declared with `declare_id`.
|
||||||
|
|
||||||
|
#### Using `Account<'a, T>` with non-anchor program accounts
|
||||||
|
|
||||||
|
There may be cases where you want your program to interact with a non-Anchor program. You can still get all the benefits of `Account` but you have to write a custom wrapper type instead of using `#[account]`. For instance, Anchor provides wrapper types for the token program accounts so they can be used with `Account`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use anchor_lang::prelude::*;
|
||||||
|
use anchor_spl::token::TokenAccount;
|
||||||
|
|
||||||
|
declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
|
||||||
|
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
|
||||||
|
if ctx.accounts.token_account.amount > 0 {
|
||||||
|
ctx.accounts.my_account.data = data;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MyAccount {
|
||||||
|
data: u64,
|
||||||
|
mint: Pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetData<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub my_account: Account<'info, MyAccount>,
|
||||||
|
#[account(
|
||||||
|
constraint = my_account.mint == token_account.mint,
|
||||||
|
has_one = owner
|
||||||
|
)]
|
||||||
|
pub token_account: Account<'info, TokenAccount>,
|
||||||
|
pub owner: Signer<'info>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To run this example, add `anchor-spl = "<version>"` to the dependencies section in your `Cargo.toml`, located in the `programs/<your-project-name>/` directory. `<version>` should be equal to the `anchor-lang` version you're using.
|
||||||
|
|
||||||
|
In this example, we set the `data` field of an account if the caller has admin rights. We decide whether the caller is an admin by checking whether they own admin tokens for the account they want to change. We do most of this via constraints which we will look at in the next section.
|
||||||
|
The important thing to take away is that we use the `TokenAccount` type (that wraps around the token program's `Account` struct and adds the required functions) to make anchor ensure that the incoming account is owned by the token program and to make anchor deserialize it. This means we can use the `TokenAccount` properties inside our constraints (e.g. `token_account.mint`) as well as in the instruction function.
|
||||||
|
|
||||||
|
Check out the [reference for the Account type](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html) to learn how to implement your own wrapper types for non-anchor programs.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
> [Constraints reference](https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html)
|
||||||
|
|
||||||
|
Account types can do a lot of work for you but they're not dynamic enough to handle all the security checks a secure program requires.
|
||||||
|
|
||||||
|
Add constraints to an account with the following format:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[account(<constraints>)]
|
||||||
|
pub account: AccountType
|
||||||
|
```
|
||||||
|
|
||||||
|
Some constraints support custom Errors (we will explore errors [later](./errors.md)):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[account(...,<constraint> @ MyError::MyErrorVariant, ...)]
|
||||||
|
pub account: AccountType
|
||||||
|
```
|
||||||
|
|
||||||
|
For example, in the examples above, we used the `mut` constraint to indicate that `my_account` should be mutable. We used `has_one` to check that `token_account.owner == owner.key()`. And finally we used `constraint` to check an arbitrary expression; in this case, whether the incoming `TokenAccount` belongs to the admin mint.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetData<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub my_account: Account<'info, MyAccount>,
|
||||||
|
#[account(
|
||||||
|
constraint = my_account.mint == token_account.mint,
|
||||||
|
has_one = owner
|
||||||
|
)]
|
||||||
|
pub token_account: Account<'info, TokenAccount>,
|
||||||
|
pub owner: Signer<'info>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find information about all constraints in the reference. We will cover some of the most important ones in the milestone project at the end of the Essentials section.
|
||||||
|
|
||||||
|
## Safety checks
|
||||||
|
|
||||||
|
Two of the Anchor account types, [AccountInfo](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account_info/index.html) and [UncheckedAccount](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/unchecked_account/index.html) do not implement any checks on the account being passed. Anchor implements safety checks that encourage additional documentation describing why additional checks are not necesssary.
|
||||||
|
|
||||||
|
Attempting to build a program containing the following excerpt with `anchor build`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize<'info> {
|
||||||
|
pub potentially_dangerous: UncheckedAccount<'info>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
will result in an error similar to the following:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
Error:
|
||||||
|
/anchor/tests/unchecked/programs/unchecked/src/lib.rs:15:8
|
||||||
|
Struct field "potentially_dangerous" is unsafe, but is not documented.
|
||||||
|
Please add a `/// CHECK:` doc comment explaining why no checks through types are necessary.
|
||||||
|
See https://book.anchor-lang.com/anchor_in_depth/the_accounts_struct.html#safety-checks for more information.
|
||||||
|
```
|
||||||
|
|
||||||
|
To fix this, write a doc comment describing the potential security implications, e.g.:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Initialize<'info> {
|
||||||
|
/// CHECK: This is not dangerous because we don't read or write from this account
|
||||||
|
pub potentially_dangerous: UncheckedAccount<'info>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note the doc comment needs to be a [line or block doc comment](https://doc.rust-lang.org/reference/comments.html#doc-comments) (/// or /\*\*) to be interepreted as doc attribute by Rust. Double slash comments (//) are not interpreted as such.
|
||||||
|
|
||||||
|
{% callout type="warning" title="Note" %}
|
||||||
|
The doc comment needs to be a [line or block doc comment](https://doc.rust-lang.org/reference/comments.html#doc-comments) (/// or /\*\*) to be interepreted as doc attribute by Rust. Double slash comments (//) are not interpreted as such.
|
||||||
|
{% /callout %}
|
|
@ -0,0 +1,76 @@
|
||||||
|
---
|
||||||
|
title: The Program Module
|
||||||
|
description: Anchor - The Program Module
|
||||||
|
---
|
||||||
|
|
||||||
|
The program module is where you define your business logic. You do so by writing functions which can be called by clients or other programs. You've already seen one example of such a function, the `set_data` function from the previous section.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: u64) -> Result<()> {
|
||||||
|
if ctx.accounts.token_account.amount > 0 {
|
||||||
|
ctx.accounts.my_account.data = data;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
> [Context Reference](https://docs.rs/anchor-lang/latest/anchor_lang/context/index.html)
|
||||||
|
|
||||||
|
Each endpoint function takes a `Context` type as its first argument. Through this context argument it can access the accounts (`ctx.accounts`), the program id (`ctx.program_id`) of the executing program, and the remaining accounts (`ctx.remaining_accounts`). `remaining_accounts` is a vector that contains all accounts that were passed into the instruction but are not declared in the `Accounts` struct. This is useful when you want your function to handle a variable amount of accounts, e.g. when initializing a game with a variable number of players.
|
||||||
|
|
||||||
|
## Instruction Data
|
||||||
|
|
||||||
|
If your function requires instruction data, you can add it by adding arguments to the function after the context argument. Anchor will then automatically deserialize the instruction data into the arguments. You can have as many as you like. You can even pass in your own types as long as you use`#[derive(AnchorDeserialize)]` on them or implement `AnchorDeserialize` for them yourself. Here's an example with a custom type used as an instruction data arg:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: Data) -> Result<()> {
|
||||||
|
ctx.accounts.my_account.data = data.data;
|
||||||
|
ctx.accounts.my_account.age = data.age;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MyAccount {
|
||||||
|
pub data: u64,
|
||||||
|
pub age: u8
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Eq, PartialEq, Clone, Copy, Debug)]
|
||||||
|
pub struct Data {
|
||||||
|
pub data: u64,
|
||||||
|
pub age: u8
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Conveniently, `#[account]` implements `Anchor(De)Serialize` for `MyAccount`, so the example above can be simplified.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[program]
|
||||||
|
mod hello_anchor {
|
||||||
|
use super::*;
|
||||||
|
pub fn set_data(ctx: Context<SetData>, data: MyAccount) -> Result<()> {
|
||||||
|
ctx.accounts.my_account.set_inner(data);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[account]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MyAccount {
|
||||||
|
pub data: u64,
|
||||||
|
pub age: u8
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,568 @@
|
||||||
|
---
|
||||||
|
title: Project - Tic-Tac-Toe
|
||||||
|
description: Anchor - Milestone Project - Tic-Tac-Toe
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- TODO: fix -->
|
||||||
|
|
||||||
|
> [Program Code](https://github.com/coral-xyz/anchor/tree/master/docs/programs/tic-tac-toe)
|
||||||
|
|
||||||
|
You're now ready to build your first anchor project. Create a new anchor workspace with
|
||||||
|
|
||||||
|
```shell
|
||||||
|
anchor init tic-tac-toe
|
||||||
|
```
|
||||||
|
|
||||||
|
The program will have 2 instructions. First, we need to setup the game. We need to save who is playing it and create a board to play on. Then, the players take turns until there is a winner or a tie.
|
||||||
|
|
||||||
|
We recommend keeping programs in a single `lib.rs` file until they get too big. We would not split up this project into multiple files either but there is a section at the end of this chapter that explains how to do it for this and other programs.
|
||||||
|
|
||||||
|
## Setting up the game
|
||||||
|
|
||||||
|
### State
|
||||||
|
|
||||||
|
Let's begin by thinking about what data we should store. Each game has players, turns, a board, and a game state. This game state describes whether the game is active, tied, or one of the two players won. We can save all this data in an account. This means that each new game will have its own account. Add the following to the bottom of the `lib.rs` file:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[account]
|
||||||
|
pub struct Game {
|
||||||
|
players: [Pubkey; 2], // (32 * 2)
|
||||||
|
turn: u8, // 1
|
||||||
|
board: [[Option<Sign>; 3]; 3], // 9 * (1 + 1) = 18
|
||||||
|
state: GameState, // 32 + 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the game account. Next to the field definitions, you can see how many bytes each field requires. This will be very important later. Let's also add the `Sign` and the `GameState` type.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
|
||||||
|
pub enum GameState {
|
||||||
|
Active,
|
||||||
|
Tie,
|
||||||
|
Won { winner: Pubkey },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
AnchorSerialize,
|
||||||
|
AnchorDeserialize,
|
||||||
|
FromPrimitive,
|
||||||
|
ToPrimitive,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
PartialEq,
|
||||||
|
Eq
|
||||||
|
)]
|
||||||
|
pub enum Sign {
|
||||||
|
X,
|
||||||
|
O,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `GameState` and `Sign` derive some traits. `AnchorSerialize` and `AnchorDeserialize` are the crucial ones. All types that are used in types that are marked with `#[account]` must implement these two traits (or be marked with `#[account]` themselves). All other traits are important to our game logic and we are going to use them later. Generally, it is good practice to derive even more traits to make the life of others trying to interface with your program easier (see [Rust's API guidelines](https://rust-lang.github.io/api-guidelines/interoperability.html#types-eagerly-implement-common-traits-c-common-traits)) but for brevity's sake, we are not going to do that in this guide.
|
||||||
|
|
||||||
|
This won't quite work yet because `FromPrimitive` and `ToPrimitive` are unknown. Go to the `Cargo.toml` file right outside `src` (not the one at the root of the workspace) and add these two dependencies:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
num-traits = "0.2"
|
||||||
|
num-derive = "0.3"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, import them at the top of `lib.rs`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use num_derive::*;
|
||||||
|
use num_traits::*;
|
||||||
|
```
|
||||||
|
|
||||||
|
Now add the game logic:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl Game {
|
||||||
|
pub const MAXIMUM_SIZE: usize = (32 * 2) + 1 + (9 * (1 + 1)) + (32 + 1);
|
||||||
|
|
||||||
|
pub fn start(&mut self, players: [Pubkey; 2]) -> Result<()> {
|
||||||
|
require_eq!(self.turn, 0, TicTacToeError::GameAlreadyStarted);
|
||||||
|
self.players = players;
|
||||||
|
self.turn = 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.state == GameState::Active
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_player_index(&self) -> usize {
|
||||||
|
((self.turn - 1) % 2) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_player(&self) -> Pubkey {
|
||||||
|
self.players[self.current_player_index()]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(&mut self, tile: &Tile) -> Result<()> {
|
||||||
|
require!(self.is_active(), TicTacToeError::GameAlreadyOver);
|
||||||
|
|
||||||
|
match tile {
|
||||||
|
tile @ Tile {
|
||||||
|
row: 0..=2,
|
||||||
|
column: 0..=2,
|
||||||
|
} => match self.board[tile.row as usize][tile.column as usize] {
|
||||||
|
Some(_) => return Err(TicTacToeError::TileAlreadySet.into()),
|
||||||
|
None => {
|
||||||
|
self.board[tile.row as usize][tile.column as usize] =
|
||||||
|
Some(Sign::from_usize(self.current_player_index()).unwrap());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => return Err(TicTacToeError::TileOutOfBounds.into()),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_state();
|
||||||
|
|
||||||
|
if GameState::Active == self.state {
|
||||||
|
self.turn += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_winning_trio(&self, trio: [(usize, usize); 3]) -> bool {
|
||||||
|
let [first, second, third] = trio;
|
||||||
|
self.board[first.0][first.1].is_some()
|
||||||
|
&& self.board[first.0][first.1] == self.board[second.0][second.1]
|
||||||
|
&& self.board[first.0][first.1] == self.board[third.0][third.1]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_state(&mut self) {
|
||||||
|
for i in 0..=2 {
|
||||||
|
// three of the same in one row
|
||||||
|
if self.is_winning_trio([(i, 0), (i, 1), (i, 2)]) {
|
||||||
|
self.state = GameState::Won {
|
||||||
|
winner: self.current_player(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// three of the same in one column
|
||||||
|
if self.is_winning_trio([(0, i), (1, i), (2, i)]) {
|
||||||
|
self.state = GameState::Won {
|
||||||
|
winner: self.current_player(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// three of the same in one diagonal
|
||||||
|
if self.is_winning_trio([(0, 0), (1, 1), (2, 2)])
|
||||||
|
|| self.is_winning_trio([(0, 2), (1, 1), (2, 0)])
|
||||||
|
{
|
||||||
|
self.state = GameState::Won {
|
||||||
|
winner: self.current_player(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reaching this code means the game has not been won,
|
||||||
|
// so if there are unfilled tiles left, it's still active
|
||||||
|
for row in 0..=2 {
|
||||||
|
for column in 0..=2 {
|
||||||
|
if self.board[row][column].is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// game has not been won
|
||||||
|
// game has no more free tiles
|
||||||
|
// -> game ends in a tie
|
||||||
|
self.state = GameState::Tie;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We are not going to explore this code in detail together because it's rather simple rust code. It's just tic-tac-toe after all! Roughly, what happens when `play` is called:
|
||||||
|
|
||||||
|
1. Return error if game is over or
|
||||||
|
return error if given row or column are outside the 3x3 board or
|
||||||
|
return error if tile on board is already set
|
||||||
|
2. Determine current player and set tile to X or O
|
||||||
|
3. Update game state
|
||||||
|
4. If game is still active, increase the turn
|
||||||
|
|
||||||
|
Currently, the code doesn't compile because we need to add the `Tile`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||||
|
pub struct Tile {
|
||||||
|
row: u8,
|
||||||
|
column: u8,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
and the `TicTacToeError` type.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[error_code]
|
||||||
|
pub enum TicTacToeError {
|
||||||
|
TileOutOfBounds,
|
||||||
|
TileAlreadySet,
|
||||||
|
GameAlreadyOver,
|
||||||
|
NotPlayersTurn,
|
||||||
|
GameAlreadyStarted
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Setup Instruction
|
||||||
|
|
||||||
|
Before we write any game logic, we can add the instruction that will set up the game in its initial state. Rename the already existing instruction function and accounts struct to `setup_game` and `SetupGame` respectively. Now think about which accounts are needed to set up the game. Clearly, we need the game account. Before we can fill it with values, we need to create it. For that, we use the `init` constraint.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetupGame<'info> {
|
||||||
|
#[account(init)]
|
||||||
|
pub game: Account<'info, Game>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`init` immediately shouts at us and tells us to add a payer. Why do we need it? Because `init` creates `rent-exempt` accounts and someone has to pay for that. Naturally, if we want to take money from someone, we should make them sign as well as mark their account as mutable.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetupGame<'info> {
|
||||||
|
#[account(init, payer = player_one)]
|
||||||
|
pub game: Account<'info, Game>,
|
||||||
|
#[account(mut)]
|
||||||
|
pub player_one: Signer<'info>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`init` is not happy yet. It wants the system program to be inside the struct because `init` creates the game account by making a call to that program. So let's add it.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetupGame<'info> {
|
||||||
|
#[account(init, payer = player_one)]
|
||||||
|
pub game: Account<'info, Game>,
|
||||||
|
#[account(mut)]
|
||||||
|
pub player_one: Signer<'info>,
|
||||||
|
pub system_program: Program<'info, System>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
There's one more thing to do to complete `SetupGame`. Every account is created with a fixed amount of space, so we have to add this space to the instruction as well. This is what the comments next to the `Game` struct indicated.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct SetupGame<'info> {
|
||||||
|
#[account(init, payer = player_one, space = 8 + Game::MAXIMUM_SIZE)]
|
||||||
|
pub game: Account<'info, Game>,
|
||||||
|
#[account(mut)]
|
||||||
|
pub player_one: Signer<'info>,
|
||||||
|
pub system_program: Program<'info, System>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Let us briefly explain how we arrived at the `Game::MAXIMUM_SIZE`. Anchor uses the [borsh](https://borsh.io) specification to (de)serialize its state accounts.
|
||||||
|
|
||||||
|
- Pubkey has a length of `32` bytes so `2*32 = 64`
|
||||||
|
- u8 as a vector has a length of `1`
|
||||||
|
- the `board` has a length of (`9 * (1 + 1)`). We know the board has 9 tiles (-> `9`) of type `Option` which borsh serializes with 1 byte (set to `1` for Some and `0` for None) plus the size of whatever's in the `Option`. In this case, it's a simple enum with types that don't hold more types so the maximum size of the enum is also just `1` (for its discriminant). In total that means we get `9 (tiles) * (1 (Option) + 1(Sign discriminant))`.
|
||||||
|
- `state` is also an enum so we need `1` byte for the discriminant. We have to init the account with the maximum size and the maximum size of an enum is the size of its biggest variant. In this case that's the `winner` variant which holds a Pubkey. A Pubkey is `32` bytes long so the size of `state` is `1 (discriminant) + 32 (winner pubkey)` (`MAXIMUM_SIZE` is a [`const`](https://doc.rust-lang.org/std/keyword.const.html) variable so specifying it in terms of a sum of the sizes of `Game`'s members' fields does not incur any runtime cost).
|
||||||
|
|
||||||
|
In addition to the game's size, we have to add another 8 to the space. This is space for the internal discriminator which anchor sets automatically. In short, the discriminator is how anchor can differentiate between different accounts of the same program. For more information, check out the Anchor space reference.
|
||||||
|
|
||||||
|
> [Anchor Space Reference](./../anchor_references/space.md)
|
||||||
|
|
||||||
|
> (What about using `mem::size_of<Game>()`? This almost works but not quite. The issue is that borsh will always serialize an option as 1 byte for the variant identifier and then additional x bytes for the content if it's Some. Rust uses null-pointer optimization to make Option's variant identifier 0 bytes when it can, so an option is sometimes just as big as its contents. This is the case with `Sign`. This means the `MAXIMUM_SIZE` could also be expressed as `mem::size_of<Game>() + 9`.)
|
||||||
|
|
||||||
|
And with this, `SetupGame` is complete and we can move on to the `setup_game` function. (If you like playing detective, you can pause here and try to figure out why what we just did will not work. Hint: Have a look at the [specification](https://borsh.io/) of the serialization library Anchor uses. If you cannot figure it out, don't worry. We are going to fix it very soon, together.)
|
||||||
|
|
||||||
|
Let's start by adding an argument to the `setup_game` function.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn setup_game(ctx: Context<SetupGame>, player_two: Pubkey) -> Result<()> {
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Why didn't we just add `player_two` as an account in the accounts struct? There are two reasons for this. First, adding it there requires a little more space in the transaction that saves whether the account is writable and whether it's a signer. But we care about neither the mutability of the account nor whether it's a signer. We just need its address. This brings us to the second and more important reason: Simultaneous network transactions can affect each other if they share the same accounts. For example, if we add `player_two` to the accounts struct, during our transaction, no other transaction can edit `player_two`'s account. Therefore, we block all other transactions that want to edit `player_two`'s account, even though we neither want to read from nor write to the account. We just care about its address!
|
||||||
|
|
||||||
|
Finish the instruction function by setting the game to its initial values:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn setup_game(ctx: Context<SetupGame>, player_two: Pubkey) -> Result<()> {
|
||||||
|
ctx.accounts.game.start([ctx.accounts.player_one.key(), player_two])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, run `anchor build`. On top of compiling your program, this command creates an [IDL](https://en.wikipedia.org/wiki/Interface_description_language) for your program. You can find it in `target/idl`. The anchor typescript client can automatically parse this IDL and generate functions based on it. What this means is that each anchor program gets its own typescript client for free! (Technically, you don't have to call `anchor build` before testing. `anchor test` will do it for you.)
|
||||||
|
|
||||||
|
### Testing the Setup Instruction
|
||||||
|
|
||||||
|
Time to test our code! Head over into the `tests` folder in the root directory. Open the `tic-tac-toe.ts` file and remove the existing `it` test. Then, put the following into the `describe` section:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('setup game!', async () => {
|
||||||
|
const gameKeypair = anchor.web3.Keypair.generate()
|
||||||
|
const playerOne = (program.provider as anchor.AnchorProvider).wallet
|
||||||
|
const playerTwo = anchor.web3.Keypair.generate()
|
||||||
|
await program.methods
|
||||||
|
.setupGame(playerTwo.publicKey)
|
||||||
|
.accounts({
|
||||||
|
game: gameKeypair.publicKey,
|
||||||
|
playerOne: playerOne.publicKey,
|
||||||
|
})
|
||||||
|
.signers([gameKeypair])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
let gameState = await program.account.game.fetch(gameKeypair.publicKey)
|
||||||
|
expect(gameState.turn).to.equal(1)
|
||||||
|
expect(gameState.players).to.eql([playerOne.publicKey, playerTwo.publicKey])
|
||||||
|
expect(gameState.state).to.eql({ active: {} })
|
||||||
|
expect(gameState.board).to.eql([
|
||||||
|
[null, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
])
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
and add this to the top of your file:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { expect } from 'chai'
|
||||||
|
```
|
||||||
|
|
||||||
|
> When you adjust your test files it may happen that you'll see errors everywhere.
|
||||||
|
> This is likely because the test file is looking for types from your program that haven't been generated yet.
|
||||||
|
> To generate them, run `anchor build`. This builds the program and creates the idl and typescript types.
|
||||||
|
|
||||||
|
The test begins by creating some keypairs. Importantly, `playerOne` is not a keypair but the wallet of the program's provider. The provider details are defined in the `Anchor.toml` file in the root of the project. The provider serves as the keypair that pays for (and therefore signs) all transactions.
|
||||||
|
Then, we send the transaction.
|
||||||
|
The structure of the transaction function is as follows: First come the instruction arguments. For this function, the public key of the second player. Then come the accounts. Lastly, we add a signers array. We have to add the `gameKeypair` here because whenever an account gets created, it has to sign its creation transaction. We don't have to add `playerOne` even though we gave it the `Signer` type in the program because it is the program provider and therefore signs the transaction by default.
|
||||||
|
We did not have to specify the `system_program` account. This is because anchor recognizes this account and is able to infer it. This is also true for other known accounts such as the `token_program` or the `rent` sysvar account.
|
||||||
|
|
||||||
|
After the transaction returns, we can fetch the state of the game account. You can fetch account state using the `program.account` namespace.
|
||||||
|
Finally, we verify the game has been set up properly by comparing the actual state and the expected state. To learn how Anchor maps the Rust types to the js/ts types, check out the [Javascript Anchor Types Reference](./../anchor_references/javascript_anchor_types_reference.md).
|
||||||
|
|
||||||
|
Now, run `anchor test`. This starts up (and subsequently shuts down) a local validator (make sure you don't have one running before) and runs your tests using the test script defined in `Anchor.toml`.
|
||||||
|
|
||||||
|
> If you get the error `Error: Unable to read keypair file` when running the test, you likely need to generate a Solana keypair using `solana-keygen new`.
|
||||||
|
|
||||||
|
## Playing the game
|
||||||
|
|
||||||
|
### The Play Instruction
|
||||||
|
|
||||||
|
The `Play` accounts struct is straightforward. We need the game and a player:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct Play<'info> {
|
||||||
|
#[account(mut)]
|
||||||
|
pub game: Account<'info, Game>,
|
||||||
|
pub player: Signer<'info>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`player` needs to sign or someone else could play for the player.
|
||||||
|
|
||||||
|
Finally, we can add the `play` function inside the program module.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn play(ctx: Context<Play>, tile: Tile) -> Result<()> {
|
||||||
|
let game = &mut ctx.accounts.game;
|
||||||
|
|
||||||
|
require_keys_eq!(
|
||||||
|
game.current_player(),
|
||||||
|
ctx.accounts.player.key(),
|
||||||
|
TicTacToeError::NotPlayersTurn
|
||||||
|
);
|
||||||
|
|
||||||
|
game.play(&tile)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
We've checked in the accounts struct that the `player` account has signed the transaction, but we do not check that it is the `player` we expect. That's what the `require_keys_eq` check in `play` is for.
|
||||||
|
|
||||||
|
### Testing the Play Instruction
|
||||||
|
|
||||||
|
Testing the `play` instruction works the exact same way. To avoid repeating yourself, create a helper function at the top of the test file:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
async function play(
|
||||||
|
program: Program<TicTacToe>,
|
||||||
|
game,
|
||||||
|
player,
|
||||||
|
tile,
|
||||||
|
expectedTurn,
|
||||||
|
expectedGameState,
|
||||||
|
expectedBoard
|
||||||
|
) {
|
||||||
|
await program.methods
|
||||||
|
.play(tile)
|
||||||
|
.accounts({
|
||||||
|
player: player.publicKey,
|
||||||
|
game,
|
||||||
|
})
|
||||||
|
.signers(player instanceof (anchor.Wallet as any) ? [] : [player])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
const gameState = await program.account.game.fetch(game)
|
||||||
|
expect(gameState.turn).to.equal(expectedTurn)
|
||||||
|
expect(gameState.state).to.eql(expectedGameState)
|
||||||
|
expect(gameState.board).to.eql(expectedBoard)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can create then a new `it` test, setup the game like in the previous test, but then keep calling the `play` function you just added to simulate a complete run of the game. Let's begin with the first turn:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
it('player one wins', async () => {
|
||||||
|
const gameKeypair = anchor.web3.Keypair.generate()
|
||||||
|
const playerOne = program.provider.wallet
|
||||||
|
const playerTwo = anchor.web3.Keypair.generate()
|
||||||
|
await program.methods
|
||||||
|
.setupGame(playerTwo.publicKey)
|
||||||
|
.accounts({
|
||||||
|
game: gameKeypair.publicKey,
|
||||||
|
playerOne: playerOne.publicKey,
|
||||||
|
})
|
||||||
|
.signers([gameKeypair])
|
||||||
|
.rpc()
|
||||||
|
|
||||||
|
let gameState = await program.account.game.fetch(gameKeypair.publicKey)
|
||||||
|
expect(gameState.turn).to.equal(1)
|
||||||
|
expect(gameState.players).to.eql([playerOne.publicKey, playerTwo.publicKey])
|
||||||
|
expect(gameState.state).to.eql({ active: {} })
|
||||||
|
expect(gameState.board).to.eql([
|
||||||
|
[null, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
])
|
||||||
|
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne,
|
||||||
|
{ row: 0, column: 0 },
|
||||||
|
2,
|
||||||
|
{ active: {} },
|
||||||
|
[
|
||||||
|
[{ x: {} }, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
and run `anchor test`.
|
||||||
|
|
||||||
|
You can finish writing the test by yourself (or check out [the reference implementation](https://github.com/project-serum/anchor-book/tree/master/programs/tic-tac-toe)). Try to simulate a win and a tie!
|
||||||
|
|
||||||
|
Proper testing also includes tests that try to exploit the contract. You can check whether you've protected yourself properly by calling `play` with unexpected parameters. You can also familiarize yourself with the returned `AnchorErrors` this way. For example:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerTwo,
|
||||||
|
{ row: 5, column: 1 }, // ERROR: out of bounds row
|
||||||
|
4,
|
||||||
|
{ active: {} },
|
||||||
|
[
|
||||||
|
[{ x: {} }, { x: {} }, null],
|
||||||
|
[{ o: {} }, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
// we use this to make sure we definitely throw an error
|
||||||
|
chai.assert(false, "should've failed but didn't ")
|
||||||
|
} catch (_err) {
|
||||||
|
expect(_err).to.be.instanceOf(AnchorError)
|
||||||
|
const err: AnchorError = _err
|
||||||
|
expect(err.error.errorCode.number).to.equal(6000)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await play(
|
||||||
|
program,
|
||||||
|
gameKeypair.publicKey,
|
||||||
|
playerOne, // ERROR: same player in subsequent turns
|
||||||
|
|
||||||
|
// change sth about the tx because
|
||||||
|
// duplicate tx that come in too fast
|
||||||
|
// after each other may get dropped
|
||||||
|
{ row: 1, column: 0 },
|
||||||
|
2,
|
||||||
|
{ active: {} },
|
||||||
|
[
|
||||||
|
[{ x: {} }, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
[null, null, null],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
chai.assert(false, "should've failed but didn't ")
|
||||||
|
} catch (_err) {
|
||||||
|
expect(_err).to.be.instanceOf(AnchorError)
|
||||||
|
const err: AnchorError = _err
|
||||||
|
expect(err.error.errorCode.code).to.equal('NotPlayersTurn')
|
||||||
|
expect(err.error.errorCode.number).to.equal(6003)
|
||||||
|
expect(err.program.equals(program.programId)).is.true
|
||||||
|
expect(err.error.comparedValues).to.deep.equal([
|
||||||
|
playerTwo.publicKey,
|
||||||
|
playerOne.publicKey,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Solana has three main clusters: `mainnet-beta`, `devnet`, and `testnet`.
|
||||||
|
For developers, `devnet` and `mainnet-beta` are the most interesting. `devnet` is where you test your application in a more realistic environment than `localnet`. `testnet` is mostly for validators.
|
||||||
|
|
||||||
|
We are going to deploy on `devnet`.
|
||||||
|
|
||||||
|
Here is your deployment checklist 🚀
|
||||||
|
|
||||||
|
1. Run `anchor build`. Your program keypair is now in `target/deploy`. Keep this keypair secret. You can reuse it on all clusters.
|
||||||
|
2. Run `anchor keys list` to display the keypair's public key and copy it into your `declare_id!` macro at the top of `lib.rs`.
|
||||||
|
3. Run `anchor build` again. This step is necessary to include the new program id in the binary.
|
||||||
|
4. Change the `provider.cluster` variable in `Anchor.toml` to `devnet`.
|
||||||
|
5. Run `anchor deploy`
|
||||||
|
6. Run `anchor test`
|
||||||
|
|
||||||
|
There is more to deployments than this e.g. understanding how the BPFLoader works, how to manage keys, how to upgrade your programs and more. Keep reading to learn more!
|
||||||
|
|
||||||
|
## Program directory organization
|
||||||
|
|
||||||
|
> [Program Code](https://github.com/project-serum/anchor-book/tree/master/programs/tic-tac-toe)
|
||||||
|
|
||||||
|
Eventually, some programs become too big to keep them in a single file and it makes sense to break them up.
|
||||||
|
|
||||||
|
Splitting a program into multiple files works almost the exact same way as splitting up a regular rust program, so if you haven't already, now is the time to read all about that in the [rust book](https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html).
|
||||||
|
|
||||||
|
We recommend the following directory structure (using the tic-tac-toe program as an example):
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
+-- lib.rs
|
||||||
|
+-- errors.rs
|
||||||
|
+-- instructions
|
||||||
|
| +-- play.rs
|
||||||
|
| +-- setup_game.rs
|
||||||
|
| +-- mod.rs
|
||||||
|
+-- state
|
||||||
|
| +-- game.rs
|
||||||
|
| +-- mod.rs
|
||||||
|
```
|
||||||
|
|
||||||
|
The crucial difference to a normal rust layout is the way that instructions have to be imported. The `lib.rs` file has to import each instruction module with a wildcard import (e.g. `use instructions::play::*;`). This has to be done because the `#[program]` macro depends on generated code inside each instruction file.
|
||||||
|
|
||||||
|
To make the imports shorter you can re-export the instruction modules in the `mod.rs` file in the instructions directory with the `pub use` syntax and then import all instructions in the `lib.rs` file with `use instructions::*;`.
|
||||||
|
|
||||||
|
Well done! You've finished the essentials section. You can now move on to the more advanced parts of Anchor.
|
|
@ -1,4 +1,7 @@
|
||||||
# Verifiable Builds
|
---
|
||||||
|
title: Verifiable Builds
|
||||||
|
description: Anchor - Verifiable Builds
|
||||||
|
---
|
||||||
|
|
||||||
Building programs with the Solana CLI may embed machine specfic
|
Building programs with the Solana CLI may embed machine specfic
|
||||||
code into the resulting binary. As a result, building the same program
|
code into the resulting binary. As a result, building the same program
|
||||||
|
@ -6,6 +9,8 @@ on different machines may produce different executables. To get around this
|
||||||
problem, one can build inside a docker image with pinned dependencies to produce
|
problem, one can build inside a docker image with pinned dependencies to produce
|
||||||
a verifiable build.
|
a verifiable build.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Anchor makes this easy by providing CLI commands to build and take care of
|
Anchor makes this easy by providing CLI commands to build and take care of
|
||||||
docker for you. To get started, first make sure you
|
docker for you. To get started, first make sure you
|
||||||
[install](https://docs.docker.com/get-docker/) docker on your local machine.
|
[install](https://docs.docker.com/get-docker/) docker on your local machine.
|
||||||
|
@ -14,7 +19,7 @@ docker for you. To get started, first make sure you
|
||||||
|
|
||||||
To produce a verifiable build, run
|
To produce a verifiable build, run
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
anchor build --verifiable
|
anchor build --verifiable
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -22,7 +27,7 @@ anchor build --verifiable
|
||||||
|
|
||||||
To verify a build against a program deployed on mainnet, run
|
To verify a build against a program deployed on mainnet, run
|
||||||
|
|
||||||
```bash
|
```shell
|
||||||
anchor verify -p <lib-name> <program-id>
|
anchor verify -p <lib-name> <program-id>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -34,17 +39,18 @@ If the program has an IDL, it will also check the IDL deployed on chain matches.
|
||||||
|
|
||||||
A docker image for each version of Anchor is published on [Docker Hub](https://hub.docker.com/r/projectserum/build). They are tagged in the form `projectserum/build:<version>`. For example, to get the image for Anchor `v0.24.2` one can run
|
A docker image for each version of Anchor is published on [Docker Hub](https://hub.docker.com/r/projectserum/build). They are tagged in the form `projectserum/build:<version>`. For example, to get the image for Anchor `v0.24.2` one can run
|
||||||
|
|
||||||
```
|
```shell
|
||||||
docker pull projectserum/build:v0.24.2
|
docker pull projectserum/build:v0.24.2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Removing an Image
|
## Removing an Image
|
||||||
In the event you run a verifiable build from the CLI and exit prematurely,
|
|
||||||
it's possible the docker image may still be building in the background.
|
In the event you run a verifiable build from the CLI and exit prematurely,
|
||||||
|
it's possible the docker image may still be building in the background.
|
||||||
|
|
||||||
To remove, run
|
To remove, run
|
||||||
|
|
||||||
```
|
```shell
|
||||||
docker rm -f anchor-program
|
docker rm -f anchor-program
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
title: Getting started
|
||||||
|
pageTitle: Anchor - Solana Sealevel Framework
|
||||||
|
description: Anchor is a framework for Solana's Sealevel runtime providing several convenient developer tools for writing smart contracts.
|
||||||
|
---
|
||||||
|
|
||||||
|
Anchor is a framework for Solana's Sealevel runtime providing several convenient developer tools for writing smart contracts. {% .lead %}
|
||||||
|
|
||||||
|
{% link-grid %}
|
||||||
|
|
||||||
|
{% link-grid-link title="Installation" icon="installation" href="/docs/installation" description="Step-by-step guides to setting up your system and installing Anchor." /%}
|
||||||
|
|
||||||
|
{% link-grid-link title="Intro to Solana" icon="presets" href="/docs/intro-to-solana" description="Brief intro to programming on Solana." /%}
|
||||||
|
|
||||||
|
{% link-grid-link title="High-Level Overview" icon="plugins" href="/docs/high-level-overview" description="High-Level Overview of an Anchor program." /%}
|
||||||
|
|
||||||
|
{% link-grid-link title="CLI reference" icon="theming" href="/docs/cli" description="A CLI is provided to support building and managing an Anchor workspace." /%}
|
||||||
|
|
||||||
|
{% /link-grid %}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What is Anchor
|
||||||
|
|
||||||
|
Anchor is a framework for quickly building secure Solana programs.
|
||||||
|
|
||||||
|
With Anchor you can build programs quickly because it writes various boilerplate for you such as (de)serialization of accounts and instruction data.
|
||||||
|
|
||||||
|
You can build secure programs more easily because Anchor handles certain security checks for you. On top of that, it allows you to succinctly define additional checks and keep them separate from your business logic.
|
||||||
|
|
||||||
|
Both of these aspects mean that instead of working on the tedious parts of raw Solana programs, you can spend more time working on what matters most, your product.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
Anchor's official documentation is split up into multiple parts, namely the guide, which is what you are reading right now and the references.
|
||||||
|
|
||||||
|
There are three references. One for the [core library](https://docs.rs/anchor-lang/latest/anchor_lang/) and one for each official client library ([typescript](https://project-serum.github.io/anchor/ts/index.html) and [rust](https://docs.rs/anchor-client/latest/anchor_client/)). These references are close to the code and detailed. If you know what you are looking for and want to understand how it works more deeply, you'll find explanations there.
|
||||||
|
|
||||||
|
However, if you're new to anchor, you need to know what anchor has to offer before you can even try to understand it more deeply. That's what this guide is for. Its purpose is to introduce you to anchor, to help you become familiar with it. It teaches you what features are available in Anchor so you can explore them yourself in detail using the references.
|
||||||
|
|
||||||
|
### Twitter
|
||||||
|
|
||||||
|
[Stay up to date on Twitter](https://twitter.com/anchorlang)
|
||||||
|
|
||||||
|
### Join the community
|
||||||
|
|
||||||
|
[Discord Invitation](http://discord.gg/ZCHmqvXgDw)
|
|
@ -0,0 +1,503 @@
|
||||||
|
/*! @docsearch/css 3.1.0 | MIT License | © Algolia, Inc. and contributors | https://docsearch.algolia.com */
|
||||||
|
:root {
|
||||||
|
--docsearch-primary-color: red;
|
||||||
|
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||||
|
--docsearch-muted-color: theme('colors.slate.500');
|
||||||
|
--docsearch-emphasis-color: theme('colors.slate.900');
|
||||||
|
--docsearch-logo-color: #5468ff;
|
||||||
|
--docsearch-modal-width: 35rem;
|
||||||
|
--docsearch-modal-height: 37.5rem;
|
||||||
|
--docsearch-modal-background: theme('colors.white');
|
||||||
|
--docsearch-modal-shadow: theme('boxShadow.xl');
|
||||||
|
--docsearch-searchbox-height: 3rem;
|
||||||
|
--docsearch-hit-color: theme('colors.slate.700');
|
||||||
|
--docsearch-hit-active-color: theme('colors.sky.600');
|
||||||
|
--docsearch-hit-active-background: theme('colors.slate.100');
|
||||||
|
--docsearch-footer-height: 3rem;
|
||||||
|
--docsearch-border-color: theme('colors.slate.200');
|
||||||
|
--docsearch-input-color: theme('colors.slate.900');
|
||||||
|
--docsearch-heading-color: theme('colors.slate.900');
|
||||||
|
--docsearch-key-background: theme('colors.slate.100');
|
||||||
|
--docsearch-key-hover-background: theme('colors.slate.200');
|
||||||
|
--docsearch-key-color: theme('colors.slate.500');
|
||||||
|
--docsearch-action-color: theme('colors.slate.400');
|
||||||
|
--docsearch-action-active-background: theme('colors.slate.200');
|
||||||
|
--docsearch-loading-background: theme('colors.slate.400');
|
||||||
|
--docsearch-loading-foreground: theme('colors.slate.900');
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--docsearch-highlight-color: var(--docsearch-primary-color);
|
||||||
|
--docsearch-muted-color: theme('colors.slate.400');
|
||||||
|
--docsearch-emphasis-color: theme('colors.white');
|
||||||
|
--docsearch-logo-color: theme('colors.slate.300');
|
||||||
|
--docsearch-modal-background: theme('colors.slate.800');
|
||||||
|
--docsearch-modal-shadow: 0 0 0 1px theme('colors.slate.700'),
|
||||||
|
theme('boxShadow.xl');
|
||||||
|
--docsearch-hit-color: theme('colors.slate.300');
|
||||||
|
--docsearch-hit-active-color: theme('colors.sky.400');
|
||||||
|
--docsearch-hit-active-background: rgb(51 65 85 / 0.3);
|
||||||
|
--docsearch-border-color: rgb(148 163 184 / 0.1);
|
||||||
|
--docsearch-heading-color: theme('colors.white');
|
||||||
|
--docsearch-key-background: rgb(51 65 85 / 0.4);
|
||||||
|
--docsearch-key-hover-background: rgb(51 65 85 / 0.8);
|
||||||
|
--docsearch-key-color: theme('colors.slate.400');
|
||||||
|
--docsearch-input-color: theme('colors.white');
|
||||||
|
--docsearch-action-color: theme('colors.slate.500');
|
||||||
|
--docsearch-action-active-background: theme('colors.slate.700');
|
||||||
|
--docsearch-loading-background: theme('colors.slate.500');
|
||||||
|
--docsearch-loading-foreground: theme('colors.white');
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch--active {
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Container {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 200;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
height: calc(var(--docsearch-vh, 1vh) * 100);
|
||||||
|
background-color: rgb(15 23 42 / 0.5);
|
||||||
|
backdrop-filter: blur(theme('backdropBlur.DEFAULT'));
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Link {
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
color: var(--docsearch-highlight-color);
|
||||||
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Modal {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
height: calc(var(--docsearch-vh, 1vh) * 100);
|
||||||
|
background: var(--docsearch-modal-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-SearchBar {
|
||||||
|
display: flex;
|
||||||
|
height: var(--docsearch-searchbox-height);
|
||||||
|
border-bottom: 1px solid var(--docsearch-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Form {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Input {
|
||||||
|
appearance: none;
|
||||||
|
color: var(--docsearch-input-color);
|
||||||
|
flex: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0 1rem 0 3rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Input::placeholder {
|
||||||
|
color: theme('colors.slate.400');
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Input::-webkit-search-cancel-button,
|
||||||
|
.DocSearch-Input::-webkit-search-decoration,
|
||||||
|
.DocSearch-Input::-webkit-search-results-button,
|
||||||
|
.DocSearch-Input::-webkit-search-results-decoration {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Reset {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Container--Stalled .DocSearch-MagnifierLabel,
|
||||||
|
.DocSearch-LoadingIndicator {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.875rem;
|
||||||
|
left: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-LoadingIndicator svg {
|
||||||
|
height: 1rem;
|
||||||
|
width: 1rem;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-LoadingIndicator path,
|
||||||
|
.DocSearch-LoadingIndicator circle {
|
||||||
|
vector-effect: non-scaling-stroke;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-LoadingIndicator circle {
|
||||||
|
stroke: var(--docsearch-loading-background);
|
||||||
|
stroke-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-LoadingIndicator path {
|
||||||
|
stroke: var(--docsearch-loading-foreground);
|
||||||
|
stroke-opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-MagnifierLabel {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.875rem;
|
||||||
|
left: 1rem;
|
||||||
|
pointer-events: none;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z' fill='%2394A3B8'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .DocSearch-MagnifierLabel {
|
||||||
|
background: url("data:image/svg+xml,%3Csvg fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.293 17.707a1 1 0 0 0 1.414-1.414l-1.414 1.414ZM9 14a5 5 0 0 1-5-5H2a7 7 0 0 0 7 7v-2ZM4 9a5 5 0 0 1 5-5V2a7 7 0 0 0-7 7h2Zm5-5a5 5 0 0 1 5 5h2a7 7 0 0 0-7-7v2Zm8.707 12.293-3.757-3.757-1.414 1.414 3.757 3.757 1.414-1.414ZM14 9a4.98 4.98 0 0 1-1.464 3.536l1.414 1.414A6.98 6.98 0 0 0 16 9h-2Zm-1.464 3.536A4.98 4.98 0 0 1 9 14v2a6.98 6.98 0 0 0 4.95-2.05l-1.414-1.414Z' fill='%2364748b'/%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-MagnifierLabel svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Dropdown {
|
||||||
|
height: 100%;
|
||||||
|
max-height: calc(
|
||||||
|
var(--docsearch-vh, 1vh) * 100 - var(--docsearch-searchbox-height) -
|
||||||
|
var(--docsearch-footer-height)
|
||||||
|
);
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-y: overlay;
|
||||||
|
padding: 1rem 0.5rem;
|
||||||
|
scrollbar-color: var(--docsearch-muted-color)
|
||||||
|
var(--docsearch-modal-background);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Dropdown::-webkit-scrollbar {
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Dropdown::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Dropdown::-webkit-scrollbar-thumb {
|
||||||
|
background-color: var(--docsearch-muted-color);
|
||||||
|
border: 3px solid var(--docsearch-modal-background);
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-StartScreen {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Help,
|
||||||
|
.DocSearch-Label {
|
||||||
|
color: var(--docsearch-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Help {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Title {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--docsearch-muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Title strong {
|
||||||
|
color: var(--docsearch-emphasis-color);
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Logo a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Logo svg {
|
||||||
|
color: var(--docsearch-logo-color);
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hits + .DocSearch-Hits {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hits mark {
|
||||||
|
background: none;
|
||||||
|
color: var(--docsearch-hit-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-HitsFooter {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit--deleting,
|
||||||
|
.DocSearch-Hit--favoriting {
|
||||||
|
transform: scale(1);
|
||||||
|
transition: all 0.0001s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit a {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: theme('borderRadius.lg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-source,
|
||||||
|
.DocSearch-NoResults .DocSearch-Help {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-family: theme('fontFamily.display');
|
||||||
|
color: var(--docsearch-heading-color);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-Tree {
|
||||||
|
width: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-Tree * {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit[aria-selected='true'] a,
|
||||||
|
.DocSearch-Prefill:hover,
|
||||||
|
.DocSearch-Prefill:focus {
|
||||||
|
background-color: var(--docsearch-hit-active-background);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit[aria-selected='true'] mark {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-Container,
|
||||||
|
.DocSearch-Prefill {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
color: var(--docsearch-hit-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-action {
|
||||||
|
color: var(--docsearch-action-color);
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-action + .DocSearch-Hit-action {
|
||||||
|
margin-left: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-action-button {
|
||||||
|
border-radius: 50%;
|
||||||
|
color: inherit;
|
||||||
|
height: 1.5rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-action svg {
|
||||||
|
height: 1.125rem;
|
||||||
|
width: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.DocSearch-Hit-Select-Icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-Select-Icon {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-action-button:focus,
|
||||||
|
.DocSearch-Hit-action-button:hover {
|
||||||
|
background: var(--docsearch-action-active-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-content-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-title,
|
||||||
|
.DocSearch-Prefill {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit-path {
|
||||||
|
color: var(--docsearch-muted-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-path,
|
||||||
|
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-text,
|
||||||
|
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-title,
|
||||||
|
.DocSearch-Hit[aria-selected='true'] mark,
|
||||||
|
.DocSearch-Prefill:hover,
|
||||||
|
.DocSearch-Prefill:focus {
|
||||||
|
color: var(--docsearch-hit-active-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-NoResults .DocSearch-Screen-Icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-NoResults .DocSearch-Title {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem 1rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-NoResults-Prefill-List {
|
||||||
|
margin: 0 -0.5rem;
|
||||||
|
padding: 1rem 0.5rem 0;
|
||||||
|
border-top: 1px solid var(--docsearch-border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Prefill {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: theme('borderRadius.lg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--docsearch-footer-height);
|
||||||
|
z-index: 300;
|
||||||
|
border-top: 1px solid var(--docsearch-border-color);
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Commands {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Cancel {
|
||||||
|
background: var(--docsearch-key-background);
|
||||||
|
color: var(--docsearch-key-color);
|
||||||
|
align-self: center;
|
||||||
|
flex: none;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
user-select: none;
|
||||||
|
border-radius: theme('borderRadius.md');
|
||||||
|
padding: 0 0.375rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Cancel:hover {
|
||||||
|
background: var(--docsearch-key-hover-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen sm {
|
||||||
|
.DocSearch-Container {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Modal {
|
||||||
|
height: auto;
|
||||||
|
border-radius: theme('borderRadius.xl');
|
||||||
|
box-shadow: var(--docsearch-modal-shadow);
|
||||||
|
margin: 4rem auto auto;
|
||||||
|
width: auto;
|
||||||
|
max-width: var(--docsearch-modal-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Input {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Footer {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Commands {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Commands li {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Commands li:not(:last-of-type) {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Commands-Key {
|
||||||
|
background: var(--docsearch-key-background);
|
||||||
|
color: var(--docsearch-key-color);
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: theme('borderRadius.md');
|
||||||
|
margin-right: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DocSearch-Dropdown {
|
||||||
|
height: auto;
|
||||||
|
max-height: calc(
|
||||||
|
var(--docsearch-modal-height) - var(--docsearch-searchbox-height) -
|
||||||
|
var(--docsearch-footer-height)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lexend';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/fonts/lexend.woff2) format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: block;
|
||||||
|
font-style: normal;
|
||||||
|
font-named-instance: 'Regular';
|
||||||
|
src: url('/fonts/Inter-roman.var.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: block;
|
||||||
|
font-style: italic;
|
||||||
|
font-named-instance: 'Italic';
|
||||||
|
src: url('/fonts/Inter-italic.var.woff2') format('woff2');
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
pre[class*='language-'] {
|
||||||
|
color: theme('colors.slate.50');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.tag,
|
||||||
|
.token.class-name,
|
||||||
|
.token.selector,
|
||||||
|
.token.selector .class,
|
||||||
|
.token.selector.class,
|
||||||
|
.token.function {
|
||||||
|
color: theme('colors.pink.400');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-name,
|
||||||
|
.token.keyword,
|
||||||
|
.token.rule,
|
||||||
|
.token.pseudo-class,
|
||||||
|
.token.important {
|
||||||
|
color: theme('colors.slate.300');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.module {
|
||||||
|
color: theme('colors.pink.400');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.attr-value,
|
||||||
|
.token.class,
|
||||||
|
.token.string,
|
||||||
|
.token.property {
|
||||||
|
color: theme('colors.sky.300');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation,
|
||||||
|
.token.attr-equals {
|
||||||
|
color: theme('colors.slate.500');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.unit,
|
||||||
|
.language-css .token.function {
|
||||||
|
color: theme('colors.teal.200');
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.comment,
|
||||||
|
.token.operator,
|
||||||
|
.token.combinator {
|
||||||
|
color: theme('colors.slate.400');
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import './fonts.css';
|
||||||
|
@import './docsearch.css';
|
||||||
|
@import './prism.css';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
|
@ -1,190 +0,0 @@
|
||||||
# A Minimal Example
|
|
||||||
|
|
||||||
Here, we introduce Anchor's core syntax elements and project workflow. This tutorial assumes all
|
|
||||||
[prerequisites](../getting-started/installation.md) are installed.
|
|
||||||
|
|
||||||
## Clone the Repo
|
|
||||||
|
|
||||||
To get started, clone the repo.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/coral-xyz/anchor
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, checkout the tagged branch of the same version of the anchor cli you have installed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout tags/<version>
|
|
||||||
```
|
|
||||||
|
|
||||||
Change directories to the [example](https://github.com/coral-xyz/anchor/tree/master/examples/tutorial/basic-0).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd anchor/examples/tutorial/basic-0
|
|
||||||
```
|
|
||||||
|
|
||||||
And install any additional JavaScript dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Starting a Localnet
|
|
||||||
|
|
||||||
In a separate terminal, start a local network. If you're running solana
|
|
||||||
for the first time, generate a wallet.
|
|
||||||
|
|
||||||
```
|
|
||||||
solana-keygen new
|
|
||||||
```
|
|
||||||
|
|
||||||
Then run
|
|
||||||
|
|
||||||
```
|
|
||||||
solana-test-validator
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, shut it down.
|
|
||||||
|
|
||||||
The test validator will be used when testing Anchor programs. Make sure to turn off the validator before you begin testing Anchor programs.
|
|
||||||
|
|
||||||
::: details
|
|
||||||
As you'll see later, starting a localnet manually like this is not necessary when testing with Anchor,
|
|
||||||
but is done for educational purposes in this tutorial.
|
|
||||||
:::
|
|
||||||
|
|
||||||
## Defining a Program
|
|
||||||
|
|
||||||
We define the minimum viable program as follows.
|
|
||||||
|
|
||||||
<<< @/../examples/tutorial/basic-0/programs/basic-0/src/lib.rs
|
|
||||||
|
|
||||||
* `#[program]` First, notice that a program is defined with the `#[program]` attribute, where each
|
|
||||||
inner method defines an RPC request handler, or, in Solana parlance, an "instruction"
|
|
||||||
handler. These handlers are the entrypoints to your program that clients may invoke, as
|
|
||||||
we will see soon.
|
|
||||||
|
|
||||||
* `Context<Initialize>` The first parameter of _every_ RPC handler is the `Context` struct, which is a simple
|
|
||||||
container for the currently executing `program_id` generic over
|
|
||||||
`Accounts`--here, the `Initialize` struct.
|
|
||||||
|
|
||||||
* `#[derive(Accounts)]` The `Accounts` derive macro marks a struct containing all the accounts that must be
|
|
||||||
specified for a given instruction. To understand Accounts on Solana, see the
|
|
||||||
[docs](https://docs.solana.com/developing/programming-model/accounts).
|
|
||||||
In subsequent tutorials, we'll demonstrate how an `Accounts` struct can be used to
|
|
||||||
specify constraints on accounts given to your program. Since this example doesn't touch any
|
|
||||||
accounts, we skip this (important) detail.
|
|
||||||
|
|
||||||
## Building and Emitting an IDL
|
|
||||||
|
|
||||||
After creating a program, you can use the `anchor` CLI to build and emit an IDL, from which clients
|
|
||||||
can be generated.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
anchor build
|
|
||||||
```
|
|
||||||
|
|
||||||
::: details
|
|
||||||
The `build` command is a convenience combining two steps.
|
|
||||||
|
|
||||||
1) `cargo build-bpf`
|
|
||||||
2) `anchor idl parse -f program/src/lib.rs -o target/idl/basic_0.json`.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Once run, you should see your build artifacts, as usual, in your `target/` directory. Additionally,
|
|
||||||
a `target/idl/basic_0.json` file is created. Inspecting its contents you should see
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"version": "0.1.0",
|
|
||||||
"name": "basic_0",
|
|
||||||
"instructions": [
|
|
||||||
{
|
|
||||||
"name": "initialize",
|
|
||||||
"accounts": [],
|
|
||||||
"args": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
From this file a client can be generated. Note that this file is created by parsing the `src/lib.rs`
|
|
||||||
file in your program's crate.
|
|
||||||
|
|
||||||
::: tip
|
|
||||||
If you've developed on Ethereum, the IDL is analogous to the `abi.json`.
|
|
||||||
:::
|
|
||||||
|
|
||||||
The `build` command also generates a random new keypair in the `target/` directory (if there's not one already) whose public key will be the address of your program once deployed. You can obtain the address by running `anchor keys list`.
|
|
||||||
Make sure that the public key inside your `lib.rs` (the argument to `declare_id!`) file and your `Anchor.toml` matches the one returned by `anchor keys list`. Then run `build` again to include the `lib.rs` changes in the build.
|
|
||||||
|
|
||||||
## Deploying
|
|
||||||
|
|
||||||
Once built, we can deploy the program by running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
anchor deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
## Generating a Client
|
|
||||||
|
|
||||||
Now that we've built a program, deployed it to a local cluster, and generated an IDL,
|
|
||||||
we can use the IDL to generate a client to speak to our on-chain program. For example,
|
|
||||||
see [client.js](https://github.com/coral-xyz/anchor/tree/master/examples/tutorial/basic-0/client.js).
|
|
||||||
|
|
||||||
<<< @/../examples/tutorial/basic-0/client.js#main
|
|
||||||
|
|
||||||
Notice how we dynamically created the `initialize` method under
|
|
||||||
the `rpc` namespace.
|
|
||||||
|
|
||||||
Now, make sure to plugin your program's address into `<YOUR-PROGRAM-ID>` (a mild
|
|
||||||
annoyance that we'll address next). In order to run the client, you'll also need the path
|
|
||||||
to your wallet's keypair you generated when you ran `solana-keygen new`; you can find it
|
|
||||||
by running
|
|
||||||
|
|
||||||
```bash
|
|
||||||
solana config get keypair
|
|
||||||
```
|
|
||||||
|
|
||||||
Once you've got it, run the client with the environment variable `ANCHOR_WALLET` set to
|
|
||||||
that path, e.g.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ANCHOR_WALLET=<YOUR-KEYPAIR-PATH> node client.js
|
|
||||||
```
|
|
||||||
|
|
||||||
You just successfully created a client and executed a transaction on your localnet.
|
|
||||||
|
|
||||||
## Workspaces
|
|
||||||
|
|
||||||
So far we've seen the basics of how to create, deploy, and make RPCs to a program, but
|
|
||||||
deploying a program, copy and pasting the address, and explicitly reading
|
|
||||||
an IDL is all a bit tedious, and can easily get out of hand the more tests and the more
|
|
||||||
programs you have. For this reason, we introduce the concept of a workspace.
|
|
||||||
|
|
||||||
Inspecting [tests/basic-0.js](https://github.com/coral-xyz/anchor/tree/master/examples/tutorial/basic-0/tests/basic-0.js), we see the above example can be reduced to
|
|
||||||
|
|
||||||
<<< @/../examples/tutorial/basic-0/tests/basic-0.js#code
|
|
||||||
|
|
||||||
The `workspace` namespace provides access to all programs in the local project and is
|
|
||||||
automatically updated to reflect the latest deployment, making it easy to change
|
|
||||||
your program, update your JavaScript, and run your tests in a fast feedback loop.
|
|
||||||
|
|
||||||
::: tip NOTE
|
|
||||||
For now, the workspace feature is only available when running the `anchor test` command,
|
|
||||||
which will automatically `build`, `deploy`, and `test` all programs against a localnet
|
|
||||||
in one command.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Finally, we can run the test. Don't forget to kill the local validator started earlier.
|
|
||||||
We won't need to start one manually for any future tutorials.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
anchor test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
We've introduced the basic syntax of writing programs in Anchor along with a productive
|
|
||||||
workflow for building and testing. However, programs aren't all that interesting without
|
|
||||||
interacting with persistent state. We'll cover that next.
|
|
|
@ -1,106 +0,0 @@
|
||||||
# Arguments and Accounts
|
|
||||||
|
|
||||||
This tutorial covers the basics of creating and mutating accounts using Anchor.
|
|
||||||
It's recommended to read [Tutorial 0](./tutorial-0.md) first, as this tutorial will
|
|
||||||
build on top of it.
|
|
||||||
|
|
||||||
## Clone the Repo
|
|
||||||
|
|
||||||
To get started, clone the repo.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/coral-xyz/anchor
|
|
||||||
```
|
|
||||||
|
|
||||||
Change directories to the [example](https://github.com/coral-xyz/anchor/tree/master/examples/tutorial/basic-1).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd anchor/examples/tutorial/basic-1
|
|
||||||
```
|
|
||||||
|
|
||||||
And install any additional JavaScript dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Defining a Program
|
|
||||||
|
|
||||||
We define our program as follows
|
|
||||||
|
|
||||||
<<< @/../examples/tutorial/basic-1/programs/basic-1/src/lib.rs
|
|
||||||
|
|
||||||
Some new syntax elements are introduced here.
|
|
||||||
|
|
||||||
### `initialize` instruction
|
|
||||||
|
|
||||||
First, let's start with the initialize instruction. Notice the `data` argument passed into the program. This argument and any other valid
|
|
||||||
Rust types can be passed to the instruction to define inputs to the program.
|
|
||||||
|
|
||||||
Additionally,
|
|
||||||
notice how we take a mutable reference to `my_account` and assign the `data` to it. This leads us to
|
|
||||||
the `Initialize` struct, deriving `Accounts`. There are two things to notice about `Initialize`.
|
|
||||||
|
|
||||||
1. The `my_account` field is of type `Account<'info, MyAccount>` and the deserialized data structure is `MyAccount`.
|
|
||||||
2. The `my_account` field is marked with the `init` attribute. This will create a new
|
|
||||||
account owned by the current program, zero initialized. When using `init`, one must also provide
|
|
||||||
`payer`, which will fund the account creation, `space`, which defines how large the account should be,
|
|
||||||
and the `system_program`, which is required by the runtime for creating the account.
|
|
||||||
|
|
||||||
::: details
|
|
||||||
All accounts created with Anchor are laid out as follows: `8-byte-discriminator || borsh
|
|
||||||
serialized data`. The 8-byte-discriminator is created from the first 8 bytes of the
|
|
||||||
`Sha256` hash of the account's type--using the example above, `sha256("account:MyAccount")[..8]`.
|
|
||||||
The `account:` is a fixed prefix.
|
|
||||||
|
|
||||||
Importantly, this allows a program to know for certain an account is indeed of a given type.
|
|
||||||
Without it, a program would be vulnerable to account injection attacks, where a malicious user
|
|
||||||
specifies an account of an unexpected type, causing the program to do unexpected things.
|
|
||||||
|
|
||||||
On account creation, this 8-byte discriminator doesn't exist, since the account storage is
|
|
||||||
zeroed. The first time an Anchor program mutates an account, this discriminator is prepended
|
|
||||||
to the account storage array and all subsequent accesses to the account (not decorated with
|
|
||||||
`#[account(init)]`) will check for this discriminator.
|
|
||||||
:::
|
|
||||||
|
|
||||||
### `update` instruction
|
|
||||||
|
|
||||||
Similarly, the `Update` accounts struct is marked with the `#[account(mut)]` attribute.
|
|
||||||
Marking an account as `mut` persists any changes made upon exiting the program.
|
|
||||||
|
|
||||||
Here we've covered the basics of how to interact with accounts. In a later tutorial,
|
|
||||||
we'll delve more deeply into deriving `Accounts`, but for now, just know
|
|
||||||
you must mark an account `init` when using it for the first time and `mut`
|
|
||||||
for persisting changes.
|
|
||||||
|
|
||||||
## Creating and Initializing Accounts
|
|
||||||
|
|
||||||
We can interact with the program as follows.
|
|
||||||
|
|
||||||
<<< @/../examples/tutorial/basic-1/tests/basic-1.js#code-simplified
|
|
||||||
|
|
||||||
The last element passed into the method is common amongst all dynamically generated
|
|
||||||
methods on the `rpc` namespace, containing several options for a transaction. Here,
|
|
||||||
we specify the `accounts` field, an object of all the addresses the transaction
|
|
||||||
needs to touch, and the `signers` array of all `Signer` objects needed to sign the
|
|
||||||
transaction. Because `myAccount` is being created, the Solana runtime requires it
|
|
||||||
to sign the transaction.
|
|
||||||
|
|
||||||
::: details
|
|
||||||
If you've developed on Solana before, you might notice two things 1) the ordering of the accounts doesn't
|
|
||||||
matter and 2) the `isWritable` and `isSigner`
|
|
||||||
options are not specified on the account anywhere. In both cases, the framework takes care
|
|
||||||
of these details for you, by reading the IDL.
|
|
||||||
:::
|
|
||||||
|
|
||||||
As before, we can run the example tests.
|
|
||||||
|
|
||||||
```
|
|
||||||
anchor test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
We've covered all the basics of developing applications using Anchor. However, we've
|
|
||||||
left out one important aspect to ensure the security of our programs--validating input
|
|
||||||
and access control. We'll cover that next.
|
|
|
@ -1,77 +0,0 @@
|
||||||
# Account Constraints and Access Control
|
|
||||||
|
|
||||||
This tutorial covers how to specify constraints and access control on accounts, a problem
|
|
||||||
somewhat unique to the parallel nature of Solana.
|
|
||||||
|
|
||||||
On Solana, a transaction must specify all accounts required for execution. And because an untrusted client specifies those accounts, a program must responsibly validate all such accounts are what the client claims they are--in addition to any instruction specific access control the program needs to do.
|
|
||||||
|
|
||||||
For example, you could imagine easily writing a faulty token program that forgets to check if the **signer** of a transaction claiming to be the **owner** of a Token `Account` actually matches the **owner** on that account. Furthermore, imagine what might happen if the program expects a `Mint` account but a malicious user gives a token `Account`.
|
|
||||||
|
|
||||||
To address these problems, Anchor provides several types, traits, and macros. It's easiest to understand by seeing how they're used in an example, but a couple include
|
|
||||||
|
|
||||||
- [Accounts](https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html): derive macro implementing the `Accounts` [trait](https://docs.rs/anchor-lang/latest/anchor_lang/trait.Accounts.html), allowing a struct to transform
|
|
||||||
from the untrusted `&[AccountInfo]` slice given to a Solana program into a validated struct
|
|
||||||
of deserialized account types.
|
|
||||||
- [#[account]](https://docs.rs/anchor-lang/latest/anchor_lang/attr.account.html): attribute macro implementing [AccountSerialize](https://docs.rs/anchor-lang/latest/anchor_lang/trait.AccountSerialize.html) and [AccountDeserialize](https://docs.rs/anchor-lang/latest/anchor_lang/trait.AnchorDeserialize.html), automatically prepending a unique 8 byte discriminator to the account array. The discriminator is defined by the first 8 bytes of the `Sha256` hash of the account's Rust identifier--i.e., the struct type name--and ensures no account can be substituted for another.
|
|
||||||
- [Account](https://docs.rs/anchor-lang/latest/anchor_lang/accounts/account/struct.Account.html): a wrapper type for a deserialized account implementing `AccountDeserialize`. Using this type within an `Accounts` struct will ensure the account is **owned** by the address defined by `declare_id!` where the inner account was defined.
|
|
||||||
|
|
||||||
With the above, we can define preconditions for any instruction handler expecting a certain set of
|
|
||||||
accounts, allowing us to more easily reason about the security of our programs.
|
|
||||||
|
|
||||||
## Clone the Repo
|
|
||||||
|
|
||||||
To get started, clone the repo.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/coral-xyz/anchor
|
|
||||||
```
|
|
||||||
|
|
||||||
Change directories to the [example](https://github.com/coral-xyz/anchor/tree/master/examples/tutorial/basic-2).
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd anchor/examples/tutorial/basic-2
|
|
||||||
```
|
|
||||||
|
|
||||||
And install any additional JavaScript dependencies:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn install
|
|
||||||
```
|
|
||||||
|
|
||||||
## Defining a Program
|
|
||||||
|
|
||||||
Here we have a simple **Counter** program, where anyone can create a counter, but only the assigned
|
|
||||||
**authority** can increment it.
|
|
||||||
|
|
||||||
<<< @/../examples/tutorial/basic-2/programs/basic-2/src/lib.rs
|
|
||||||
|
|
||||||
If you've gone through the previous tutorials the `create` instruction should be straightforward.
|
|
||||||
Let's focus on the `increment` instruction, specifically the `Increment` struct deriving
|
|
||||||
`Accounts`.
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[derive(Accounts)]
|
|
||||||
pub struct Increment<'info> {
|
|
||||||
#[account(mut, has_one = authority)]
|
|
||||||
pub counter: Account<'info, Counter>,
|
|
||||||
pub authority: Signer<'info>,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Here, a couple `#[account(..)]` attributes are used.
|
|
||||||
|
|
||||||
- `mut`: tells the program to persist all changes to the account.
|
|
||||||
- `has_one`: enforces the constraint that `Increment.counter.authority == Increment.authority.key`.
|
|
||||||
|
|
||||||
Another new concept here is the `Signer` type. This enforces the constraint that the `authority`
|
|
||||||
account **signed** the transaction. However, anchor doesn't fetch the data on that account.
|
|
||||||
|
|
||||||
If any of these constraints do not hold, then the `increment` instruction will never be executed.
|
|
||||||
This allows us to completely separate account validation from our program's business logic, allowing us
|
|
||||||
to reason about each concern more easily. For more, see the full [list](https://docs.rs/anchor-lang/latest/anchor_lang/derive.Accounts.html) of account constraints.
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
We've covered the basics for writing a single program using Anchor on Solana. But the power of
|
|
||||||
blockchains come not from a single program, but from combining multiple _composable_ programs
|
|
||||||
(buzzword...check). Next, we'll see how to call one program from another.
|
|