Aller au contenu

Créer une dApp à partir de zéro

Ce guide explorera les bases de la création d'un projet d'application décentralisée (dApp) sur Alephium.

Prérequis

  • Écrire du code en Typescript
  • Opérer dans un Terminal
  • Version de nodejs >= 16 installée
  • Version de npm >= 8 installée

Créez un nouveau projet de dApp: Distributeur de jetons

Dans ce tutoriel, nous écrirons notre première dApp: un distributeur de jetons.

Le code ici est tiré de notre page de démarrage, mais nous verrons étape par étape comment construire ce guide.

Créez un nouveau dossier de projet et naviguez dedans :

Bash
mkdir alephium-faucet-tuto
cd alephium-faucet-tuto

Créons maintenant un dossier contracts où nous stockerons tous nos contrats:

Bash
mkdir contracts

Notre premier contrat sera token.ral que vous pouvez trouver ici. Vous pouvez copier l'intégralité du fichier dans votre dossiercontracts.

Examinons-le, morceau par morceau :

token.ral
Rust
import "std/fungible_token_interface"

Contract TokenFaucet(
    symbol: ByteVec,
    name: ByteVec,
    decimals: U256,
    supply: U256,
    mut balance: U256
) implements IFungibleToken {

Les quatre premiers champs seront des valeurs immuables qui stockent les données nécessaires pour servir notre IFungibleToken interface. mut balance est une valeur mutable qui garde une trace du nombre de jetons restants dans ce distributeur.

Vous pouvez voir que notre contrat émet un event et définit un code d'error. Lisez ce qui suit pour plus d'informations sur les événements et la gestion des erreurs.

Cela est suivi de 5 méthodes d'accès pour les différents arguments du contrat.

La dernière méthode est là où la magie opère :

token.ral
Rust
@using(assetsInContract = true, updateFields = true, checkExternalCaller = false)
pub fn withdraw(amount: U256) -> () {
    // Les événements de débogage peuvent être utiles pour l'analyse des erreurs
    emit Debug(`The current balance is ${balance}`)

    // Assurez-vous que le montant est valide
    assert!(amount <= 2, ErrorCodes.InvalidWithdrawAmount)
    // Les fonctions suffixées par `!` sont des fonctions intégrées.
    transferTokenFromSelf!(callerAddress!(), selfTokenId!(), amount)
    // Ralph n'autorise pas le débordement.
    balance = balance - amount

    // Émettre l'événement défini précédemment.
    emit Withdraw(callerAddress!(), amount)
}

Avec assert!, nous nous assurons que personne ne prend plus de 2 jetons en même temps.
transferTokenFromSelf effectuera en fait le transfert des jetons.
Nous mettons à jour le champ mut balance avec le nouveau solde. En cas de débordement, une erreur sera déclenchée et la transaction ne sera pas effectuée. callerAddress!() et selfTokenId!() sont des fonctions intégrées, vous pouvez en lire plus à leur sujet sur notre page des fonctions intégrées.


Compilez votre contrat

Le compilateur doit contacter le nœud complet pour compiler le contrat, vous devrez utiliser les bonnes informations définies lors de la création de votre devnet. Si vous ne l'avez pas encore démarré, c'est le moment. Nous définissons l'URL du nœud en utilisant le fichier de configuration suivant: alephium.config.ts. Créez ce fichier dans le répertoire racine de votre projet et collez le code suivant:

alephium.config.ts
TypeScript
import { Configuration } from '@alephium/cli'

export type Settings = {}

const configuration: Configuration<Settings> = {
  networks: {
    devnet: {
      // Assurez-vous que les deux valeurs correspondent à ce qui est dans votre configuration devnet
      nodeUrl: 'http://localhost:22973',
      networkId: 2
    }
  }
}

export default configuration

Maintenant, compilons :

Bash
npx @alephium/cli@latest compile

Il se peut qu'il vous demande une confirmation pour installer le dernier paquet @alephium/cli. Sélectionnez oui pour continuer.

Une fois que la commande ci-dessus réussit, vous remarquerez qu'un nouveau dossier appelé artifacts a été créé. IIl contient plusieurs fichiers liés à votre contrat. Par exemple, artifacts/ts/TokenFaucet.ts génère de nombreuses fonctions d'aide telles que at, fetchState, call*, etc., ainsi que de nombreuses fonctions de test.


Testez votre contrat

Le SDK fournit des fonctionnalités de test unitaire, qui appellent le contrat en envoyant une transaction, mais au lieu de changer l'état de la blockchain, elles renvoient le nouvel état du contrat, les sorties de transaction et les événements.

Installez le framework de test:

Bash
npm install ts-jest @types/jest

Vous aurez également besoin de notre package @alephium/web3:

Bash
npm install @alephium/web3 @alephium/web3-test

Créez un dossier test:

Bash
mkdir test

et créez le fichier de test minimaliste test/token.test.ts avec le contenu suivant:

test/token.test.ts
TypeScript
import { web3, Project, addressFromContractId } from '@alephium/web3'
import { randomContractId, testAddress } from '@alephium/web3-test'
import { TokenFaucet } from '../artifacts/ts'

describe('unit tests', () => {
  it('Withdraws 1 token from TokenFaucet', async () => {

    // Utilisez le bon hôte et le bon port
    web3.setCurrentNodeProvider('http://127.0.0.1:22973')
    await Project.build()

    const testContractId = randomContractId()
    const testParams = {
      // une adresse aléatoire où réside le contrat de test dans les tests
      address: addressFromContractId(testContractId),
      // actifs détenus par le contrat de test avant un test
      initialAsset: { alphAmount: 10n ** 18n, tokens: [{ id: testContractId, amount: 10n }] },
      // état initial du contrat de test
      initialFields: {
        symbol: Buffer.from('TF', 'utf8').toString('hex'),
        name: Buffer.from('TokenFaucet', 'utf8').toString('hex'),
        decimals: 18n,
        supply: 10n ** 18n,
        balance: 10n
      },
      // arguments pour tester la fonction cible du contrat de test
      testArgs: { amount: 1n },
      // actifs détenus par l'appelant de la fonction
      inputAssets: [{ address: testAddress, asset: { alphAmount: 10n ** 18n } }]
    }

    const testResult = await TokenFaucet.tests.withdraw(testParams)
    console.log(testResult)
  })
})

Un test plus complexe peut être trouvé dans notre projet de modèle.

Sans entrer dans trop de détails, TypeScript a besoin d'une configuration pour exécuter le test, alors créez simplement un fichier appelé tsconfig.json idans le répertoire racine de votre projet et collez le code suivant:

tsconfig.json
JSON
{
  "compilerOptions": {
    "outDir": "dist",
    "target": "es2020",
    "esModuleInterop": true,
    "module": "commonjs",
    "resolveJsonModule": true
  },
  "exclude": ["node_modules"],
  "include": ["src/**/*.ts", "test/**/*.ts", "scripts/**/*.ts", "alephium.config.ts", "artifacts/**/*.ts"]
}

Vous pouvez maintenant exécuter le test:

Bash
npx @alephium/cli@latest test

Vous devriez pouvoir voir sur votre terminal la sortie de l'appel à la méthode de retrait.

🎉🎉 Félicitations! 🎉🎉

Vous avez créé votre premier contrat et écrit un test pour l'appeler et le tester localement! Il est temps de déployer votre contrat.


Déployez votre contrat

Maintenant, les choses deviennent sérieuses, nous allons déployer notre contrat sur notre devnet 🚀

La commande deploy exécutera tous les scripts de déploiement qu'elle trouve dans le dossier scripts. Créez le dossier scripts dans le dossier racine du projet:

Bash
mkdir scripts

Créez maintenant un fichier de script de déploiement appelé 0_deploy_faucet.ts dans le dossier scripts et collez-y le code suivant. Notez que les scripts de déploiement doivent toujours être préfixés par des nombres (en commençant par0).

0_deploy_faucet.ts
TypeScript
import { Deployer, DeployFunction, Network } from '@alephium/cli'
import { Settings } from '../alephium.config'
import { TokenFaucet } from '../artifacts/ts'

// Cette fonction de déploiement sera appelée automatiquement par l'outil de déploiement de l'interface en ligne de commande (CLI)
// Notez que les scripts de déploiement doivent être préfixés par des nombres (en commençant par 0)
const deployFaucet: DeployFunction<Settings> = async (
  deployer: Deployer
): Promise<void> => {
  const issueTokenAmount = 100n
  const result = await deployer.deployContract(TokenFaucet, {
    // Le montant de jetons à émettre
    issueTokenAmount: issueTokenAmount,
    // Les états initiaux du contrat du robinet à jetons
    initialFields: {
      symbol: Buffer.from('TF', 'utf8').toString('hex'),
      name: Buffer.from('TokenFaucet', 'utf8').toString('hex'),
      decimals: 18n,
      supply: issueTokenAmount,
      balance: issueTokenAmount
    }
  })
  console.log('Token faucet contract id: ' + result.contractInstance.contractId)
  console.log('Token faucet contract address: ' + result.contractInstance.address)
}

export default deployFaucet

La fonction deployContrat du Deployer tprend notre contrat et le déploie avec les arguments corrects. Vous pouvez également ajouter un argument taskTag a pour marquer votre déploiement avec un nom spécifique. Par défaut, il utilisera le nom du contrat, mais si vous déployez le même contrat plusieurs fois avec des champs initiaux différents, votre fichier .deployment sera écrasé. L'utilisation d'un taskTag spécifique résout ce problème.

À partir de l'interface DeployContractParams, nous pouvons voir que initialFields est obligatoire car il contient les arguments de notre contrat TokenFaucet.

Avec issueTokenAmount vous pouvez définir combien de jetons vous voulez émettre, c'est nécessaire si vous voulez créer un jeton, sinon aucun ID de jeton ne sera créé.

Maintenant, déployons!

Bash
npx @alephium/cli@latest deploy

...OUPS... Ça ne fonctionne pas???

Si vous avez reçu l'erreur The node chain id x is different from configured chain id y, vérifiez votre networkId dans la configuration devnet et le fichier alephium.config.ts.

No UTXO found ???

Bien sûr, nous n'avons pas fourni le how-to-use-my-utxos, nous devons définir nos clés privées.

Vous devrez exporter les clés privées depuis notre extension de portefeuille (peut-être le ferons-nous depuis nos autres portefeuilles plus tard), assurez-vous d'utiliser un portefeuille avec des fonds, comme celui de l'allocation de genèse de votre devnet. Si vous avez utilisé la méthode docker pour lancer votre devnet, cela pourrait avoir fonctionné car nous définissons une clé privée par défaut dans notre package cli basée sur l'allocation de genèse.

Modifions notre alephium.config.ts

`alephium.config.ts
TypeScript
const configuration: Configuration<void> = {
  networks: {
    devnet: {
      nodeUrl: 'http://localhost:22973',
      networkId: 2,
      // La clé privée de mon adresse de genèse  132mqFF2BuxGigdaMTGSruuW29kmEs2eEGcpquG4YZRNh
      privateKeys: ['672c8292041176c9056bb0dd1d91d34711ceed2493b5afc83f2012b27df2c559']
    }
  }
}

ATTENTION

Les applications réelles devraient utiliser des variables d'environnement ou des techniques similaires pour les paramètres sensibles comme les privateKeys. Ne communiquez pas vos clés privées sur un dépôt de code source.

et réessayons de déployer :

Bash
npx @alephium/cli@latest deploy
Bash
Contracts are compiled already. Loading them from folder "artifacts"
Deploying contract TokenFaucet
Deployer - group 1 - 132mqFF2BuxGigdaMTGSruuW29kmEs2eEGcpquG4YZRNh
Token faucet contract id: d00e9c788ddd572b0c186f0599a264f4c79f009c632c8040b7c5f71bfc0ec301
Token faucet contract address: 28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA
 Deployment scripts executed!

Félicitations! Votre contrat est déployé. Nous pouvons vérifier le solde du contrat. Utilisez curl et modifiez l'adresse du contrat en fonction du résultat de votre déploiement:

Bash
curl 'http://localhost:22973/addresses/28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA/balance'

La réponse devrait ressembler à ceci :

réponse
JSON
{
  "balance": "1000000000000000000",
  "balanceHint": "1 ALPH",
  "lockedBalance": "0",
  "lockedBalanceHint": "0 ALPH",
  "tokenBalances": [
    {
      "id": "d00e9c788ddd572b0c186f0599a264f4c79f009c632c8040b7c5f71bfc0ec301",
      "amount": "100"
    }
  ],
  "utxoNum": 1
}

Nous pouvons voir notre ID de token, avec les 100 tokens que nous avons décidé d'émettre.

Vérifions maintenant l'état du contrat en obtenant d'abord le groupe de notre adresse :

Bash
curl 'http://localhost:22973/addresses/28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA/group'
curl 'http://localhost:22973/contracts/28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA/state?group=1'

Réponse de l'état du contrat :

réponse
JSON
{
  "address": "28h7qSmkAAeNyoBuQKGyp1WG8VfdKPePCCFGKwp2Y8yyA",
  "bytecode": "050609121b4024402d404a010000000102ce0002010000000102ce0102010000000102ce0202010000000102ce0302010000000102a0000201020101001116000e320c7bb4b11600aba00016002ba10005b416005f",
  "codeHash": "641343b4f1c08b03969b127b452acc7535cad20231bc32af6c0b5f218dd8ff0c",
  "initialStateHash": "06595afa695949e915dfc1220dfb47125b01751d9e193f4c5fa1c7fc3566673d",
  "immFields": [
    {
      "type": "ByteVec",
      "value": "5446"
    },
    {
      "type": "ByteVec",
      "value": "546f6b656e466175636574"
    },
    {
      "type": "U256",
      "value": "18"
    },
    {
      "type": "U256",
      "value": "100"
    }
  ],
  "mutFields": [
    {
      "type": "U256",
      "value": "100"
    }
  ],
  "asset": {
    "attoAlphAmount": "1000000000000000000",
    "tokens": [
      {
        "id": "d00e9c788ddd572b0c186f0599a264f4c79f009c632c8040b7c5f71bfc0ec301",
        "amount": "100"
      }
    ]
  }
}

Dans les immFields, nous pouvons voir nos arguments initiaux de TokenFaucet (symbol, name, decimals, supply). Nous pouvons également voir que mutFields contient le solde actuel du token. Nous vérifierons ce champ plus tard après avoir appelé le faucet.

La commande deploy a également créé un fichier .deployments.devnet.json, avec le résultat du déploiement. Il est important de conserver ce fichier pour interagir facilement avec le contrat, même si toutes les informations peuvent être trouvées sur la blockchain.


Interagir avec le contrat déployé

Avoir un faucet de tokens c'est bien, obtenir des tokens à partir de celui-ci c'est encore mieux.

Nous pouvons maintenant écrire du code pour interagir avec le contrat de faucet.

Nous devrons installer notre package cli et la dépendance typescript si ce n'est pas encore le cas:

Text Only
npm install @alephium/cli typescript

Nous allons maintenant voir une autre option pour interagir avec la blockchain. Auparavant, nous utilisions la DeployFunction avec nos fichiers scripts/<number>_* qui sont automatiquement déployés avec l'outil CLI.

Une autre façon est de créer un projet d'application Web squelette en utilisant TypeScript. Créez un dossier src dans le dossier racine du projet et un fichier appelé tokens.ts avec le contenu suivant.

tokens.ts
TypeScript
import { Deployments } from '@alephium/cli'
import { DUST_AMOUNT, web3, Project, NodeProvider } from '@alephium/web3'
import { PrivateKeyWallet} from '@alephium/web3-wallet'
import configuration from '../alephium.config'
import { TokenFaucet, Withdraw } from '../artifacts/ts'

async function withdraw() {

  // Sélectionnez notre réseau défini dans alephium.config.ts
  const network = configuration.networks.devnet

  // NodeProvider est une abstraction d'une connexion au réseau Alephium
  const nodeProvider = new NodeProvider(network.nodeUrl)

  // Parfois, il est pratique de configurer un NodeProvider global pour votre projet:
  web3.setCurrentNodeProvider(nodeProvider)

  // Connectez notre portefeuille, typiquement dans une application réelle vous connecteriez votre extension Web ou votre portefeuille de bureau
  const wallet = new PrivateKeyWallet({privateKey: '672c8292041176c9056bb0dd1d91d34711ceed2493b5afc83f2012b27df2c559' })

  // Compilez les contrats du projet s'ils ne sont pas compilés
  Project.build()

  //.deployments contient les infos de notre déploiement `TokenFaucet` , car nous devons connaître le contractId et l'adresse
  // Ceci a été auto-généré avec le `cli deploy` de notre `scripts/0_deploy_faucet.ts`
  const deployments = await Deployments.from('.deployments.devnet.json')

  // Assurez-vous qu'il correspond à votre groupe d'adresses
  const accountGroup = 1

  const deployed = deployments.getDeployedContractResult(accountGroup, 'TokenFaucet')

  if(deployed !== undefined) {
    const tokenId = deployed.contractInstance.contractId
    const tokenAddress = deployed.contractInstance.address

    // Soumettez une transaction pour utiliser le script de transaction
    // Il utilise notre `wallet` pour signer la transaction.
    await Withdraw.execute(wallet, {
      initialFields: { token: tokenId, amount: 1n },
      attoAlphAmount: DUST_AMOUNT
    })

    // Obtenez l'état le plus récent du contrat de jeton, `mut balance` devrait avoir changé
    const faucet = TokenFaucet.at(tokenAddress)
    const state = await faucet.fetchState()
    console.log(state.fields)

    // Obtenez le solde du portefeuille pour voir si le jeton y est
    const balance = await wallet.nodeProvider.addresses.getAddressesAddressBalance(wallet.account.address)
    console.log(balance)
  } else {
    console.log('`deployed` is undefined')
  }
}

// Effectuons un retrait
withdraw()

Pour les personnes attentives, vous verrez quelque chose de nouveau provenant de nos artifacts: Withdraw qui est un TxScript requis pour interagir avec le contrat TokenFaucet. Son code est assez simple. Créez un fichier appelé withdraw.ral dans le dossier contracts et collez le code suivant:

withdraw.ral
Rust
TxScript Withdraw(token: TokenFaucet, amount: U256) {
    token.withdraw(amount)
}

Nous devons maintenant recompiler nos contrats pour obtenir l'artefact pour Withdraw:

Bash
npx @alephium/cli@latest compile

Vous pouvez maintenant compiler le code TypeScript en JavaScript avec:

Bash
npx tsc --build .

OOPS, vous devriez obtenir une erreur provenant du fichier alephium.config.ts, jusqu'à présent la configuration était utilisée comme un simple JSON, mais maintenant TypeScript veut qu'elle respecte son interface. En particulier, les networks sont un enregistrement qui doit contenir les 3 NetworkType. Vous pouvez essayer de le corriger vous-même ou mettre à jour votre fichier alephium.config.ts avec:

alephium.config.ts
TypeScript
import { Configuration } from '@alephium/cli'

export type Settings = {}

const configuration: Configuration<Settings> = {
  defaultNetwork: 'devnet',
  networks: {
    devnet: {
      nodeUrl: 'http://localhost:22973',
      networkId: 2, //Use the same as in your devnet configuration
      privateKeys: ['672c8292041176c9056bb0dd1d91d34711ceed2493b5afc83f2012b27df2c559'],
      settings: {}
    },
    testnet: {
      nodeUrl: '',
      privateKeys: [],
      settings: {}
    },
    mainnet: {
      nodeUrl: '',
      privateKeys: [],
      settings: {}
    }
  }
}

export default configuration

Maintenant, recompilez

Text Only
npx tsc --build .

Un dossier dist devrait avoir été créé, continuez et interagissez avec le robinet à jetons déployé:

Text Only
node dist/src/token.js

Vous devriez maintenant être un fier propriétaire du jeton que vous avez créé.


Et après?

Vous pouvez trouver un exemple plus complexe du tutoriel du robinet à jetons dans le projet alephium nextjs-template project.


Connexion aux portefeuilles

Les dApps nécessitent une intégration de portefeuille pour les utilisateurs de la dApp afin d'authentifier et d'interagir avec la blockchain Alephium, telles que la signature de transactions. Actuellement, les dApps peuvent être intégrées à la fois avec le Extension Wallet et WalletConnect. Veuillez vous référer aux pages respectives pour plus de détails.


En savoir plus