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

Meistern Sie das Schreiben von Solana-Programmen mit nur diesem einen Artikel! Umfassend von der Umgebungs-Einrichtung über die Vertragslogik bis hin zum Testen von Programmen.

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

Einleitung

Im Jahr 2024 stieg Solana rasant auf und wurde zur viertgrößten öffentlichen Blockchain nach Total Value Locked (TVL), was das Interesse von Investoren und Entwicklern auf sich zog.

BlockSec hat die Serie "Solana Simplified" kuratiert, die Artikel über die Grundkonzepte 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 den Kernkonzepten des Solana-Netzwerks befasst, einschließlich seines Betriebsmechanismus, seines Account-Modells und seiner Transaktionen, und damit eine solide Grundlage für das Schreiben korrekter und leistungsstarker Solana-Contracts 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 hilft, die Konzepte aus dem ersten Artikel zu festigen und einige bisher nicht besprochene Funktionen von Solana vorzustellen. 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 unter Windows und macOS möglicherweise nicht.

👉 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 mit der Solana-Programmentwicklung befassen, 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 Test-Skript 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 lokale Dateisystem-Wallet zu generieren:

solana-keygen new

Standardmäßig erstellt dieser Befehl eine private Schlüsseldatei unter ~/.config/solana/id.json, die wir später im Test-Skript verwenden werden. Sie können den Befehl solana address verwenden, um die damit generierte Adresse anzuzeigen:

$ solana address
9FcGDHvzs8Czo4B1pQq8GPdXfdGHqNqayvh6bu4hVGfy

Devnet

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

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

SOL Tokens

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

Programm schreiben

Das Programm, das wir vorstellen werden, ermöglicht es Benutzern, Artikel zu posten und alle derzeit geposteten Artikel aufzulisten. Es verarbeitet drei Arten von Anweisungen:

  • init-Anweisung: Initialisiert das Programm durch Erstellen eines Datenkontos zur Speicherung des aktuellen maximalen Artikelsindexes.
  • post-Anweisung: Speichert einen geposteten Artikel in einem neuen Datenkonto.
  • list-Anweisung: Gibt alle geposteten Artikel im Log 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 Solana CLI-Version ü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 (ähnliche Teile in anderen Rust-Dateien werden übersprungen):

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 an der Anweisung beteiligten Konten.
  • instruction_data: Das Byte-Array zur Verarbeitung von Anweisungen.

process_instruction extrahiert das erste Byte von instruction_data, um den Typ der Anweisung zu identifizieren, und ruft 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 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, ruft dann die Funktion find_program_address auf, um die Adresse des Datenkontos zur Speicherung des aktuellen maximalen Artikelsindexes, index_pda_key, zu berechnen. Anschließend stellt das Programm sicher, 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 Programmadresse 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 öffentlichen Schlüsseln generiert wurden.

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

Die Zeilen 23 bis 24 berechnen den benötigten Speicherplatz für die Speicherung der Kontodaten und die Mindestmiete für die Erstellung des Kontos.

Die Zeilen 27 bis 37 erstellen eine create_account-Anweisung aus dem SystemProgram und rufen sie mit der Methode invoke_signed auf. Die Aktion, eine Anweisung innerhalb eines Programms an ein anderes Programm aufzurufen, wird als CPI (Cross-Program Invocation) bezeichnet. Auf Solana können Sie zwei Funktionen für CPI verwenden: 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, 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 Unterzeichner markiert, als hätten sie Signaturen bereitgestellt.

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 Unterzeichner 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 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 geposteten Artikel und hat daher einen zusätzlichen Parameter namens instruction_data_inner, der die serialisierten Daten des Artikels speichert.

Wie üblich beginnt das Programm mit der Extraktion 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 verwenden die Zeilen 27 bis 30 find_program_address, um zu überprüfen, ob die Adresse des übergebenen 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 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 mit jedem Artikel-Upload inkrementiert, was sicherstellt, dass die generierte Adresse eindeutig ist.

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

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 wie bei der init-Anweisung: Das Programm berechnet zuerst den benötigten Speicherplatz 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 Feld data von new_article_pda serialisiert, und der aktuelle maximale Index wird 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 ob die Adresse jedes Kontos korrekt ist.

Am Ende des Programms iteriert es über jedes Konto und verwendet das Makro msg! aus solana-program, um den deserialisierten Artikelinhalt einzeln auszugeben.

Transaktionstest

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

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 bereitgestellten Programms, das wir im letzten Abschnitt geschrieben haben, und POST_ARTICLE_SCHEMA ist das Objekt für die Artikel-Serialisierung.

Im Hauptteil des Skripts wird zunächst die Funktion loadKeyFromFile aufgerufen, um die private Schlüsseldatei zu parsen und ein Keypair-Objekt als Payer für Transaktionsgebühren zu erhalten. Dann wird die Methode findProgramAddressSync von @solana/web3.js verwendet, um die Adresse von indexPda zu berechnen. Diese Methode verwendet denselben Algorithmus wie der Rust-Contract, um sicherzustellen, 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 einzige Anweisung enthält. Das Feld data dieser Anweisung enthält nur ein Byte "0", was angibt, 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 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 Logs nach 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 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 dann npm start, um das Skript auszuführen.

Anschließend 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 aufweist. 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 inspizieren. Im Abschnitt Program Logs finden wir die ausgegebenen Indizes, Titel und Inhalte der beiden Artikel.

Schlussfolgerung

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 dieser Schritt-für-Schritt-Anleitung glauben wir, dass Sie gelernt haben, 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