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:
$
touchtest
/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"
)
;
contract
(
"Greeter"
,
(
)
=>
{
it
(
"has been deployed successfully"
,
async
(
)
=>
{
const
greeter
=
await
GreeterContract
.
deployed
(
)
;
assert
(
greeter
,
"contract was not deployed"
)
;
}
)
;
}
)
;
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.Truffle-Tests verwenden Mocha, aber mit einem kleinen Unterschied. Die Funktion
contract
funktioniert ähnlich wie die eingebaute Funktiondescribe
, 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.Jede Interaktion mit der Blockchain wird asynchron sein. Deshalb werden wir statt Promises und der Methode
Promise.prototype.then
die Vorteile der asynchronen/await-Syntax nutzen, die jetzt in JavaScript verfügbar ist.Wenn der Begrüßer wahrheitsgemäß ist (existiert), besteht unser Test.
Wenn wir unsere Tests durchführen, erhalten wir eine Fehlermeldung, die wie folgt aussieht:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Migrations.solError: 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
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.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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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!
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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:
$
truffletest
Compiling your contracts...
===========================
>
Compiling ./contracts/Greeter.sol>
Compiling ./contracts/Migrations.solContract: 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.solContract: 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.