Aller au contenu

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:

  1. Rendre le DSL des contrats intelligents aussi simple que possible.
  2. Il devrait y avoir une-- et de préférence une seule --façon évidente de le faire.
  3. 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

Rust
//  Le type de `a` ... `d` est U256.
let a = 10
let b = 10u
let c = 1_000_000_000
let d = 1e18

I256

Rust
// Le type de `a` ... `d` est I256.
let a = -10
let b = 10i
let c = -1_000_000_000
let d = -1e18

Bool

Rust
// Le type de `a` et `b` est Bool.
let a = false
let b = true

ByteVec

Rust
// 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

Rust
// 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.

Rust
// 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.

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.

Rust
// 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.

Rust
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:

Rust
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:

Rust
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:

Rust
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:

Rust
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:

Rust
assert!(#00 ++ #11 == #0011, 0)

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

Rust
// 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.

Rust
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

Rust
fn foo() -> (U256, Boolean, ByteVec) {
  return 1, false, #00
}

Instructions de condition if-else/expressions

Rust
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

Rust
// Boucle for
fn foo() -> () {
  for (let mut index = 0; index <= 4; index = index + 1) {
    bar(index)
  }
}

While loop

Rust
// 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:

Rust
let value = 0
while (true) {
  let value = 0 // ERREUR, définitions de variables dupliquées
  // ...
}
C'est une conception intentionnelle car l'occultation de variables n'est généralement pas une bonne pratique.

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.

Rust
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 :

Rust
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:

Rust
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 est false pour les contrats, true pour les scripts.
  • assetsInContract = true/false: indique si la fonction utilise des actifs de contrat. La valeur par défaut est false pour les contrats.
  • checkExternalCaller = true/false: indique si la fonction vérifie l'appelant. La valeur par défaut est true pour les contrats.
  • updateFields = true/false: indique si la fonction modifie les champs du contrat. La valeur par défaut est false 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).

Rust
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 :

  1. Si une fonction est annotée preapprovedAssets = true mais n'utilise pas la syntaxe entre crochets, le compilateur signalera une erreur.
  2. 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

Rust
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 :

  1. 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.

Rust
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 :

  1. Elle ne peut pas modifier les champs du contrat.
  2. Elle ne peut pas utiliser d'actifs.
  3. 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.

Rust
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:

Rust
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:

  1. Address: chaque contrat a une adresse unique
  2. ID du contrat: chaque contrat a un ID de contrat unique
  3. 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.

Rust
// 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.

Rust
// 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:

Rust
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.

Rust
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.

Rust
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.

Rust
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 :

Rust
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éé.

Rust
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 :

Rust
let bazId = // The contract id of `Baz`
Foo(bazId).foo() // ERROR

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.
Rust
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:

Rust
let bazId = // L'identifiant de contrat de `Baz`
Foo(bazId).foo()
let _ = Bar(bazId).bar()

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 :

Text Only
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.

Rust
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() -> () {
    // ...
  }
}