Solana vereinfacht 02: Dein erster Solana Smart Contract von Grund auf

Meistern Sie die Solana-Programmierung in einem Artikel! Von Umgebungssetup und Vertragslogik bis zum Testen von Programmen.

Solana vereinfacht 02: Dein erster Solana Smart Contract von Grund auf

Einleitung

Im Jahr 2024 stieg Solana in der Rangliste der öffentlichen Blockchains nach Total Value Locked (TVL) auf Platz vier auf und zog damit das Interesse von Investoren und Entwicklern auf sich.

BlockSec hat die Serie "Solana vereinfacht" zusammengestellt, die Artikel zu grundlegenden Konzepten von Solana, Tutorials zum Schreiben von Solana-Smart-Contracts und Anleitungen zur Analyse von Solana-Transaktionen enthält. Ziel ist es, den Lesern zu helfen, das Solana-Ökosystem zu verstehen und wesentliche Fähigkeiten für die Entwicklung von Projekten und die Durchführung von Transaktionen auf Solana zu beherrschen.

Im ersten Artikel dieser Serie haben wir uns mit Schlüsselkonzepten des Solana-Netzwerks befasst, einschließlich seines Betriebsmechanismus, seines Kontenmodells und seiner Transaktionen, und damit eine solide Grundlage für das Schreiben korrekter und leistungsstarker Solana-Contracts gelegt.

In diesem Artikel führen wir Sie durch das Schreiben eines Solana-Programms (d. h. eines Solana-Smart-Contracts) zum Veröffentlichen und Anzeigen von Artikeln. Dies wird helfen, die Konzepte aus dem ersten Artikel zu festigen und einige Funktionen von Solana einzuführen, die wir bisher noch nicht besprochen haben. Das entsprechende Programm und der Testcode wurden auf GitHub veröffentlicht.

Umgebung einrichten

Hinweis: Die folgenden Befehle beziehen sich nur auf das Ubuntu-System. Einige Befehle funktionieren unter Windows und macOS möglicherweise nicht.

👉 Sie können alternative Befehle verwenden oder sich auf die Einrichtung lokaler Entwicklung und die Installation der Solana CLI beziehen, um dies zu lösen.

Wir werden Solana-Programme in der lokalen Umgebung kompilieren und bereitstellen. Bevor wir uns der Solana-Programmentwicklung widmen, müssen wir einige Befehle und Abhängigkeiten installieren.

Rust

Solana-Programme werden überwiegend in der Programmiersprache Rust geschrieben, daher müssen wir den folgenden Befehl ausführen, um Rust zu installieren:

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Node.js & TypeScript

Das Testskript ist in TypeScript geschrieben, daher müssen wir die Node.js- und TypeScript-Toolchain installieren:

curl -fsSL https://deb.nodesource.com/setup_15.x | sudo -E bash -
sudo apt-get install -y nodejs npm
npm install -g typescript
npm install -g ts-node

Solana CLI

Als Nächstes müssen wir das Solana CLI-Tool installieren, das Befehle für Aufgaben wie das Erstellen von Wallets und das Bereitstellen von Contracts bereitstellt:

sh -c "$(curl -sSfL https://release.solana.com/stable/install)"

Führen Sie den folgenden Befehl aus, um die Änderungen im aktuellen Terminal anzuwenden:

source ~/.profile

Sie können solana --version ausführen, um zu überprüfen, ob die Solana CLI erfolgreich installiert wurde:

$ solana --version
solana-cli 1.17.25 (src:d0ed878d; feat:3580551090, client:SolanaLabs)

Lokale Wallet

Führen Sie als Nächstes den folgenden Befehl aus, um eine Wallet auf dem lokalen Dateisystem zu generieren:

solana-keygen new

Standardmäßig erstellt dieser Befehl eine Private-Key-Datei unter ~/.config/solana/id.json, die wir später im Testskript verwenden werden. Sie können den Befehl solana address verwenden, um die damit generierte Adresse anzuzeigen:

$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy

Devnet

Das Programm wird auf Devnet bereitgestellt, daher müssen wir den folgenden Befehl ausführen, um die Solana CLI zu Devnet zu wechseln:

solana config set --url https://api.devnet.solana.com

SOL-Token

Da das Bereitstellen von Contracts und das Senden von Testtransaktionen nicht kostenlos sind, müssen wir einige SOL-Token anfordern. Sie können solana airdrop 2 ausführen oder Token direkt vom öffentlichen Web-Faucet anfordern.

Programm schreiben

Das Programm, das wir einführen werden, ermöglicht es Benutzern, Artikel zu veröffentlichen und alle derzeit veröffentlichten Artikel aufzulisten. Es behandelt drei Arten von Anweisungen:

  • init-Anweisung: Initialisiert das Programm durch Erstellen eines Datenkontos, um den aktuellen maximalen Artikelindex zu speichern.
  • post-Anweisung: Speichert einen veröffentlichten Artikel in einem neuen Datenkonto.
  • list-Anweisung: Gibt alle veröffentlichten Artikel im Protokoll aus.

Unten sehen Sie die Struktur des Programms:

$ tree program/
program/
├── Cargo.lock
├── Cargo.toml
└── src
    ├── instructions
    │   ├── init.rs
    │   ├── list.rs
    │   ├── mod.rs
    │   └── post.rs
    ├── lib.rs
    └── processor.rs

Cargo.toml gibt die externen Bibliotheken für das Projekt an:

[dependencies]
borsh = "0.10.0"
borsh-derive = "0.10.0"
solana-program = "=1.17.25"

Hinweis: Die Version von solana-program sollte mit der installierten Version der Solana CLI übereinstimmen (Sie können dies mit solana --version überprüfen). Andernfalls können während der Kompilierung Fehler auftreten.

Die Einstiegsfunktion des Contracts ist in processor.rs definiert. Die Datei beginnt mit dem Import der erforderlichen Abhängigkeiten (wir werden ähnliche Teile in anderen Rust-Dateien überspringen):

Anschließend folgt die Definition der Einstiegsfunktion des Contracts:

In Zeile 10 des Programms gibt das Makro entrypoint! aus der Solana-Bibliothek an, dass die Einstiegsfunktion process_instruction ist. Diese Funktion nimmt drei Parameter entgegen:

  • program_id: Die bereitgestellte Adresse des aktuellen Programms.
  • accounts: Alle am Befehl beteiligten Konten.
  • instruction_data: Das Byte-Array, das zur Verarbeitung von Befehlen verwendet wird.

process_instruction extrahiert das erste Byte von instruction_data, um den Typ der Anweisung zu identifizieren und ruft die entsprechende Funktion auf, um sie zu behandeln. Als Nächstes sehen wir uns an, wie diese drei Funktionen implementiert sind.

init

Jede der drei Funktionen ist in einer Datei mit demselben Namen unter dem Verzeichnis program/src/instructions definiert. Beginnen wir mit init.rs.

Da die init-Anweisung kein zusätzliches Byte-Array benötigt, akzeptiert sie nur zwei Parameter: program_id und accounts.

Zwischen den Zeilen 12 und 15 extrahiert das Programm sequenziell die erforderlichen Kontoinformationen und ruft dann die Funktion find_program_address auf, um die Adresse des Datenkontos zu berechnen, das zur Speicherung des aktuellen maximalen Artikelindex verwendet wird, index_pda_key. Dann stellt das Programm fest, dass index_pda_key mit der Adresse von index_pda übereinstimmt.

Hier ist die Adresse von index_pda eine spezielle Adresse namens PDAs (Program Derived Addresses). Im Gegensatz zu "Wallet"-Kontoadressen, die aus öffentlichen Schlüsseln generiert werden, werden PDAs aus einem optionalen Byte-Array, einem Byte namens "Bump" und der Adresse eines Programms abgeleitet. Das Byte-Array und die Programm-Adresse werden explizit vom Aufrufer bereitgestellt, während der Bump automatisch generiert und von der Funktion find_program_address zurückgegeben wird. Der Bump stellt sicher, dass auf diese Weise erstellte Adressen nicht mit Adressen kollidieren, die aus einem öffentlichen Schlüssel generiert wurden.

Nachdem die Adresse von index_pda als erwartet bestätigt wurde, verwenden wir die vom SystemProgram bereitgestellte create_account-Anweisung, um index_pda zu erstellen. In Zeile 22 erstellt das Programm eine Variable vom Typ IndexAccountData, die den aktuellen maximalen Artikelindex (initialisiert auf 0) aufzeichnet. Wie in der folgenden Abbildung gezeigt, implementiert dieser Typ die Traits BorshSerialize und BorshDeserialize, wodurch er im Borsh-Format serialisiert und deserialisiert werden kann:

Die Zeilen 23 bis 24 berechnen den erforderlichen Speicherplatz für die Speicherung der Kontodaten und die minimale Miete für die Erstellung des Kontos.

Die Zeilen 27 bis 37 erstellen eine create_account-Anweisung vom SystemProgram und rufen sie mit der Methode invoke_signed auf. Die Aktion, eine Anweisung an ein anderes Programm innerhalb eines Programms aufzurufen, wird als CPI (Cross-Program Invocation) bezeichnet. In Solana können Sie zwei Funktionen verwenden, um CPI auszuführen: invoke und invoke_signed.

Die Funktion invoke verarbeitet die gegebene Anweisung direkt und ihre Funktionssignatur lautet wie folgt:

pub fn invoke(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>]
) -> Result<(), ProgramError>

Der einzige Unterschied zwischen invoke_signed und invoke besteht darin, dass invoke_signed eine Möglichkeit bietet, für PDA-Konten zu signieren. PDA-Konten haben keine öffentlichen/privaten Schlüssel und können daher keine Signaturen direkt bereitstellen. invoke_signed löst dieses Problem. Diese Funktion akzeptiert einen zusätzlichen Parameter namens signers_seeds. Ihre Funktionssignatur lautet wie folgt.

Ähnlich wie die Funktion find_program_address funktioniert, berechnet invoke_signed eine Reihe von PDA-Adressen basierend auf den bereitgestellten signers_seeds und der program_id des Aufrufers. Wenn account_infos diese PDA-Adressen enthält, werden sie als Signierer markiert, als ob sie Signaturen bereitgestellt hätten.

pub fn invoke_signed(
    instruction: &Instruction,
    account_infos: &[AccountInfo<'_>],
    signers_seeds: &[&[&[u8]]]
) -> Result<(), ProgramError>

Hier, da die create_account-Anweisung erfordert, dass das zu erstellende Konto ein Signierer ist, müssen wir invoke_signed verwenden, um eine Signatur für index_pda bereitzustellen.

Am Ende des Programms ruft der Contract die Methode serialize auf index_data auf, um die Daten zu serialisieren und sie in das Feld data von index_pda zu schreiben.

post_article

Als Nächstes sehen wir uns die Implementierung der post-Anweisung an. Im Gegensatz zur init-Anweisung akzeptiert die post-Anweisung einen veröffentlichten Artikel, sodass sie einen zusätzlichen Parameter namens instruction_data_inner hat, der die serialisierten Daten des Artikels speichert.

Wie üblich beginnt das Programm mit der Extraktion der vom Befehl benötigten Kontoinformationen. Da wir für jeden Artikel ein separates PDA-Konto generieren, akzeptiert das Programm in Zeile 23 ein zusätzliches Konto, new_article_pda.

Ähnlich wie bei der init-Anweisung verwenden die Zeilen 27 bis 30 find_program_address, um zu verifizieren, dass die übergebene Adresse von index_pda korrekt ist. Wir deserialisieren dann die Daten in seinem Feld data, um den aktuell zugewiesenen maximalen Index zu lesen.

Die Zeilen 33 bis 40 überprüfen auch die Adresse von new_article_pda. Das Programm verwendet den String "ARTICLE_PDA" und den aktuellen Index, um eine neue Adresse zu generieren. Der Index erhöht sich mit jedem hochgeladenen Artikel, was sicherstellt, dass die generierte Adresse eindeutig ist.

Als Nächstes deserialisiert das Programm instruction_data_inner in Artikeldaten vom Typ PostArticleData:

Diese Struktur hat nur zwei Felder: title und content. In den Zeilen 45 bis 49 führt das Programm eine Prüfung durch, um sicherzustellen, dass der Artikel nicht zu groß ist, um in ein einzelnes Konto zu passen.

Die folgenden Schritte sind ebenfalls ähnlich zur init-Anweisung: Das Programm berechnet zunächst den erforderlichen Speicherplatz und die Miete für das Konto, ruft dann die create_account-Anweisung des SystemProgram auf, um das PDA-Konto für die Speicherung des Artikels zu erstellen. Schließlich wird der veröffentlichte Artikel in das Feld data von new_article_pda serialisiert und der aktuelle maximale Index um eins erhöht und in das Feld data von index_pda serialisiert.

list_articles

Schließlich sehen wir uns die Implementierung der list-Anweisung an. Da diese Anweisung alle Artikel auflisten muss, wird in Zeile 15 ein Vektor verwendet, um alle PDA-Konten darzustellen, die die Artikel speichern, und die Adresse des index_pda-Kontos wird ebenfalls validiert.

Danach überprüft das Programm, ob die Größe des Vektors mit dem aktuellen maximalen Index übereinstimmt, und prüft die Korrektheit der Adresse jedes Kontos.

Am Ende des Programms durchläuft es jedes Konto und verwendet das Makro msg! von solana-program, um den deserialisierten Artikelinhalt nacheinander auszugeben.

Transaktionsprüfung

Um unseren Contract zu testen, verwenden wir ein TypeScript-Skript unter client/main.ts im Repository.

Oben im Skript importieren wir alle erforderlichen Bibliotheken und definieren drei globale Konstanten. KEYPAIR_PATH gibt den Pfad zur mit solana-keygen new generierten Private-Key-Datei an, PROGRAM_ID ist die Adresse des im letzten Abschnitt geschriebenen bereitgestellten Programms, und POST_ARTICLE_SCHEMA ist das Objekt für die Artikelserialisierung.

Im Hauptteil des Skripts ruft es zunächst die Funktion loadKeyFromFile auf, um die Private-Key-Datei zu parsen und ein Keypair-Objekt als Zahler für Transaktionsgebühren zu erhalten. Dann verwendet es die von @solana/web3.js bereitgestellte Methode findProgramAddressSync, um die Adresse von indexPda zu berechnen. Diese Methode verwendet denselben Algorithmus wie der Rust-Contract und stellt sicher, dass sie mit denselben Parametern dieselbe Adresse berechnet. Das connection-Objekt gibt an, dass wir devnet zum Testen verwenden werden.

Als Nächstes senden wir die erste Initialisierungstransaktion. Wir erstellen eine Transaktion, die eine einzelne Anweisung enthält. Das Feld data dieser Anweisung enthält nur ein Byte "0", was anzeigt, dass es sich um einen Aufruf der init-Anweisung handelt. Die Transaktion wird dann mit sendAndConfirmTransaction gesendet und bestätigt.

Die zweite Transaktion ruft die post-Anweisung auf, um einige Artikel zu veröffentlichen. Von Zeile 56 bis 63 serialisiert das Skript zwei Artikel und berechnet die entsprechenden PDA-Adressen. Dann werden zwei post-Anweisungen zu einer einzigen Transaktion hinzugefügt, um diese Artikel zu erstellen.

Die letzte Transaktion ruft die list-Anweisung auf, und dann werden die Protokolle nach Bestätigung der Transaktion ausgegeben.

Um das Programm mit dem Skript zu testen, ersetzen Sie KEYPAIR_PATH durch den Pfad zu Ihrem Private Key (falls er nicht der Standardpfad ist). Kompilieren und stellen Sie dann den Rust-Smart-Contract bereit, indem Sie die folgenden Befehle ausführen:

cd program
cargo build-bpf
solana program deploy ./target/deploy/hello_solana.so

Kopieren Sie dann die bereitgestellte Adresse und fügen Sie sie in das Feld PROGRAM_ID ein. Führen Sie im Stammverzeichnis des Projekts npm install aus, um die Abhängigkeiten zu installieren, und führen Sie dann npm start aus, um das Skript auszuführen.

Danach können wir Solscan verwenden, um die Ausführungsdetails der Testtransaktionen anzuzeigen.

Schauen wir uns direkt die zweite Transaktion zum Veröffentlichen von Artikeln an. Sie enthält zwei Anweisungen, von denen jede das erste Byte der Instruction Data Raw als 0x01 hat. Zusätzlich enthält jede Anweisung eine interne Anweisung, die ein Aufruf der CreateAccount-Anweisung des System Program ist.

Ebenso können wir die dritte Transaktion zum Auflisten aller Artikel überprüfen. Im Abschnitt Program Logs finden wir, dass die Indizes, Titel und Inhalte der beiden Artikel ausgegeben werden.

Fazit

In diesem Artikel haben wir zunächst erklärt, wie die Solana-Programmier- und Laufzeitumgebung lokal eingerichtet wird. Dann haben wir die Implementierungslogik eines Solana-Contracts detailliert beschrieben. Schließlich haben wir die Contract-Funktionalitäten mit einem TypeScript-Skript getestet. Mit diesem Schritt-für-Schritt-Tutorial haben Sie gelernt, wie man einen einfachen Solana-Smart-Contract schreibt 🥳.

Im nächsten Artikel geben wir eine detaillierte Anleitung, wie man Solana-Transaktionen mit Phalcon Explorer (Phalcon Explorer unterstützt jetzt Solana) anzeigt und analysiert. Bleiben Sie dran!

Lesen Sie andere Artikel dieser Serie:

Sign up for the latest updates