Kapitel 4. Unser erster Smart Contract

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Nachdem wir nun alles installiert haben, ist es an der Zeit, unseren ersten Vertrag zu erstellen. Ganz in der Tradition der einführenden Programmierbücher wird uns unser erstes Programm mit "Hallo, Welt!" begrüßen.

Bei der Entwicklung dieses Programms werden wir lernen, wie wir die von Truffle bereitgestellten Werkzeuge zum Erstellen und Testen unserer Anwendung nutzen können. Außerdem werden wir die Solidity-Sprache kennenlernen und einen Blick auf Funktionen und Zustandsvariablen werfen.

Unser Ziel in diesem Kapitel ist es, einen Rhythmus zu finden, wie wir unsere Anwendung entwickeln und herauszufinden, ob wir auf dem richtigen Weg sind. Um uns dabei zu helfen, werden wir die testgetriebene Entwicklung (TDD) für eine sofortige Feedbackschleife einsetzen.

Beginnen wir damit, unser Projekt einzurichten.

Einrichtung

Während wir uns einrichten, brauchen wir ein Verzeichnis für unsere neue Anwendung. Erstellen wir zunächst ein Verzeichnis namens greeter und wechseln wir in unser neues Verzeichnis. Öffne dein Terminal und gib die folgenden Befehle ein:

$ mkdir greeter
$ cd greeter

Wir werden nun ein neues Truffle-Projekt wie folgt initialisieren:

$ truffle init

Dieser Befehl erzeugt die folgende Ausgabe:

✔ Preparing to download
✔ Downloading
✔ Cleaning up temporary files
✔ Setting up box

Unbox successful. Sweet!

Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

Unser Greeter-Verzeichnis sollte jetzt die folgenden Dateien enthalten:

greeter
├── contracts
│   └── Migrations.sol
├── migrations
│   └── 1_initial_migration.js
├── test
└── truffle-config.js

Die in der Ausgabe angegebenen Befehle stimmen gut mit der Verzeichnisstruktur überein, die bei der Initialisierung unserer Anwendung erstellt wurde. truffle compile kompiliert alle Verträge im Verzeichnis contracts, truffle migrate stellt die kompilierten Verträge bereit, indem es die Skripte in unserem Verzeichnis migrations ausführt, und truffle test führt die Tests in unserem Verzeichnis test aus.

Das letzte Element, das für dich erstellt wird, ist die Datei truffle-config.js. Hier werden wir unsere anwendungsspezifischen Konfigurationen ablegen.

Jetzt, wo wir unsere erste Struktur haben, können wir mit der Entwicklung beginnen.

Unser erster Test

Wenn wir Funktionen für unsere Verträge implementieren, werden wir TDD einsetzen, um die kurze Feedbackschleife zu nutzen, die es bietet. Wenn du mit TDD nicht vertraut bist, ist das eine Art, Software zu schreiben, bei der wir zunächst mit einem fehlgeschlagenen Test beginnen und dann den Code schreiben, der erforderlich ist, um den Test zu bestehen. Sobald alles funktioniert, können wir den Code überarbeiten, um ihn wartbarer zu machen.

Die Testunterstützung, die Truffle bietet, ist einer der Bereiche, in denen der Toolbelt wirklich glänzt. Es bietet Testunterstützung sowohl in JavaScript als auch in Solidity. Für unsere Beispiele werden wir JavaScript verwenden, da es viel weiter verbreitet ist und es einfacher ist, zusätzliche Ressourcen zu finden, wenn du nicht weiterkommst. Wenn du dich mit dem Schreiben von Tests in Solidity beschäftigen möchtest, kannst du die Truffle-Testdokumentation zu Rate ziehen.

Unser erster Test in Beispiel 4-1 soll sicherstellen, dass unser leerer Vertrag richtig eingesetzt werden kann. Das mag unnötig erscheinen, aber die Fehler, die uns dabei unterlaufen, sind eine gute Möglichkeit, einige der Fehler zu erkennen, die uns in unserer Karriere begegnen werden.

Erstelle im Testverzeichnis eine Datei namens greeter_test.js:

$ touch test/greeter_test.js

Füge dann den Testcode hinzu, wie in Beispiel 4-1.

Beispiel 4-1. Testen, ob unser Vertrag eingesetzt werden kann
const GreeterContract = artifacts.require("Greeter"); 1

contract("Greeter", () => {                           2
  it("has been deployed successfully", async () => {  3
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract was not deployed");    4
  });
});
1

Truffle bietet eine Möglichkeit, Verträge zu laden und mit ihnen zu interagieren, die über die Funktion artifacts.require kompiliert wurden. Hier übergibst du den Namen des Vertrags und nicht den Namen der Datei, da eine Datei mehrere Vertragsdeklarationen enthalten kann.

2

Truffle-Tests verwenden Mocha, aber mit einem kleinen Unterschied. Die Funktion contract funktioniert ähnlich wie die eingebaute Funktion describe, hat aber den zusätzlichen Vorteil, dass sie die Clean-Room-Funktion von Truffle nutzt. Diese Funktion bedeutet, dass neue Verträge bereitgestellt werden, bevor die darin verschachtelten Tests ausgeführt werden. Dadurch wird verhindert, dass der Status zwischen verschiedenen Testgruppen ausgetauscht wird.

3

Jede Interaktion mit der Blockchain wird asynchron sein. Deshalb werden wir statt Promises und der MethodePromise.prototype.then die Vorteile der asynchronen/await-Syntax nutzen, die jetzt in JavaScript verfügbar ist.

4

Wenn der Begrüßer wahrheitsgemäß ist (existiert), besteht unser Test.

Wenn wir unsere Tests durchführen, erhalten wir eine Fehlermeldung, die wie folgt aussieht:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Migrations.sol

Error: Could not find artifacts for Greeter from any sources
    at Resolver.require (/usr/local/lib/node_modules/truffle/build/webpack:...
    at TestResolver.require (/usr/local/lib/node_modules/truffle/build/...
    at Object.require (/usr/local/lib/node_modules/truffle/build/webpack:...

    ...omitted..

Truffle v5.0.31 (core: 5.0.31)
Node v12.8.0

Das gibt uns ein brauchbares Feedback. Die Fehlermeldung besagt, dass Truffle nach dem Zusammenstellen unserer Verträge keinen Vertrag namens Greeter finden konnte. Da wir diesen Vertrag noch nicht erstellt haben, ist diese Fehlermeldung durchaus verständlich. Wenn du jedoch bereits einen Vertrag erstellt hast und diesen Fehler immer noch bekommst, liegt das wahrscheinlich an einem Tippfehler in der Vertragsdeklaration in der Solidity-Datei oder in der Anweisung artifacts.require.

Erstellen wir die Greeter-Datei und fügen den Code aus Beispiel 4-2 ein, um zu sehen, welche Rückmeldungen unsere Testsuite liefert.

Im Terminal erstellst du die Greeter-Datei:

$ touch contracts/Greeter.sol
Beispiel 4-2. Leerer Greeter-Vertrag
pragma solidity >= 0.4.0 < 0.7.0; 1

contract Greeter { 2

}
1

Die Zeile pragma ist eine Compiler-Anweisung. Hier teilen wir dem Solidity-Compiler mit, dass unser Code mit Solidity Version 0.4.0 bis einschließlich Version 0.7.0 kompatibel ist.

2

Die Verträge in Solidity sind den Klassen in objektorientierten Programmiersprachen sehr ähnlich. Die Daten und Funktionen oder Methoden, die innerhalb der öffnenden und schließenden geschweiften Klammern des Vertrags definiert sind, werden auf diesen Vertrag beschränkt.

Nachdem wir diese Änderungen vorgenommen haben, führen wir unsere Tests erneut durch:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    1) has been deployed successfully
    > No events were emitted


  0 passing (34ms)
  1 failing

  1) Contract: Greeter
       has been deployed successfully:
     Error: Greeter has not been deployed to detected network...
      at Object.checkNetworkArtifactMatch (/usr/local/lib/node_modules/...
      at Function.deployed (/usr/local/lib/node_modules/truffle/build/...
      at processTicksAndRejections (internal/process/task_queues.js:85:5)
      at Context.<anonymous> (test/greeter_test.js:5:21)

Die Fehlermeldung zeigt an, dass unser Vertrag noch nicht im Netzwerk vorhanden ist, d.h. er wurde noch nicht bereitgestellt. Jedes Mal, wenn wir den Befehl truffle test ausführen, kompiliert Truffle zunächst unsere Verträge und stellt sie dann in einem Testnetzwerk bereit. Um unseren Vertrag zu deployen, müssen wir ein weiteres Tool aus dem Truffle-Toolbelt nutzen: Migrationen.

Migrationen sind in JavaScript geschriebene Skripte, mit denen wir den Einsatz unserer Verträge automatisieren. Der Standardvertrag Migrations, der sich in contracts/Migrations.sol befindet, ist der Vertrag, der von migrations/1_initial_migration.js bereitgestellt wird und derzeit der einzige Vertrag ist, der den Weg ins Testnetzwerk gefunden hat. Um unseren Greeter-Vertrag zum Netzwerk hinzuzufügen, müssen wir eine Migration mit dem Code in Beispiel 4-3 erstellen.

Zuerst müssen wir eine Datei erstellen, die unseren Migrationscode enthält:

$ touch migrations/2_deploy_greeter.js

Dann können wir den Code in Beispiel 4-3 hinzufügen.

Beispiel 4-3. Einsetzen des Greeter-Vertrags
const GreeterContract = artifacts.require("Greeter");

module.exports = function(deployer) {
  deployer.deploy(GreeterContract);
}

Bei unserer ersten Migration ist nicht viel los. Wir verwenden das bereitgestellte Deployer-Objekt, um den Greeter-Vertrag zu verteilen. Das ist alles, was wir tun müssen, um unseren Vertrag im lokalen Testnetzwerk verfügbar zu machen. Aber keine Sorge: Im nächsten Kapitel werden wir uns eingehender mit Migrationen beschäftigen. Führen Sie nun die Tests ein weiteres Mal durch:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully


  1 passing (27ms)

Erfolg! Dieser Test zeigt uns, dass wir alles richtig eingerichtet haben und bereit sind, mit der Implementierung der Funktionen zu beginnen.

Hallo sagen

Dies ist normalerweise der Punkt, an dem wir eine Variante von printf oder println aufrufen würden, um unsere Begrüßung auszugeben, aber in Solidity haben wir keinen Zugriff auf den Standardausgang, das Dateisystem, das Netzwerk oder eine andere Ein-/Ausgabe (I/O). Was wir haben, sind Funktionen.

Nach dem Einsatz wird unser Smart Contract im Ethereum-Netzwerk unter einer bestimmten Adresse gespeichert. Er ruht so lange, bis eine Anfrage eingeht, die ihn auffordert, eine bestimmte Arbeit auszuführen. Was wir wollen, ist eine Funktion, die "Hallo, Welt!" sagen kann, und wie zuvor beginnen wir mit einem Test.

In test/greeter_test.js fügen wir den Test aus Beispiel 4-4 ein.

Beispiel 4-4. Prüfung auf Hello, World!
describe("greet()", () => {
  it("returns 'Hello, World!'", async () => {
    const greeter = await GreeterContract.deployed();
    const expected = "Hello, World!";
    const actual = await greeter.greet();

    assert.equal(actual, expected, "greeted with 'Hello, World!'");
  });
});

In diesem Fall setzen wir einen erwarteten Wert und rufen dann den Wert aus unserem Vertrag ab, um zu sehen, ob sie gleich sind. Wir müssen die Testfunktion als async markieren, da wir unsere lokale Test-Blockchain aufrufen werden, um mit dem Vertrag zu interagieren.

Wenn du den Test durchführst, siehst du Folgendes:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      1) returns 'Hello, World!'
    > No events were emitted


  1 passing (42ms)
  1 failing

  1) Contract: Greeter
       greet()
         returns 'Hello, World!':
     TypeError: greeter.greet is not a function
      at Context.<anonymous> (test/greeter_test.js:13:36)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Wenn wir uns auf den Fehler konzentrieren, sehen wir, dass greeter.greet keine Funktion ist. Es ist an der Zeit, eine Funktion zu unserem Vertrag hinzuzufügen. Aktualisiere den Greeter-Vertrag mit der Funktion in Beispiel 4-5.

Beispiel 4-5. Hinzufügen der Funktion greet zum Greeter
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {

    function greet() external pure returns(string memory) {
        return "Hello, World!";
    }

}

Hier haben wir eine Funktion mit dem Bezeichner oder Namen greet erstellt, die keine Parameter benötigt. Nach dem Bezeichner haben wir angegeben, dass unsere Funktion eine external Funktion ist. Das bedeutet, dass sie Teil der Schnittstelle unseres Vertrags ist und von anderen Verträgen oder Transaktionen aus aufgerufen werden kann, aber nicht innerhalb des Vertrags oder zumindest nicht ohne einen expliziten Verweis auf das Objekt, für das sie aufgerufen wird. Unsere anderen Optionen sind public, internal und private.

public Funktionen sind ebenfalls Teil der Schnittstelle, d.h. sie können von anderen Verträgen oder Transaktionen aus aufgerufen werden, aber zusätzlich können sie auch intern aufgerufen werden. Das bedeutet, dass du einen impliziten Empfänger der Nachricht verwenden kannst, wenn du die Methode innerhalb einer Methode aufrufst.

internal und private Funktionen müssen den impliziten Empfänger verwenden oder können, mit anderen Worten, nicht auf einem Objekt oder auf this aufgerufen werden. Der Hauptunterschied zwischen diesen beiden Modifikatoren besteht darin, dass private Funktionen nur innerhalb des Vertrags sichtbar sind, in dem sie definiert sind, und nicht in abgeleiteten Verträgen.

Funktionen, die den Zustand der Variablen des Vertrags nicht verändern, können entweder als pure oder view gekennzeichnet werden. pure Funktionen lesen nicht aus der Blockchain. Stattdessen arbeiten sie mit den übergebenen Daten oder, wie in unserem Fall, mit Daten, für die überhaupt keine Eingabe erforderlich war. view Funktionen dürfen Daten aus der Blockchain lesen, aber auch sie sind eingeschränkt, da sie nicht in die Blockchain schreiben können.

Nachdem wir erklärt haben, dass diese Funktion pure ist, geben wir an, was wir von unserer Funktion zurückgeben wollen. Solidity erlaubt zwar mehrere Rückgabewerte, aber in unserem Fall wird nur ein Wert zurückgegeben: der Typ string. Außerdem geben wir mit dem Schlüsselwort memory an, dass es sich um einen Wert handelt, der sich nicht in der persistenten Speicherung unseres Vertrags befindet.

Der Hauptteil unserer Funktion gibt die gesuchte Zeichenkette zurück: "Hello, World!" Damit sollten die Anforderungen unseres Tests erfüllt sein. Aber verlass dich nicht auf unser Wort, sondern führe die Tests noch einmal durch, um sie zu überprüfen:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (51ms)


  2 passing (82ms)

Nachdem dieser Test bestanden ist, können wir unseren Vertrag etwas flexibler gestalten, indem wir unseren Nutzern die Möglichkeit geben, die Begrüßung zu ändern.

Unseren Vertrag dynamisch gestalten

Jetzt, da unser Vertrag einen fest kodierten Wert zurückgegeben hat, machen wir weiter und machen die Begrüßung dynamisch. Dazu müssen wir eine weitere Funktion hinzufügen, mit der wir die Nachricht festlegen können, die von unserer Funktion greet() zurückgegeben wird.

Vorhin haben wir die Funktion "Reinraum" erwähnt, wenn wir die Funktion contract in unseren Tests verwenden. Diese Funktion stellt neue Instanzen unserer Verträge bereit, die in der Callback-Funktion für diesen Codeblock verwendet werden. In dem Test, den wir jetzt schreiben, wollen wir sicherstellen, dass unsere Zustandsänderungen vom Rest der Tests isoliert bleiben, damit die Reihenfolge unserer Tests keinen Einfluss auf den Erfolg oder Misserfolg unserer Testsuite hat. Um dies zu erreichen, erstellen wir einen weiteren contract Block in unserer Datei test/greeter-test.js, wie in Beispiel 4-6 gezeigt.

Beispiel 4-6. Testen, ob die Begrüßung dynamisch gestaltet werden kann
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", () => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    });
  });
});

contract("Greeter: update greeting", () => {
  describe("setGreeting(string)", () => {
    it("sets greeting to passed in string", async () => {
      const greeter = await GreeterContract.deployed()
      const expected = "Hi there!";

      await greeter.setGreeting(expected);
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeting was not updated");
    });
  });
});

Du wirst feststellen, dass der Aufbau ähnlich wie bei unserem vorherigen Test ist. Wir setzen eine Variable, die unseren erwarteten Rückgabewert enthält. Das ist die Zeichenkette, die wir auch an die Funktion setGreeting übergeben werden. Dann aktualisieren wir die Begrüßung und fragen die greet nach ihrem Rückgabewert. Dies sind beides asynchrone Aufrufe, für die wir das Schlüsselwort await verwenden. Zum Schluss vergleichen wir den Wert von greet mit unserem erwarteten Wert.

Wenn wir die Tests durchführen, erhalten wir die folgende Ausgabe:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (48ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      1) sets greeting to passed in string
    > No events were emitted


  2 passing (111ms)
  1 failing

  1) Contract: Greeter: update greeting
       setGreeting(string)
         sets greeting to passed in string:
     TypeError: greeter.setGreeting is not a function
      at Context.<anonymous> (test/greeter_test.js:26:21)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Unsere Tests zeigen, dass die Funktion setGreeting noch nicht existiert; fügen wir diese Funktion unserem Vertrag hinzu. Füge in unserer Datei contracts/Greeter.sol nach der Funktion greet die Funktionssignatur aus Beispiel 4-7 ein.

Beispiel 4-7. Hinzufügen von setGreeting() zu Greeter
function setGreeting(string calldata greeting) external {

}

Unsere Funktion setGreeting soll den Status unseres Vertrags mit einer neuen Begrüßung aktualisieren, was bedeutet, dass wir einen Parameter für diesen neuen Wert akzeptieren müssen. Dieser neue Wert wird als Zeichenkette erwartet und wird mit dem Bezeichner greeting bezeichnet. Genau wie unsere Funktion greet ist diese Funktion dafür gedacht, von externen Skripten oder anderen Verträgen aufgerufen zu werden, und wird nicht intern referenziert.

Da diese Funktion von außen aufgerufen wird, sind die Daten, die als Parameter übergeben werden, nicht Teil der persistenten Speicherung des Vertrags, sondern sind als Teil der Aufrufdaten enthalten und müssen mit dem Datenort calldata gekennzeichnet werden. Der Speicherort calldata wird nur benötigt, wenn die Funktion als extern deklariert ist und wenn der Datentyp des Parameters ein Referenztyp ist, wie z. B. ein Mapping, eine Struktur, ein String oder ein Array. Die Verwendung von Werttypen wie int oder address erfordert diese Kennzeichnung nicht.

Wenn wir unsere Tests jetzt mit der deklarierten Funktion ausführen, erhalten wir die folgende Ausgabe:

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (45ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      1) sets greeting to passed in string
    > No events were emitted


  2 passing (215ms)
  1 failing

  1) Contract: Greeter: update greeting
       setGreeting(string)
         sets greeting to passed in string:

      greeting was not updated
      + expected - actual

      -Hello, World!
      +Hi there!

      at Context.<anonymous> (test/greeter_test.js:29:14)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Bei der Überprüfung des Testfehlers erfahren wir, dass der von der Funktion greet zurückgegebene Wert nicht den Erwartungen entsprach. Unsere Funktion hat noch nichts getan und deshalb hat sich die Ansage nie geändert.

Um eine Variable in einer Funktion zu aktualisieren und diese Variable in einer anderen Funktion verfügbar zu machen, müssen wir die Daten in der persistenten Speicherung des Vertrags speichern, indem wir eine Zustandsvariable verwenden, wie in Beispiel 4-8 gezeigt.

Füge in unserer Datei contracts/Greeter.sol den Code aus Beispiel 4-8 zum Greeter-Vertrag hinzu.

Beispiel 4-8. Hinzufügen einer Zustandsvariablen zum Greeter-Vertrag
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
    string private _greeting;

    function greet() external pure returns(string memory) {
        return "Hello, World!";
    }

    function setGreeting(string calldata greeting) external {

    }

}

Zustandsvariablen stehen allen Funktionen zur Verfügung, die innerhalb eines Vertrags definiert sind, ähnlich wie Instanzvariablen oder Mitgliedsvariablen in anderen objektorientierten Sprachen. In ihnen werden auch Daten gespeichert, die während der gesamten Lebensdauer des Vertrags bestehen bleiben. Wie Funktionen können auch Zustandsvariablen mit verschiedenen Sichtbarkeitsmodifikatoren deklariert werden, z. B. public, internal und private. In unserem vorherigen Beispiel haben wir den private Modifikator verwendet, was bedeutet, dass diese Variable in unserem Greeter-Vertrag zugänglich ist.

Hinweis

Alle Daten auf der Blockchain sind für die Außenwelt öffentlich sichtbar. Zustandsvariable Modifikatoren schränken nur ein, wie mit den Daten innerhalb des Vertrags oder anderer Verträge interagiert werden kann.

Wenn du eine Funktion schreibst, die eine Zustandsvariable aktualisiert, wie es bei setGreeting der Fall sein wird, kannst du den Parameter nicht genauso nennen wie die Zustandsvariable. Um diese Art von Konflikten zu vermeiden, ist es üblich, den Namen von Zustandsvariablen oder Parametern einen Unterstrich (_) voranzustellen.

Aktualisieren wir die Funktion setGreeting, um die Zustandsvariable zu setzen, wie in Beispiel 4-9 gezeigt.

Beispiel 4-9. Statusvariable aktualisieren
function setGreeting(string calldata greeting) external {
    _greeting = greeting;
}

Auch mit dieser Änderung würde unser Test noch immer fehlschlagen. Wir wollen nun die Funktion greet aktualisieren, um aus dieser Zustandsvariablen zu lesen, aber es gibt einige Dinge, die wir beachten müssen, bevor wir diese Änderung vornehmen. Erstens ist unsere Funktion derzeit als pure markiert. Wir müssen die Funktion in eine view Funktion umwandeln, da wir nun auf die in der Blockchain gespeicherten Daten zugreifen werden. Sobald wir in unserer Funktion greet dazu übergehen, die Zustandsvariable auszulesen, haben wir unsere Standardbegrüßung nicht mehr und der erste Test wird fehlschlagen. Um diese Probleme zu lösen, geben wir greeting den Standardwert "Hello, World!". Außerdem ändern wir die Funktion von pure in view und aktualisieren den Rückgabewert, um den in greeting gespeicherten Wert zu verwenden. Beispiel 4-10 zeigt unseren Vertrag mit all diesen Änderungen.

Beispiel 4-10. Lesen aus unserer Statusvariablen
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
    string private _greeting = "Hello, World!";

    function greet() external view returns(string memory) {
        return _greeting;
    }

    function setGreeting(string calldata greeting) external {
        _greeting = greeting;
    }

}

Nachdem wir unsere Tests durchgeführt haben, sehen wir, dass alle drei Tests bestanden sind!

$ truffle test
Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (57ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (116ms)


  3 passing (224ms)

Den Greeter besitzbar machen

So wie es jetzt ist, kann jeder die Botschaft unseres Greeter-Vertrags ändern. Das mag in manchen Fällen in Ordnung sein, aber es könnte auch dazu führen, dass jemand die Nachricht in etwas ändert, das weniger einladend ist. Um dies zu verhindern, werden wir den Vertrag um den Begriff des Besitzes erweitern und die Möglichkeit, die Begrüßung zu ändern, auf den Besitzer beschränken.

Zu diesem Zweck wollen wir den Eigentümer des Greeter-Vertrags auf die Adresse setzen, die den Vertrag eingesetzt hat. Das bedeutet, dass wir die Adresse während der Initialisierung speichern müssen, und dafür müssen wir eine Konstruktorfunktion schreiben. Außerdem müssen wir auf einige Informationen aus dem msg Objekt zugreifen. Das Objekt msg ist global verfügbar und enthält die Calldata, den Absender der Nachricht, die Signatur der aufgerufenen Funktion und den Wert (wie viel Wei gesendet wurde).

Unser erster Test soll feststellen, ob ein Eigentümer existiert, indem wir eine owner getter-Funktion aufrufen. Da dies nicht von einer Zustandsänderung abhängt, fügen wir diesen Test in den ersten Greeter-Testblock ein, wie in Beispiel 4-11 gezeigt.

Beispiel 4-11. Testen, ob ein Eigentümer existiert
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", () => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    })
  });

  describe("owner()", () => {
    it("returns the address of the owner", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();

      assert(owner, "the current owner");
    });
  });
})

Wenn du diesen Test durchführst, tritt folgender Fehler auf:

$ truffle test

Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (54ms)
    owner()
      1) returns the address of the owner
    > No events were emitted

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (274ms)


  3 passing (409ms)
  1 failing

  1) Contract: Greeter
       owner()
         returns the address of the owner:
     TypeError: greeter.owner is not a function
      at Context.<anonymous> (test/greeter_test.js:22:35)
      at processTicksAndRejections (internal/process/task_queues.js:85:5)

Dieser Fehler kommt dir bekannt vor. Wir haben in unserem Greeter-Vertrag keine Funktion namens owner definiert. Da es sich um eine Getter-Funktion handelt, müssen wir eine Zustandsvariable hinzufügen, die die Adresse des Eigentümers enthält, und dann sollte unsere Funktion diese Adresse zurückgeben. Die Solidity-Sprache bietet zwei address Typen: address und address payable. Der Unterschied zwischen ihnen besteht darin, dass address payable den Zugriff auf die Methoden transfer und send ermöglicht und dass Variablen dieses Typs auch Äther empfangen können. Da wir keinen Äther an diese Adresse senden, können wir den Typ address für unsere Zwecke verwenden.

Aktualisieren wir nun den Greeter-Vertrag mit diesen Änderungen, wie in Beispiel 4-12 dargestellt.

Beispiel 4-12. Hinzufügen der Eigentumsstatusvariable und der Getter-Funktion
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
  string private _greeting = "Hello, World!";
  address private _owner;

  function greet() external view returns(string memory) {
    return _greeting;
  }

  function setGreeting(string calldata greeting) external {
    _greeting = greeting;
  }

  function owner() public view returns(address) {
    return _owner;
  }
}

Unsere Tests zeigen, dass wir wieder einmal bestanden haben:

Compiling your contracts...
===========================
> Compiling ./contracts/Greeter.sol
> Compiling ./contracts/Migrations.sol



  Contract: Greeter
    ✓ has been deployed successfully
    greet()
      ✓ returns 'Hello, World!' (57ms)
    owner()
      ✓ returns the address of the owner (50ms)

  Contract: Greeter: update greeting
    setGreeting(string)
      ✓ sets greeting to passed in string (125ms)


  4 passing (292ms)

Was wir hier wirklich testen wollen, ist, dass die Eigentümeradresse mit der Bereitstellungsadresse übereinstimmt. Dazu fügen wir jetzt einen Test hinzu, um zu prüfen, ob die Eigentümeradresse mit dem Standardkonto übereinstimmt (dem Konto, das für die Bereitstellung des Vertrags verwendet wird). Dazu benötigen wir Zugriff auf die Konten in unserer Testumgebung. Glücklicherweise hat Truffle diese über die Variable accounts zugänglich gemacht. Diese müssen wir an den Testcode-Block der Ebene contract übergeben, wie in Beispiel 4-13 gezeigt.

Beispiel 4-13. Der Testeigentümer ist derselbe wie der Verteiler
const GreeterContract = artifacts.require("Greeter");

contract("Greeter", (accounts) => {
  it("has been deployed successfully", async () => {
    const greeter = await GreeterContract.deployed();
    assert(greeter, "contract failed to deploy");
  });

  describe("greet()", () => {
    it("returns 'Hello, World!'", async () => {
      const greeter = await GreeterContract.deployed();
      const expected = "Hello, World!";
      const actual = await greeter.greet();

      assert.equal(actual, expected, "greeted with 'Hello, World!'");
    })
  });

  describe("owner()", () => {
    it("returns the address of the owner", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();

      assert(owner, "the current owner");
    });

    it("matches the address that originally deployed the contract", async () => {
      const greeter = await GreeterContract.deployed();
      const owner = await greeter.owner();
      const expected = accounts[0];

      assert.equal(owner, expected, "matches address used to deploy contract");
    });
  });
})

Beachte, dass unser contract Block jetzt einen accounts Parameter an die Funktion übergibt, die unsere Testfälle enthält. Unser neuer Test behauptet dann, dass das erste Konto dasjenige ist, das den Greeter-Vertrag eingesetzt hat. Wenn wir die Tests ausführen, erhalten wir einen Fehler, der uns mitteilt, dass owner und expected nicht übereinstimmen. Jetzt ist es an der Zeit, die Konstruktorfunktion zu schreiben.

Bis jetzt haben wir den Standardkonstruktor constructor() public {} verwendet. Jetzt müssen wir msg.sender als Eigentümer eintragen, wenn der Vertrag initialisiert wird. Unser Greeter-Vertrag sollte nun wie in Beispiel 4-14 aussehen.

Beispiel 4-14. Hinzufügen eines Konstruktors zum Greeter-Vertrag
pragma solidity >= 0.4.0 < 0.7.0;

contract Greeter {
  string private _greeting = "Hello, World!";
  address private _owner;

  constructor() public {
    _owner = msg.sender;
  }

  function greet() external view returns(string memory) {
    return _greeting;
  }

  function setGreeting(string calldata greeting) external {
    _greeting = greeting;
  }

  function owner() public view returns(address) {
    return _owner;
  }
}

Mit dieser Änderung sind unsere Tests wieder erfolgreich.

Da wir nun wissen, wer den Vertrag erstellt hat, können wir eine Einschränkung einrichten, dass nur der Eigentümer die Begrüßung aktualisieren kann. Diese Art der Zugriffskontrolle wird normalerweise mit einem Funktionsmodifikator durchgeführt.

Funktionsmodifikatoren ermöglichen es uns, eine Funktion mit Code zu erweitern, der vor und/oder nach einer Funktion ausgeführt werden kann. Sie haben in der Regel die Form einer Schutzklausel und verhindern, dass die Funktion aufgerufen wird, wenn die Klausel nicht erfüllt ist. Das ist genau das, was wir für unseren Greeter-Vertrag wollen.

Beispiel 4-15 hat die setGreeting Tests aktualisiert und die Erwartung aufgestellt, dass nur der Besitzer die Begrüßung ändern kann.

Beispiel 4-15. Test zur Beschränkung von setGreeting auf den Besitzer
contract("Greeter: update greeting", (accounts) => {
  describe("setGreeting(string)", () => {
    describe("when message is sent by the owner", () => {
      it("sets greeting to passed in string", async () => {
        const greeter = await GreeterContract.deployed()
        const expected = "The owner changed the message";

        await greeter.setGreeting(expected);
        const actual = await greeter.greet();

        assert.equal(actual, expected, "greeting updated");
      });
    });

  describe("when message is sent by another account", () => {
    it("does not set the greeting", async () => {
      const greeter = await GreeterContract.deployed()
      const expected = await greeter.greet();

      try {
        await greeter.setGreeting("Not the owner", { from: accounts[1] });
      } catch(err) {
        const errorMessage = "Ownable: caller is not the owner"
        assert.equal(err.reason, errorMessage, "greeting should not update");
        return;
      }
      assert(false, "greeting should not update");
    });
  });
})

In unseren aktualisierten Tests übergeben wir wieder den Parameter accounts an die contract Callback-Funktion, um sie für unsere Testfälle verfügbar zu machen. Außerdem haben wir einen zweiten Abschnitt für den Fall hinzugefügt, dass ein Nicht-Besitzerkonto die Nachricht sendet. Wir erwarten nun, dass ein Fehler ausgelöst wird, wenn eine Adresse, die nicht dem Eigentümer gehört, versucht, diese Änderung vorzunehmen. Wenn du dir den Aufruf von setGreeting ansiehst, wird jetzt ein zweiter Parameter übergeben; es ist ein Objekt mit einer from Eigenschaft, und wir senden diese Nachricht explizit von einem anderen Konto. Auf diese Weise könnten wir auch eine value (in Einheiten von wei) festlegen, die an den Vertrag gesendet wird.

Die Durchführung unserer Tests sollte zu einem Fehler wie diesem führen:

1 failing

1) Contract: Greeter: update greeting
     setGreeting(string)
       when message is sent by another account
         does not set the greeting:
   AssertionError: greeting should not update

Lass uns unseren Modifikator erstellen und anwenden, um das Problem zu beheben. Füge in unserem Greeter-Vertrag nach dem Konstruktor den folgenden Code ein:

modifier onlyOwner() {
  require(
    msg.sender == _owner,
    "Ownable: caller is not the owner"
  );
  _;
}

Die Syntax des Modifikators ähnelt der Funktionssyntax, allerdings ohne die Sichtbarkeitsdeklaration. In diesem Fall verwendet unser Modifikator die Funktion require, deren erstes Argument ein Ausdruck ist, der einen booleschen Wert ergibt. Wenn dieser Ausdruck ein false ergibt, wird die Transaktion vollständig rückgängig gemacht, d. h. alle Zustandsänderungen werden rückgängig gemacht und die Ausführung des Programms wird beendet. Die Funktion revert nimmt außerdem einen optionalen String-Parameter entgegen, mit dem dem Aufrufer weitere Informationen darüber gegeben werden können, warum der Vorgang fehlgeschlagen ist.

Der letzte Teil unserer Modifizierungsfunktion ist die Zeile _;. In dieser Zeile wird die Funktion aufgerufen, die geändert werden soll. Wenn du etwas nach dieser Zeile einfügst, wird es nach dem Funktionskörper ausgeführt.

Nun aktualisieren wir unsere Funktion setGreeter, um den Modifikator zu verwenden, indem wir die Deklaration onlyOwner nach external in die Funktionsdefinition einfügen:

function setGreeting(string calldata greeting) external onlyOwner {
  _greeting = greeting;
}

Wenn wir unsere Tests durchführen, sehen wir, dass sie alle erfolgreich sind!

Das ist großartig, aber wir werden noch eine letzte Änderung vornehmen, bevor wir dieses Kapitel beenden. In Kapitel 2 haben wir über OpenZeppelin gesprochen und darüber, wie sie Verträge erstellt haben, die als Grundlage für die Erstellung von Token verwendet werden können. Nun, sie haben auch Verträge, die die Idee des Eigentums implementieren, und wir duplizieren einen Teil dieses Verhaltens. Anstatt es zu duplizieren, werden wir unseren Greeter-Vertrag aktualisieren, um ihre Implementierung zu nutzen.

Zurück in unserem Terminal, im Stammverzeichnis unserer Anwendung, gibst du den folgenden Befehl ein:

$ npm install openzeppelin-solidity

Sobald dies abgeschlossen ist, aktualisierst du den oberen Teil des Greeter-Vertrags so, dass er wie in Beispiel 4-16 aussieht.

Beispiel 4-16. Erben von Ownable
pragma solidity >= 0.4.0 < 0.7.0;

import "openzeppelin-solidity/contracts/ownership/Ownable.sol";

contract Greeter is Ownable {
  ...rest of file...

Hier haben wir eine Importanweisung hinzugefügt, die alle globalen Symbole aus der importierten Datei, wie z. B. Ownable, übernimmt und im aktuellen Bereich verfügbar macht. Als Nächstes ist zu beachten, dass unser Greeter-Vertrag jetzt von Ownable über die is Syntax erbt. Solidity unterstützt Mehrfachvererbung ähnlich wie Python oder sogar C++. Die vererbenden Klassen werden mit einem Komma voneinander getrennt aufgelistet.

Damit können wir unsere Implementierungen des onlyOwner Modifikators, der owner Getter-Funktion und der Konstruktorfunktion entfernen, da Ownable diese Definitionen bereitstellt. Das mag übertrieben erscheinen, und normalerweise würde ich diesem Gedanken zustimmen. Aber bei der Arbeit mit intelligenten Verträgen ist es ratsam, Code zu verwenden, der gründlich geprüft und gut getestet wurde, denn die Sicherheit von intelligenten Verträgen ist von entscheidender Bedeutung.

Damit ist unser Greeter-Vertrag abgeschlossen. Aber bevor wir weitermachen, lass uns darüber nachdenken, was wir in diesem Kapitel gelernt haben.

Zusammenfassung

Bisher haben wir gelernt, wie man mit truffle init ein neues Smart-Contract-Projekt erstellt, das die Verzeichnisstruktur für unsere Anwendung bereitstellt. Diese Struktur enthält Verzeichnisse, in denen unsere Verträge, Tests und Migrationen gespeichert werden.

Wir haben einen Deployment-Test geschrieben, der uns bei der anfänglichen Migration half, die nötig war, um unseren Vertrag in das Testnetzwerk zu bringen. So konnten unsere nachfolgenden Tests mit dem bereitgestellten Vertrag interagieren.

Schließlich haben wir uns mit der Solidity-Sprache beschäftigt und die verschiedenen Modifikatoren für die Funktionssichtbarkeit kennengelernt (external, public, internal, private). Diese Modifikatoren stehen auch für Zustandsvariablen zur Verfügung (mit Ausnahme von external), die für die Persistierung von Daten auf der Blockchain verwendet werden. Wir haben auch das Konzept von Ownable implementiert und den OpenZeppelin Vertrag über Vererbung umgestaltet.

Im nächsten Kapitel werden wir uns damit beschäftigen, wie wir unseren Vertrag lokal und in einem der öffentlich zugänglichen Testnetzwerke einsetzen können.

Get Praktische Smart Contract Entwicklung mit Solidity und Ethereum now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.