Pour commencer
Introduction
Ralph est le langage de programmation des contrats intelligents pour la blockchain Alephium, qui se concentre sur trois objectifs : la sécurité, la simplicité et l'efficacité. Ce tutoriel fournit des conseils pour écrire des contrats intelligents Ralph propres, idiomatiques et sécurisés. Nous suivons les principes suivants lors de la conception de Ralph:
- Rendre le DSL des contrats intelligents aussi simple que possible.
- Il devrait y avoir une-- et de préférence une seule --façon évidente de le faire.
- Intégrer de bonnes pratiques.
Types
Ralph est un langage typé statiquement, mais vous n'avez pas besoin de spécifier le type pour les variables locales et les constantes grâce à l'inférence de type. Tous les types de Ralph sont des types de valeurs, c'est-à-dire qu'ils sont toujours copiés lorsqu'ils sont utilisés comme arguments de fonction ou assignés. Actuellement, Ralph ne prend en charge que les types de données suivants:
Types primitifs
U256
I256
// Le type de `a` ... `d` est I256.
let a = -10
let b = 10i
let c = -1_000_000_000
let d = -1e18
Bool
ByteVec
// Les littéraux de ByteVec doivent commencer par `#` suivi d'une chaîne hexadécimale.
let a = #00112233
// Concaténation de ByteVec
let b = #0011 ++ #2233 // `b` est #00112233
// ByteVec vide
let c = #
Address
// Les littéraux d'adresse doivent commencer par `@` suivi d'une adresse Alephium valide encodée en base58.
let a = @1DrDyTr9RpRsQnDnXo2YRiPzPW4ooHX5LLoqXrqfMrpQH
String
Ralph n'a pas de type natif pour les chaînes de caractères, mais vous pouvez définir des littéraux de chaîne de caractères qui sont encodés en ByteVec
.
// Les littéraux de chaîne de caractères commencent par `b`.
let a = b`Hello`
let b = b`World`
let c = a ++ b` ` ++ b
Tableau de taille fixe
La syntaxe des tableaux de taille fixe est influencée par Rust.
// Le type de `a0` est [U256; 4]
let a0 = [0, 1, 2, 3]
// Le type de`a1` est [[U256, 2]; 2]
let a1 = [[0, 1], [2, 3]]
// Le type de `a2` est [I256; 3]
let a2 = [0i; 3]
// Le type de `a3` est [ByteVec; 4]
let a3 = [#00, #11, #22, #33]
Struct
Dans Ralph, les structures peuvent être définies globalement et peuvent contenir des champs qui
sont soit mutables soit immuables. Cependant, pour attribuer une valeur à un champ, tous les sélecteurs
de champs doivent être mutables. Par exemple, pour défini foo.x.y.z = 123
, tous les éléments
foo
, x
, y
, and z
doivent être mutables.
// Les structures doivent être définies globalement
struct Foo { x: U256, mut y: U256 }
struct Bar { z: U256, mut foo: U256 }
Contract Baz() {
...
// f.y = 3 ne fonctionnera pas car f est immuable malgré que le champ y soit mutable
let f = Foo { x: 1, y: 2 }
// ff = f ne fonctionnera pas car ff.x est immuable malgré que ff et ff.y soient mutables
let mut ff = Foo { x: 1, y: 2 }
ff.y = 3 // Cela fonctionne car ff et y sont mutables
// b.foo.y = 5 ne fonctionnera pas car b est immuable
let b = Bar { z: 4, foo: f }
let mut bb = Bar { z: 5, foo: f }
bb.foo.y = 6 // Cela fonctionne car bb, foo et y sont tous mutables
...
}
Map (Mise à jour Rhone uniquement)
La structure de données de type carte est actuellement disponible exclusivement dans la version devnet release du nœud complet.
Dans Ralph, les cartes sont définies comme attributs globaux de contrat, éliminant le besoin d'initialisation. Sous le capot, chaque entrée de carte est construite comme un sous-contrat du contrat actuel. Ainsi, créer une entrée de carte implique un dépôt de contrat minimal, facilement réalisable à l'aide de la fonction intégrée mapEntryDeposit!()
.
Il y a 3 méthodes intégrées essentielles pour les cartes: insert!, remove!, contains!
. Les valeurs des cartes peuvent être consultées et mises à jour avec la syntaxe entre crochets map[key] = newValue
. Voici quelques exemples illustrant leur utilisation. Pour des exemples plus complets, référez-vous au dépôt blind-auction et aux tests unitaires ici.
Contract Foo() {
// Toutes les cartes doivent être définies ici avec `mapping[KeyType, ValueType]`, avant les événements et les constantes
mapping[Address, U256] counters
pub fn create() -> () {
let key = callerAddress!()
// Chaque création d'entrée de carte nécessite un dépôt minimal en ALPH
counters.insert!{key -> ALPH: mapEntryDeposit!()}(key, 0)
}
pub fn count() -> () {
let key = callerAddress!()
let value = counters[key]
// Mise à jour de la valeur de l'entrée de carte
counters[key] = value + 1
}
pub fn clear() -> U256 {
let key = callerAddress!()
let depositRecipient = key
let value = counters[key]
// Chaque suppression d'entrée de carte rembourse le dépôt de l'entrée de carte
counters.remove!(key, depositRecipient)
return value
}
pub fn contains() -> Bool {
// Vérifie l'existence d'une entrée de carte
return counters.contains!(callerAddress!())
}
}
Opérateurs
Opérateurs arithmétiques
Il existe des opérateurs arithmétiques tels que +
, -
, *
, /
, **
, et %
disponibles pour les types I256
et U256
, et ces opérateurs vérifient le dépassement de capacité à l'exécution.
Par exemple, les expressions suivantes provoqueront une erreur de la VM à l'exécution:
u256Max!() + 1 // dépassement de capacité
0 - 1 // dépassement de capacité
i256Max!() + 1i // dépassement de capacité
i256Min!() - 1i // dépassement de capacité
Opérateurs modulo 2^256
Pour le type U256
, il y a des opérateurs arithmétiques modulo 2^256
: |+|, |-|, |*|, |**|
.Voici quelques exemples:
assert!(u256Max!() |+| 1 == 0, 0) // addition modulo 2^256
assert!(0 |-| 1 == u256Max!(), 0) // soustraction modulo 2^256
assert!(u256Max!() |*| 2 == u256Max!() - 1, 0) // multiplication modulo 2^256
assert!((1 << 128) |**| 2 == 0, 0) // puissance modulo 2^256
Opérateurs modulo N
La VM fournit également deux fonctions modulo avancées pour le type U256
, addModN
et mulModN
:
assert!(mulModN!(2, 3, 4) == 2, 0) // (2 * 3) % 4
assert!(mulModN!(1 << 128, 1 << 128, u256Max!() - 1) == 2, 0)
assert!(mulModN!(u256Max!(), u256Max!(), u256Max!()) == 0, 0)
assert!(addModN!(2, 3, 4) == 1, 0) // (2 + 3) % 4
assert!(addModN!(1 << 128, 1 << 128, u256Max!()) == 1 << 129, 0)
assert!(addModN!(u256Max!(), u256Max!(), u256Max!()) == 0, 0)
Opérateurs de bits
Ralph prend en charge les opérateurs de bits tels que &
, |
, ^
, <<
, et >>
uniquement pour le type U256
, voici quelques exemples:
assert!(0xff & 0xf0 == 0xf0, 0)
assert!(0xff | 0xf0 == 0xff, 0)
assert!(0xff ^ 0xf0 == 0x0f, 0)
assert!(0xff << 8 == 0xff00, 0)
assert!(u256Max!() << 2 == u256Max!() - 3, 0)
assert!(0xff >> 4 == 0x0f, 0)
Opérateurs de comparaison
Il existe des opérateurs d'inégalité tels que <
, >
, <=
, et >=
disponibles pour les types I256
et U256
, et des opérateurs d'égalité ==
, !=
pour tous les types primitifs.
Notez que les opérateurs de comparaison ne peuvent pas être utilisés avec les types de tableau ou de structure.
Opérateurs logiques
Similaire à d'autres langages de programmation, Ralph prend également en charge les opérateurs logiques tels que &&
, ||
, et !
.
Opérateurs ByteVec
Ralph possède un opérateur spécial ++
fpour la concaténation de ByteVec
:
Fonctions
Les fonctions sont les unités exécutables de code, vous pouvez également définir des fonctions à l'intérieur d'un contrat.
Signatures de fonction
// Fonction publique, qui peut être appelée par n'importe qui
pub fn foo() -> ()
// Fonction privée, qui ne peut être appelée que à l'intérieur du contrat
fn foo() -> ()
// La fonction prend 1 paramètre et n'a pas de valeurs de retour
fn foo(a: U256) -> ()
// La fonction prend 2 paramètres et renvoie 1 valeur
fn foo(a: U256, b: Boolean) -> U256
// La fonction prend 2 paramètres et renvoie plusieurs valeurs
fn foo(a: U256, b: Boolean) -> (U256, ByteVec, Address)
Variables locales
Une fonction ne peut pas avoir de définitions de variables en double, et le nom de la variable dans la fonction ne peut pas être le même que le nom du champ de contrat.
fn foo() -> () {
// `a` est immuable, et il ne peut pas être réassigné
let a = 10
a = 9 // ERROR
// `b` est mutable, et il peut être réassigné
let mut b = 10
b = 9
}
fn bar() -> (U256, Boolean) {
return 1, false
}
fn baz() -> () {
// `a` et `b` sont tous deux immuables
let (a, b) = bar()
// `c` est immuable, mais `d` est mutable
let (c, mut d) = bar()
// Ignorer la première valeur de retour de la fonction `bar`
let (_, e) = bar()
}
Structures de contrôle
Instructions de retour
Instructions de condition if-else/expressions
fn foo() -> ByteVec {
// Instruction if else
if (a == 0) {
return #00
} else if (a == 1) {
return #01
} else {
return #02
}
}
fn foo() -> ByteVec {
return if (a == 0) #00 else if (a == 1) #01 else #02
}
For loop
// Boucle for
fn foo() -> () {
for (let mut index = 0; index <= 4; index = index + 1) {
bar(index)
}
}
While loop
// Boucle While
fn foo() -> () {
let mut index = 0
while (index <= 4) {
bar(index)
index += 1
}
}
Note
Les instructions break
et continue
ne sont pas prises en charge dans les boucles for
et while
car elles peuvent être une mauvaise pratique dans certains cas. Il est recommandé de les remplacer par un return
anticipé ou unefonction assert.
Note
En Ralph, chaque fonction a seulement une portée, donc vous ne pouvez pas définir de variables dupliquées dans le bloc while
ou for
:
Gestion des erreurs
Ralph fournit deux fonctions d'assertion intégrées pour la gestion des erreurs: assert! et panic!. L'échec de l'assertion annulera tous les changements apportés à l'état mondial par la transaction et arrêtera immédiatement l'exécution de la transaction.
enum ErrorCodes {
InvalidContractState = 0
}
fn foo(cond: Boolean) -> () {
// Cela arrêtera la transaction si `cond` est faux.
// Le client Alephium renverra le code d'erreur si la transaction échoue.
assert!(cond, ErrorCodes.InvalidContractState)
}
fn bar(cond: Boolean) -> U256 {
if (!cond) {
// La différence entre `panic!` et `asset!` est que le type de retour de `panic!` est de type bottom.
panic!(ErrorCodes.InvalidContractState)
}
return 0
}
Appels de fonctions
Les fonctions du contrat actuel peuvent être appelées directement ('en interne') ou récursivement :
Contract Foo() {
fn foo(v: U256) -> () {
if (v == 0) {
return
}
// Appel de fonction interne
bar()
// Appel de fonction récursive
foo(v - 1)
}
fn bar() -> () {
// ...
}
}
Les fonctions peuvent également être appelées de manière externe en utilisant la notation bar.func()
, où bar
est une instance de contrat et func
est une fonction appartenant à bar
:
Contract Bar() {
pub fn func() -> U256 {
// ...
}
}
Contract Foo() {
pub fn foo() -> () {
// Instancier le contrat à partir de l'identifiant du contrat
let bar = Bar(#15be9537456726c336a3cd1aa36074759c457f151ac253a500085920afe3838a)
// Appel externe
let a = bar.func()
// ...
}
}
Fonctions intégrées
Ralph fournit de nombreuses fonctions intégrées, vous pouvez vous référer à ici.
Annotations
La fonction Ralph prend également en charge les annotations, actuellement la seule annotation valide est l'annotation @using
, et les annotations définies par l'utilisateur seront prises en charge à l'avenir si nécessaire.
L'annotation @using
comporte quatre champs facultatifs :
preapprovedAssets = true/false
: indique si la fonction utilise des actifs approuvés par l'utilisateur. La valeur par défaut estfalse
pour les contrats,true
pour les scripts.assetsInContract = true/false
: indique si la fonction utilise des actifs de contrat. La valeur par défaut estfalse
pour les contrats.checkExternalCaller = true/false
: indique si la fonction vérifie l'appelant. La valeur par défaut esttrue
pour les contrats.updateFields = true/false
: indique si la fonction modifie les champs du contrat. La valeur par défaut estfalse
pour les contrats.
Utilisation d'actifs approuvés
En Ralph, si une fonction utilise des actifs, l'appelant doit explicitement approuver les actifs. Et toutes les fonctions dans la pile d'appels doivent être annotées avec @using(preapprovedAssets = true)
.
Contract Foo() {
// La fonction `foo` utilise des actifs approuvés, et elle transférera 1 ALPH et 1 jeton au contrat depuis l'appelant.
@using(preapprovedAssets = true)
fn foo(caller: Address, tokenId: ByteVec) -> () {
transferAlphToSelf!(caller, 1 alph)
transferTokenToSelf!(caller, tokenId, 1)
}
@using(preapprovedAssets = true)
fn bar(caller: Address, tokenId: ByteVec) -> () {
// Nous devons explicitement approuver les actifs lors de l'appel de la fonction `foo`.
foo{caller -> 1 alph, tokenId: 1}(caller, tokenId)
// ...
}
}
Pour l'annotation preapprovedAssets
, le compilateur effectuera les vérifications suivantes :
- Si une fonction est annotée
preapprovedAssets = true
mais n'utilise pas la syntaxe entre crochets, le compilateur signalera une erreur. - Si un appel de fonction utilise la syntaxe entre crochets mais que la fonction n'est pas annotée
preapprovedAssets = true
, le compilateur signalera une erreur.
Utilisation des actifs de contrat
Contract Foo() {
// La fonction `foo` utilise les actifs du contrat, et elle transférera 1 alph à l'appelant.
@using(assetsInContract = true)
fn foo(caller: Address) -> () {
transferAlphFromSelf!(caler, 1 alph)
}
// La fonction `bar` NE DOIT PAS être annotée avec `@using(assetsInContract = true)`
// parce que les actifs du contrat seront supprimés après utilisation.
fn bar(caller: Address) -> () {
// ...
foo(caller)
}
}
Pour l'annotation assetsInContract
, le compilateur effectuera les vérifications suivantes :
- Si une fonction est annotée
assetsInContract = true
mais n'utilise pas les actifs de contrat, le compilateur signalera une erreur.
Vous pouvez trouver plus d'informations sur les permissions d'actifs ici.
Mise à jour des champs
Les fonctions qui mettent à jour les champs modifieront les champs actuels du contrat. Si une fonction modifie les champs du contrat mais n'a pas l'annotation @using(updateFields = true)
, le compilateur signalera un avertissement ; si une fonction ne modifie pas les champs du contrat mais est annotée avec @using(updateFields = true)
, le compilateur signalera également un avertissement.
Contract Foo(a: U256, mut b: Boolean) {
// La fonction `f0` ne modifie pas les champs du contrat
fn f0() -> U256 {
return a
}
// La fonction `f1` modifie les champs du contrat
@using(updateFields = true)
fn f1() -> () {
b = false
}
// La fonction `f2` appelle la fonction `f1`, même si la fonction `f1` modifie les champs du contrat,
// la fonction `f2` n'a toujours pas besoin d'être annotée avec `@using(updateFields = true)`,
// parce que la fonction `f2` ne modifie pas directement les champs du contrat
fn f2() -> () {
f1()
}
}
Vérification de l'appelant externe
Dans les contrats intelligents, nous devons souvent vérifier si l'appelant de la fonction du contrat est autorisé. Pour éviter les bugs causés par des appelants non autorisés, le compilateur signalera des avertissements pour toutes les fonctions publiques qui ne vérifient pas les appelants externes. L'avertissement peut être supprimé avec l'annotation @using(checkExternalCaller = false)
.
Le compilateur ignorera la vérification des fonctions de vue simples. Une fonction de vue simple doit satisfaire toutes les conditions suivantes :
- Elle ne peut pas modifier les champs du contrat.
- Elle ne peut pas utiliser d'actifs.
- Tous les appels de sous-fonctions doivent également être des fonctions de vue simples.
Pour vérifier l'appelant d'une fonction, la fonction intégrée checkCaller! doit être utilisée.
Contract Foo(barId: ByteVec, mut b: Boolean) {
enum ErrorCodes {
InvalidCaller = 0
}
// Nous n'avons pas besoin d'ajouter `@using(checkExternalCaller = true)` parce que
// `checkExternalCaller` est true par défaut pour les fonctions publiques.
pub fn f0() -> () {
// La fonction `checkCaller!` intégrée est utilisée pour vérifier si l'appelant est valide.
checkCaller!(callerContractId!() == barId, ErrorCodes.InvalidCaller)
b = !b
// ...
}
// Le compilateur signalera des avertissements pour la fonction `f1`
pub fn f1() -> () {
b = !b
// ...
}
// La fonction `f2` est une fonction de vue simple, nous n'avons pas besoin d'ajouter le
// `using(checkExternalCaller = false)` pour les fonctions de vue simples.
pub fn f2() -> ByteVec {
return barId
}
// Le compilateur ne signalera PAS d'avertissements car nous avons vérifié l'appelant dans la fonction `f4`.
pub fn f3() -> () {
f4(callerContractId!())
// ...
}
fn f4(callerContractId: ByteVec) -> () {
checkCaller!(callerContractId == barId, ErrorCodes.InvalidCaller)
// ...
}
}
Il y a un autre scénario où le compilateur signalera des avertissements si un contrat appelle une fonction via une interface, cela est dû au fait que nous ne savons pas si l'implémentation de la fonction doit vérifier l'appelant externe:
Interface Bar() {
pub fn bar() -> ()
}
Contract Foo() {
// Le compilateur signalera des avertissements pour la fonction `Foo.foo`
pub fn foo(barId: ByteVec) -> () {
Bar(barId).bar()
}
}
Contrats
Info
Chaque contrat d'Alephium possède 3 formes d'identification uniques:
- Address: chaque contrat a une adresse unique
- ID du contrat: chaque contrat a un ID de contrat unique
- ID du jeton: chaque contrat peut émettre un jeton avec le même ID que son propre ID de contrat
En Ralph, l'ID du contrat est utilisé plus fréquemment. Les IDs de contrat peuvent être convertis à partir/de autres formes avec les fonctions intégrées de Ralph ou le SDK web3.
Les contrats en Ralph sont similaires aux classes dans les langages orientés objet. Chaque contrat peut contenir des déclarations de champs de contrat, d'événements, de constantes, d'énumérations et de fonctions. Toutes ces déclarations doivent être à l'intérieur d'un contrat. De plus, les contrats peuvent hériter d'autres contrats.
// Ceci est un commentaire, et actuellement Ralph ne prend en charge que les commentaires sur une ligne.
// Les noms de contrat doivent être en camel case supérieur.
// Les champs de contrat sont stockés de manière permanente dans le stockage du contrat.
Contract MyToken(supply: U256, name: ByteVec) {
// Les événements doivent être nommés en camel case supérieur.
// Les événements permettent de journaliser des activités sur la blockchain.
// Les applications peuvent écouter ces événements via l'API REST d'un client Alephium.
event Transfer(to: Address, amount: U256)
// Les variables constantes doivent être nommées en camel case supérieur.
const Version = 0
// Les énumérations peuvent être utilisées pour créer un ensemble fini de valeurs constantes.
enum ErrorCodes {
// Les constantes d'énumération doivent être nommées en camel case supérieur.
InvalidCaller = 0
}
// Les fonctions, paramètres et variables locales doivent être nommés en camel case inférieur.
pub fn transferTo(toAddress: Address) -> () {
let payloadId = #00
// ...
}
}
Champs
Les champs de contrat sont stockés de manière permanente dans le stockage du contrat, et les champs peuvent être modifiés par le code du contrat. Les applications peuvent obtenir les champs de contrat via l'API REST d'un client Alephium.
// Le contrat `Foo` a deux champs:
// `a`: immuable, il ne peut pas être modifié par le code du contrat
// `b`: mutable, il peut être modifié par le code du contrat
Contract Foo(a: U256, mut b: Boolean) {
// ...
}
// Les champs de contrat peuvent également être d'autres contrats.
// Il stockera l'ID de contrat de `Bar` dans le stockage du contrat de `Foo`.
Contract Foo(bar: Bar) {
// ...
}
Contract Bar() {
// ...
}
Fonctions Intégrées de Contrat
Parfois, nous devons créer un contrat à l'intérieur d'un contrat, et dans de tels cas, nous devons encoder les champs du contrat en ByteVec
. Ralph fournit une fonction intégrée appelée encodeFields
qui peut être utilisée pour encoder les champs de contrat en ByteVec
.
Le type de paramètre de la fonction encodeFields
est une liste des types des champs du contrat, disposés dans l'ordre de leurs définitions. Et la fonction renvoie deux valeurs ByteVec
, où la première est les champs immuables encodés, et la seconde est les champs mutables encodés.
Voici un exemple:
Contract Foo(a: U256, mut b: I256, c: ByteVec, mut d: Bool) {
// fonctions
}
Contract Bar() {
@using(preapprovedAssets = true)
fn createFoo(caller: Address, fooBytecode: ByteVec, a: U256, b: I256, c: ByteVec, d: Bool) -> (ByteVec) {
let (encodedImmFields, encodedMutFields) = Foo.encodeFields!(a, b, c, d)
return createContract!{caller -> 1 alph}(fooBytecode, encodedImmFields, encodedMutFields)
}
}
Événements
Les événements sont des signaux envoyés que les contrats peuvent déclencher. Les applications peuvent écouter ces événements via l'API REST d'un client Alephium.
Contract Token() {
// Le nombre de champs d'événement ne peut pas être supérieur à 8
event Transfer(to: Address, amount: U256)
@using(assetsInContract = true)
pub fn transfer(to: Address) -> () {
transferTokenFromSelf!(selfTokenId!(), to, 1)
// Émettre l'événement
emit Transfer(to, 1)
}
}
Sous-contrat
La machine virtuelle d'Alephium prend en charge les sous-contrats. Les sous-contrats peuvent être utilisés comme une structure de données similaire à une carte mais ils sont moins sujets au problème de gonflement de l'état. Un sous-contrat peut être créé par un contrat parent avec un chemin de sous-contrat unique.
Contract Bar(value: U256) {
pub fn getValue() -> U256 {
return value
}
}
Contract Foo(barTemplateId: ByteVec) {
event SubContractCreated(key: U256, contractId: ByteVec)
@using(preapprovedAssets = true, checkExternalCaller = false)
pub fn set(caller: Address, key: U256, value: U256) -> () {
let path = toByteVec!(key)
let (encodedImmFields, encodedMutFields) = Bar.encodeFields!(value) // Le contrat `Bar` a seulement un champ
// Crée un sous-contrat à partir de la clé et de la valeur données.
// L'identifiant du sous-contrat est `blake2b(blake2b(selfContractId!() ++ path))`.
// Cela échouera si le sous-contrat existe déjà.
let contractId = copyCreateSubContract!{caller -> ALPH: 1 alph}(
path,
barTemplateId,
encodedImmFields,
encodedMutFields
)
emit SubContractCreated(key, contractId)
}
pub fn get(key: U256) -> U256 {
let path = toByteVec!(key)
// Obtenez l'identifiant du sous-contrat avec la fonction intégrée `subContractId!`
let contractId = subContractId!(path)
return Bar(contractId).getValue()
}
}
Création de Contrats à l'intérieur d'un Contrat
Ralph prend en charge la création de contrats de manière programmatique à l'intérieur des contrats, Ralph fournit quelques fonctions intégrées pour créer des contrats, vous pouvez trouver plus d'informations ici.
Si vous souhaitez créer plusieurs instances d'un contrat, vous devriez utiliser les fonctions intégrées copyCreateContract!
, ce qui réduira considérablement le stockage on-chain et les frais de gaz de transaction.
Contract Foo(a: ByteVec, b: Address, mut c: U256) {
// ...
}
// Nous voulons créer plusieurs instances du contrat `Foo`.
// Tout d'abord, nous devons déployer un contrat modèle de `Foo`, dont l'identifiant de contrat est `fooTemplateId`.
// Ensuite, nous pouvons utiliser `copyCreateContract!` pour créer plusieurs instances.
TxScript CreateFoo(fooTemplateId: ByteVec, a: ByteVec, b: Address, c: U256) {
let (encodedImmFields, encodedMutFields) = Foo.encodeFields!(a, b, c)
copyCreateContract!(fooTemplateId, encodedImmFields, encodedMutFields)
}
Migration
Les contrats d'Alephium peuvent être mis à niveau avec deux fonctions de migration: migrate! et migrateWithFields!. Voici les trois façons typiques de les utiliser :
fn upgrade(newCode: ByteVec) -> () {
checkOwner(...)
migrate!(newCode)
}
fn upgrade(newCode: ByteVec, newImmFieldsEncoded: ByteVec, newMutFieldsEncoded: ByteVec) -> () {
checkOwner(...)
migrateWithFields!(newCode, newImmFieldsEncoded, newMutFieldsEncoded)
}
fn upgrade(newCode: ByteVec) -> () {
checkOwner(...)
let (newImmFieldsEncoded, newMutFieldsEncoded) = ContractName.encodeFields!(newFields...)
migrateWithFields!(newCode, newImmFieldsEncoded, newMutFieldsEncoded)
}
Héritage
Ralph prend également en charge l'héritage multiple, lorsqu'un contrat hérite d'autres contrats, un seul contrat est créé sur la blockchain, et le code de tous les contrats parent est compilé dans le contrat créé.
Abstract Contract Foo(a: U256) {
pub fn foo() -> () {
// ...
}
}
Abstract Contract Bar(b: ByteVec) {
pub fn bar() -> () {
// ...
}
}
// Le nom du champ du contrat enfant doit être le même que le nom du champ des contrats parent.
Contract Baz(a: U256, b: ByteVec) extends Foo(a), Bar(b) {
pub fn baz() -> () {
foo()
bar()
}
}
Note
Dans Ralph, les contrats abstraits ne sont pas instanciables, ce qui signifie que le code suivant est invalide :
Interface
Les interfaces sont similaires aux contrats abstraits avec les restrictions suivantes:
- Elles ne peuvent avoir aucune fonction implémentée.
- Elles ne peuvent pas hériter d'autres contrats, mais elles peuvent hériter d'autres interfaces.
- Elles ne peuvent pas déclarer de champs de contrat.
- Les contrats ne peuvent implémenter qu'une seule interface.
Interface Foo {
event E(a: U256)
@using(assetsInContract = true)
pub fn foo() -> ()
}
Interface Bar extends Foo {
pub fn bar() -> U256
}
Contract Baz() implements Bar {
// La signature de la fonction doit être la même que celle déclarée dans l'interface.
@using(assetsInContract = true)
pub fn foo() -> () {
// Hérite de l'événement de `Foo`
emit E(0)
// ...
}
pub fn bar() -> U256 {
// ...
}
}
Et vous pouvez instancier un contrat avec une interface:
La raison pour laquelle un contrat ne peut implémenter qu'une seule interface dans Ralph est que, lors de l'appel des méthodes de contrat, Ralph utilise des indices de méthode pour charger et appeler les méthodes de contrat. Si nous permettons à un contrat d'implémenter plusieurs interfaces, l'appel des méthodes de contrat via l'interface peut entraîner l'utilisation du mauvais indice de méthode. Par exemple :
Interface Foo {
pub fn foo() -> ();
}
Interface Bar {
pub fn bar() -> ();
}
Contract Baz() implements Foo, Bar {
pub fn foo() -> () {}
pub fn bar() -> () {}
}
Dans ce cas, à la fois Foo(bazContractId).foo()
et Bar(bazContractId).bar()
utiliseraient l'indice de méthode 0 pour appeler le contrat Baz
.
Note
Le déploiement d'un contrat nécessite le dépôt d'une certaine quantité d'ALPH dans le contrat (actuellement 1 ALPH), donc la création d'un grand nombre de sous-contrats n'est pas pratique.
TxScript
Un script de transaction est un morceau de code pour interagir avec les contrats sur la blockchain. Les scripts de transaction peuvent utiliser les actifs d'entrée des transactions en général. Un script est jetable et ne sera exécuté qu'une seule fois avec la transaction porteuse.
Contract Foo() {
pub fn foo(v: U256) -> () {
// ...
}
}
// Le `preapprovedAssets` est vrai par défaut pour `TxScript`.
// Nous définissons le `preapprovedAssets` sur faux car le script n'a pas besoin d'actifs.
@using(preapprovedAssets = false)
// Les champs de `TxScript` sont plus comme des paramètres de fonction, et ces
// champs doivent être spécifiés à chaque fois que le script est exécuté..
TxScript Main(fooId: ByteVec) {
// Le corps de `TxScript` se compose d'instructions
bar()
Foo(fooId).foo(0)
// Vous pouvez également définir des fonctions dans `TxScript`
fn bar() -> () {
// ...
}
}