Einleitung
Im Jahr 2024 stieg Solana in den Vordergrund und wurde zur viertgrößten öffentlichen Blockchain nach Total Value Locked (TVL), was das Interesse von Investoren und Entwicklern weckte.
BlockSec hat die Reihe „Solana Simplified“ kuratiert, die Artikel über die Grundkonzepte von Solana, Anleitungen zum Schreiben von Solana-Smart Contracts und Leitfäden 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 Reihe haben wir uns mit Schlüsselkonzepten des Solana-Netzwerks beschäftigt, einschließlich seines Betriebsmechanismus, seines Kontomodells und seiner Transaktionen, und damit eine solide Grundlage für das Schreiben korrekter und leistungsstarker Solana-Verträge geschaffen.
In diesem Artikel führen wir Sie durch das Schreiben eines Solana-Programms (d. h. eines Solana-Smart Contracts) zum Posten und Anzeigen von Artikeln. Dies wird helfen, die Konzepte aus dem ersten Artikel zu festigen und einige über Solana noch nicht besprochene Funktionen einzuführen. Das relevante Programm und der Testcode wurden auf GitHub veröffentlicht.
Umgebung einrichten
Hinweis: Die folgenden Befehle gelten nur für das Ubuntu-System. Einige Befehle funktionieren möglicherweise nicht unter Windows und macOS.
👉 Sie können alternative Befehle verwenden oder sich auf die Einrichtung der lokalen 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-Programm-Entwicklung 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 Verträgen 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
Als Nächstes führen Sie den folgenden Befehl aus, um eine lokale Wallet auf dem Dateisystem zu generieren:
solana-keygen new
Standardmäßig erstellt dieser Befehl eine Datei mit dem privaten Schlüssel unter ~/.config/solana/id.json, die wir später im Testskript verwenden werden. Sie können den Befehl solana address verwenden, um die generierte Adresse anzuzeigen:
$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy
Devnet
Das Programm wird im Devnet bereitgestellt, daher müssen wir den folgenden Befehl ausführen, um die Solana CLI auf das Devnet umzuschalten:
solana config set --url https://api.devnet.solana.com
SOL-Token
Da die Bereitstellung von Verträgen 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 von uns vorgestellte Programm ermöglicht es Benutzern, Artikel zu posten und alle aktuell geposteten Artikel aufzulisten. Es verarbeitet drei Arten von Anweisungen:
- init-Anweisung: Initialisiert das Programm durch Erstellung eines Datenkontos zur Speicherung des aktuellen maximalen Artikelsindex.
- post-Anweisung: Speichert einen geposteten Artikel in einem neuen Datenkonto.
- list-Anweisung: Gibt alle geposteten Artikel im Log aus.
Nachfolgend 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-programmuss mit der installierten Version der Solana CLI übereinstimmen (Sie können dies mitsolana --versionüberprüfen). Andernfalls kann es während der Kompilierung zu Fehlern kommen.
Die Einstiegsfunktion des Vertrags ist in processor.rs definiert. Die Datei beginnt mit dem Import der erforderlichen Abhängigkeiten (ähnliche Teile in anderen Rust-Dateien überspringen wir):

Anschließend folgt die Definition der Einstiegsfunktion des Vertrags:

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 zur Verarbeitung von Anweisungen.
process_instruction extrahiert das erste Byte von instruction_data, um den Anweisungstyp zu identifizieren, und ruft dann die entsprechende Funktion zur Bearbeitung auf. 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 im 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 nacheinander die erforderlichen Kontoinformationen, 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), und stellt dann sicher, dass index_pda_key mit der Adresse von index_pda übereinstimmt.
Hier ist die Adresse von index_pda eine spezielle Adresse, die als PDAs (Program Derived Addresses) bezeichnet wird. Im Gegensatz zu Adressen von "Wallet"-Konten, 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 Programmadresse werden explizit vom Aufrufer bereitgestellt, während der Bump automatisch von der Funktion find_program_address generiert und zurückgegeben wird. Der Bump stellt sicher, dass auf diese Weise erstellte Adressen nicht mit Adressen kollidieren, die aus öffentlichen Schlüsseln generiert werden.

Nachdem bestätigt wurde, dass die Adresse von index_pda erwartet wird, 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 Platz 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 für 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 hat folgende Signatur:
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. Seine Signatur 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, so 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 erstellte Konto ein Signierer ist, müssen wir invoke_signed verwenden, um eine Signatur für index_pda bereitzustellen.

Am Ende des Programms ruft der Vertrag die Methode serialize auf index_data auf, um die Daten zu serialisieren und sie in das data-Feld 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 geposteten Artikel, daher hat sie einen zusätzlichen Parameter namens instruction_data_inner, der die serialisierten Daten des Artikels speichert.
Wie üblich beginnt das Programm mit dem Extrahieren der von der Anweisung 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 werden in den Zeilen 27 bis 30 find_program_address verwendet, um zu überprüfen, ob die übergebene Adresse von index_pda korrekt ist. Wir deserialisieren dann die Daten in seinem data-Feld, um den aktuell zugewiesenen Maximalindex zu lesen.
Die Zeilen 33 bis 40 überprüfen ebenfalls 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 wird bei jedem Artikel-Upload inkrementiert, was sicherstellt, dass die generierte Adresse eindeutig ist.

Als Nächstes deserialisiert das Programm instruction_data_inner in Artikeldaten des Typs 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 einziges Konto zu passen.

Die folgenden Schritte ähneln ebenfalls der init-Anweisung: Das Programm berechnet zuerst den erforderlichen Platz und die Miete für das Konto, dann ruft es die create_account-Anweisung des SystemProgram auf, um das PDA-Konto zur Speicherung des Artikels zu erstellen. Schließlich wird der gepostete Artikel in das data-Feld von new_article_pda serialisiert und der aktuelle Maximalindex um eins erhöht und in das data-Feld 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 Maximalindex übereinstimmt, und prüft die Korrektheit der Adresse jedes Kontos.

Am Ende des Programms wird über jedes Konto iteriert und das Makro msg! aus solana-program verwendet, um den deserialisierten Artikelinhalt nacheinander auszugeben.
Transaktionstests
Um unseren Vertrag zu testen, verwenden wir ein TypeScript-Skript unter client/main.ts im Repository.

Am Anfang des Skripts importieren wir alle notwendigen Bibliotheken und definieren drei globale Konstanten. KEYPAIR_PATH gibt den Pfad zur privaten Schlüsseldatei an, die mit solana-keygen new generiert wurde, PROGRAM_ID ist die Adresse des im letzten Abschnitt geschriebenen bereitgestellten Programms, und POST_ARTICLE_SCHEMA ist das Objekt zur Serialisierung von Artikeln.


Im Hauptteil des Skripts ruft es zunächst die Funktion loadKeyFromFile auf, um die private Schlüsseldatei 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 den gleichen Algorithmus wie der Rust-Vertrag und stellt sicher, dass sie mit denselben Parametern dieselbe Adresse berechnet. Das connection-Objekt gibt an, dass wir das devnet zum Testen verwenden werden.

Als Nächstes senden wir die erste Initialisierungstransaktion. Wir erstellen eine Transaktion, die eine einzelne Anweisung enthält. Das data-Feld dieser Anweisung enthält nur ein Byte „0“, was darauf hindeutet, dass dies ein Aufruf der init-Anweisung ist. Die Transaktion wird dann gesendet und mit sendAndConfirmTransaction bestätigt.

Die zweite Transaktion ruft die post-Anweisung auf, um einige Artikel zu posten. 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 der Bestätigung der Transaktion ausgegeben.
Um das Programm mit dem Skript zu testen, ersetzen Sie KEYPAIR_PATH durch den Pfad zu Ihrem privaten Schlüssel (falls es sich nicht um den Standardpfad handelt). 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 die 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 Systemprogramms ist.

Ebenso können wir die dritte Transaktion zum Auflisten aller Artikel untersuchen. Im Abschnitt Program Logs können wir feststellen, 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-Vertrags detailliert beschrieben. Schließlich haben wir die Vertragsfunktionalitäten mit einem TypeScript-Skript getestet. Mit diesem Schritt-für-Schritt-Tutorial glauben wir, dass Sie gelernt haben, wie man einen einfachen Solana-Smart Contract schreibt 🥳.
Im nächsten Artikel stellen wir eine detaillierte Anleitung bereit, wie Solana-Transaktionen mit Phalcon Explorer (Phalcon Explorer unterstützt jetzt Solana) angezeigt und analysiert werden können. Bleiben Sie dran!



