Solidity 也能写 Solana 链程!完整实战 Solang + Anchor 全攻略

·

想在 Solana 上用你最熟的 Solidity 写程序?跟着本文 30 分钟搭建首个 Scoreboard 打分应用,从本地编译、测试到 主网部署 一步到位!

约 16 分钟完成阅读与实操

本文你将掌握

准备清单

⚠️ Anchor 若要支持新的 Solidity 语法,需安装 main 分支版本

cargo install --git https://github.com/coral-xyz/anchor anchor-cli --locked --force

验证版本

solana --version
anchor --version

Solang 速通:从 EVM 到 SVM

核心关键词:Solang、账户模型、Program Derived Address(PDA)、IDL

账户模型

示例起始定义

@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract scoreboard { ... }

Program Derived Address(PDA)

通过 种子 + bump 寻址,唯一且确定。使用注解:

IDL 自动生成

Anchor 会在 ./target/types/*.ts 里生成 IDL,等同 ABI,前端、测试一应俱全。

创建 Scoreboard 程序(实战)

1. 初始化项目

anchor init scoreboard --solidity
cd scoreboard

确认 Anchor.toml 钱包与 Localnet

[provider]
cluster = "Localnet"
wallet = "/path/to/id.json"

2. 定义数据结构

solidity/scoreboard.sol 头部写入:

@program_id("F1ipperKF9EfD821ZbbYjS319LXYiBmjhzkkf5a26rC")
contract scoreboard {
    struct UserScore {
        address player;
        uint64 currentScore;
        uint64 highestScore;
        bytes1 bump;
    }

    UserScore private accountData;
    ...
}

3. 构造函数 – 创建 PDA

紧跟结构体后面:

@payer(payer)
@seed("seed")
constructor(
    @seed bytes _payer,
    @bump bytes1 _bump,
    address player
) {
    print("New UserScore account initialized");
    accountData = UserScore(player, 0, 0, _bump);
}

4. 业务方法 – 增改查

function addPoints(uint8 numPoints) public {
    require(numPoints > 0 && numPoints < 100, "INVALID_POINTS");
    accountData.currentScore += numPoints;
    if (accountData.currentScore > accountData.highestScore) {
        accountData.highestScore = accountData.currentScore;
    }
}

function resetScore() public {
    accountData.currentScore = 0;
}

function getCurrentScore() public view returns (uint64) {
    return accountData.currentScore;
}

function getHighScore() public view returns (uint64) {
    return accountData.highestScore;
}

一键编译

anchor build

成功后会看到 LLVM IR + IDL 已生成

测试用例全覆盖

替换 tests/scoreboard.ts 为完整脚本:

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
import { expect } from "chai";
import { Scoreboard } from "../target/types/scoreboard";

const randomPoints = (min = 1, max = 100) =>
  Math.floor(Math.random() * (max - min + 1)) + min;

describe("Scoreboard", () => {
  const provider = anchor.AnchorProvider.env();
  anchor.setProvider(provider);
  const program = anchor.workspace.Scoreboard as Program<Scoreboard>;
  const wallet = provider.wallet;
  const seed = Buffer.from("seed");
  const [dataAccount, bump] = PublicKey.findProgramAddressSync(
    [seed, wallet.publicKey.toBuffer()],
    program.programId
  );

  it("1. 初始化 PDA", async () => {
    await program.methods
      .new(wallet.publicKey.toBuffer(), [bump], wallet.publicKey)
      .accounts({ dataAccount })
      .rpc();
    const score = await program.methods
      .getCurrentScore()
      .accounts({ dataAccount })
      .view();
    expect(score.toNumber()).to.eq(0);
  });

  it("2. 增加分数", async () => {
    const points = randomPoints();
    await program.methods
      .addPoints(points)
      .accounts({ dataAccount })
      .rpc();
    const score = await program.methods
      .getCurrentScore()
      .accounts({ dataAccount })
      .view();
    expect(score.toNumber()).to.eq(points);
  });

  it("3. 重置分数,高分保留", async () => {
    const prep = randomPoints();
    await program.methods
      .addPoints(prep)
      .accounts({ dataAccount })
      .rpc();

    await program.methods
      .resetScore()
      .accounts({ dataAccount })
      .rpc();

    const current = await program.methods
      .getCurrentScore()
      .accounts({ dataAccount })
      .view();
    const highest = await program.methods
      .getHighScore()
      .accounts({ dataAccount })
      .view();
    expect(current.toNumber()).to.eq(0);
    expect(highest.toNumber()).to.eq(prep);
  });
});

运行测试

anchor test --skip-local-validator

3 个用例全部绿钩 ✅


常见问题(FAQ)

Q1:Anchor --solidity 命令报错怎么办?
A:确保 Anchor 来自 main 分支;若已全局安装旧版本,先 cargo uninstall anchor-cli 再按上文装主分支。

Q2:为什么测试打印“failed to send transaction”?
A:本地 validator 未启动。可手动 solana-test-validator 或去掉 --skip-local-validator 让 Anchor 自动启动。

Q3:合约升级会更改 program ID 吗?
A:Solana 程序 不原生支持升级。需通过 BPF Loader 或使用 Squads / Goki SDK 实现“upgrade authority”,但普通项目建议重新部署并更新前端配置。

Q4:如何在前端调用合约?
A:使用 Anchor 生成的 IDL + @coral-xyz/anchor;下载后在 React/Vite 中直接 useProgramuseAccount hooks 就能跑。

Q5:PDA 种子可以修改吗?
A:种子生成后就 固定为该地址。若想换参数,需要新建不同种子的 PDA 或将数据迁移。

Q6:部署主网需要多少 SOL?
A:合约大小决定 rent exemption,几 KB 的小程序通常 0.04–0.08 SOL 即可。


将程序推上 主网

只差 3 步:

  1. 准备主网 HTTP RPC
    👉 5 秒内获得的超快 Solana 节点地址,免运维即可直连主网
  2. 更新程序 ID

    solana program deploy ./target/deploy/scoreboard.so -u mainnet-beta
    # 记录返回的新 program id
  3. 修改 Anchor.toml

    [programs.mainnet]
    scoreboard = "<NEW_PROGRAM_ID>"
    
    [provider]
    cluster = "mainnet-beta"
    wallet = "/path/to/mainnet-wallet.json"

确认余额充足后直接 anchor deploy --provider.cluster mainnet-beta。首次部署约 15 秒完成。


下一步,进阶练练手


你已完成 Solidity → Solana 的无缝迁移!把项目发到社区,或继续改良功能吧。任何疑问欢迎留言交流。祝你开发愉快!