Zum Hauptinhalt springen

Ihren gaslosen Nutzern ermöglichen, Token zu halten und Verträge aufzurufen

gaslos
ERC-20
Kontoabstraktion
Fortgeschritten
Ori Pomerantz
1. April 2026
18 Minuten Lesezeit

Einführung

Ein vorheriger Artikel behandelte den gaslosen Zugriff auf Ihre eigene Anwendung mithilfe von EIP-712-Signaturen, was jedoch auf Ihre eigenen Smart Contracts beschränkt ist. Mithilfe der Kontoabstraktion können wir Smart-Contract-Wallets erstellen, die zwei Arten von Transaktionen akzeptieren und an ein gewünschtes Ziel weiterleiten:

  • Transaktionen, die von einem bestimmten extern verwalteten Konto (EOA) gesendet werden (was erfordert, dass dieses EOA über ETH verfügt)
  • Transaktionen, die von überall gesendet, aber von demselben EOA signiert wurden.

Auf diese Weise können wir eine gaslose Möglichkeit für ein Konto bereitstellen, Vermögenswerte (Token usw.) zu halten und alle Funktionen auszuführen, die ein EOA mit Gas ausführen kann.

Warum können wir die Anfrage nicht einfach weiterleiten?

In ERC-20 und verwandten Standards ist der Kontoinhaber msg.sender (opens in a new tab), die Adresse, die den Token-Vertrag aufgerufen hat, was nicht zwangsläufig der Urheber der Transaktion, tx.origin (opens in a new tab), ist. Dies ist aus Sicherheitsgründen (opens in a new tab) erforderlich. Das bedeutet, dass bei der Weiterleitung von Token-Transfer-Anfragen versucht wird, Token von der Adresse des Relayers zu transferieren, anstatt von einer Adresse, die vom Nutzer kontrolliert wird.

Es gibt eine Lösung, die es Ihnen ermöglicht, die EOA-Adresse über EIP-7702 (opens in a new tab) zu verwenden, aber sie erfordert das Signieren einer potenziell gefährlichen Delegation, sodass Sie sie nur verwenden können, um an einen Smart Contract zu delegieren, den der Wallet-Anbieter genehmigt. Für dieses Tutorial bevorzuge ich die viel einfachere Methode, einen Smart Contract als Proxy für den Nutzer zu erstellen.

Das Ganze in Aktion sehen

  1. Stellen Sie sicher, dass Sie sowohl Node (opens in a new tab) als auch Foundry (opens in a new tab) installiert haben.

  2. Klonen Sie die Anwendung und installieren Sie die erforderliche Software.

    git clone https://github.com/qbzzt/260315-gasless-tokens.git
    cd 260315-gasless-tokens
    forge build
    cd server
    npm install
    
  3. Bearbeiten Sie .env, um SEPOLIA_PRIVATE_KEY auf eine Wallet zu setzen, die über ETH auf Sepolia verfügt. Wenn Sie Sepolia-ETH benötigen, verwenden Sie ein Faucet, um es zu erhalten. Idealerweise sollte sich dieser private Schlüssel von dem in Ihrer Browser-Wallet unterscheiden.

  4. Starten Sie den Server.

    npm run dev
    
  5. Rufen Sie die Anwendung unter der URL http://localhost:5173 (opens in a new tab) auf.

  6. Klicken Sie auf Connect with Injected, um sich mit einer Wallet zu verbinden. Genehmigen Sie dies in der Wallet und genehmigen Sie bei Bedarf den Wechsel zu Sepolia.

  7. Scrollen Sie nach unten und klicken Sie auf Deploy UserProxy (slow process).

  8. Sie können sehen, wann der User-Proxy bereitgestellt wurde, da neben UserProxy access eine Adresse steht. Wenn Sie 24 Sekunden (2 Blöcke) gewartet haben und es immer noch nicht passiert ist, gibt es möglicherweise ein Problem bei der Erkennung von Änderungen.

    Wenn dies der Fall ist, gehen Sie zum Sepolia-Block-Explorer (opens in a new tab) und geben Sie den Transaktions-Hash der Bereitstellung ein, den Sie in der Serverausgabe bei npm run dev sehen. Klicken Sie auf den erstellten Vertrag, um seine Adresse anzuzeigen, und kopieren Sie sie. Fügen Sie die Adresse in das Feld Or enter existing proxy address ein und klicken Sie dann auf Set proxy address.

  9. Klicken Sie auf Request more tokens for proxy, um einen Aufruf an die Funktion faucet (opens in a new tab) des ERC-20-Vertrags zu senden, um Token zu erhalten. Confirm (Bestätigen) Sie die Signatur in der Wallet. Natürlich erreichen die Token die Adresse des Proxys, nicht die des Nutzers.

  10. Scrollen Sie nach unten und klicken Sie auf den Link unter Last transaction:. Dadurch wird der Browser geöffnet, um Ihnen die Transaktion faucet anzuzeigen.

  11. Geben Sie unter amount to transfer eine Zahl zwischen eins und eintausend ein. Klicken Sie auf Transfer, um die Token an Ihre eigene Adresse zu transferieren. Bevor Sie für die Anfrage auf Confirm klicken, beachten Sie, dass die zu signierenden Daten undurchsichtig sind. Nutzer hätten es schwer zu verstehen, was sie signieren. Denken Sie daran, dass wir dies unten besprechen werden.

  12. Warten Sie nach der Bestätigung der Transaktion, um die Änderung sowohl bei your balance als auch bei proxy balance zu sehen. Beachten Sie, dass dies ebenfalls einige Zeit in Anspruch nehmen wird, da Sepolia eine Blockzeit von 12 Sekunden hat.

Wie es funktioniert

Für ein gasloses Erlebnis benötigen wir eine Benutzeroberfläche für den Nutzer, einen Server, um Nachrichten von der Benutzeroberfläche an die Chain weiterzuleiten, und einen Smart Contract, um sie zu empfangen und zu verifizieren.

Der Wallet-Smart-Contract

Dies ist der Smart Contract (opens in a new tab). Sein Zweck ist es, alles zu tun, was der tatsächliche Eigentümer anfordert, unabhängig davon, über welchen Kanal die Anfrage gestellt wird, und alles andere zu ignorieren. Um dies zu tun, erhalten seine Funktionen eine Zieladresse, die aufgerufen werden soll, und die Daten, die für den Aufruf verwendet werden sollen.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

contract UserProxy {
    address immutable OWNER;
    uint public nonce = 0;

Die Identität des Eigentümers und eine Nonce (opens in a new tab), um zu verhindern, dass Nachrichten wiederholt werden. Da die Nonce eine public-Variable ist, erstellt der Solidity-Compiler auch eine View-Funktion, nonce() (opens in a new tab), die es Offchain-Code ermöglicht, ihren Wert zu lesen.

    bytes32 private constant SIGNED_ACCESS_TYPEHASH =
        keccak256("SignedAccess(address target,bytes data,uint256 nonce)");

    bytes32 private constant SIGNED_ACCESS_PAYABLE_TYPEHASH =
        keccak256("SignedAccessPayable(address target,bytes data,uint256 nonce,uint256 value)");

    bytes32 immutable DOMAIN_SEPARATOR;

Die Informationen, die zur Verifizierung von EIP-712-Signaturen (opens in a new tab) erforderlich sind.

    constructor(address owner_) {
        OWNER = owner_;

Ein UserProxy ist an eine einzige Eigentümeradresse gebunden. Dies ist notwendig, da er Vermögenswerte (ERC-20-Token, NFTs usw.) besitzen kann. Wir möchten keine Vermögenswerte vermischen, die verschiedenen Eigentümern gehören.

Der Domain-Separator (opens in a new tab). Er kann nicht zur Kompilierzeit berechnet werden, da er von der Chain-ID und der Vertragsadresse abhängt. Dies macht es unmöglich, dass ein UserProxy durch eine für einen anderen vorbereitete Nachricht getäuscht wird.

    event CallResult(address target, bytes returnData);

Die Ergebnisse eines Aufrufs protokollieren.

    function directAccess(address target, bytes calldata data)
            external returns (bytes memory) {

Diese Funktion kann direkt vom Eigentümer aufgerufen werden. Wenn keine Relays verfügbar sind, kann der Eigentümer weiterhin direkt auf der Blockchain auf die Vermögenswerte zugreifen (sofern der Nutzer über ETH verfügt).

        require(msg.sender == OWNER, "Only owner can call");
        (bool success, bytes memory returnData) = target.call(data);
        require(success, "Call failed");

        emit CallResult(target, returnData);

        return returnData;
    }

Wenn wir direkt vom Eigentümer aufgerufen werden, rufen Sie das Ziel mit den bereitgestellten Aufrufdaten auf.

    function signedAccess(
        address target,
        bytes calldata data,
        uint8 v,
        bytes32 r,
        bytes32 s)

Dies ist die Hauptfunktion von UserProxy. Sie erhält target und data sowie eine Signatur.

Der Digest enthält auch die Nonce, aber wir müssen sie nicht aus der Transaktion erhalten; wir kennen bereits den richtigen Wert. Eine Signatur mit der falschen Nonce wird abgelehnt.


    // Unterzeichner wiederherstellen
    address signer = ecrecover(digest, v, r, s);
    require(signer == OWNER, "Signature invalid or not by owner");

Wenn die Signatur ungültig ist, gibt ecrecover normalerweise eine andere Adresse zurück und sie wird nicht akzeptiert.

    (bool success, bytes memory returnData) = target.call(data);
    require(success, "Call failed");

Rufen Sie den Vertrag auf, den der Nutzer uns aufzurufen angewiesen hat, und machen Sie dies rückgängig, falls es nicht erfolgreich ist.

    emit CallResult(target, returnData);

    nonce++; // Nonce erhöhen, um Replay zu verhindern

    return returnData;
}

Wenn erfolgreich, geben Sie ein Log-Ereignis aus und erhöhen Sie die Nonce.

Dies sind nahezu identische Varianten, mit denen Sie auch ETH aus dem Vertrag transferieren können.

Der Relayer

Der Relayer ist eine Serverkomponente. Er ist in JavaScript geschrieben; Sie können den Quellcode hier (opens in a new tab) einsehen.

import express from "express";
import { createServer as createViteServer } from "vite";
import { createWalletClient, createPublicClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { sepolia } from 'viem/chains'
import dotenv from 'dotenv'

Die Bibliotheken, die wir benötigen. Dies ist ein Express (opens in a new tab)-Server, der Vite (opens in a new tab) verwendet, um den Code der Benutzeroberfläche bereitzustellen. Wir verwenden Viem (opens in a new tab), um mit der Blockchain zu kommunizieren, und dotenv (opens in a new tab), um den privaten Schlüssel für die Adresse zu lesen, die die Transaktion sendet.

import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const UserProxy = require('../contracts/out/UserProxy.sol/UserProxy.json')

Dies ist eine einfache Möglichkeit, den kompilierten UserProxy zu lesen. Wir benötigen die ABI, um UserProxy aufrufen zu können, und den kompilierten Code, um ihn für einen Nutzer bereitstellen zu können.

dotenv.config()
const sepoliaAccount = privateKeyToAccount(process.env.SEPOLIA_PRIVATE_KEY)
console.log("Using account:", sepoliaAccount.address)

Lesen Sie die Datei .env, extrahieren Sie die Adresse und geben Sie sie in der Konsole aus.

Die Viem-Clients, die mit der Blockchain kommunizieren.

const start = async () => {
  const app = express()

Führen Sie einen Express-Server aus.

  app.use(express.json())

Weisen Sie Express an, den Anfrage-Body zu lesen und ihn zu parsen, falls es sich um JSON handelt.

  app.post("/server/deploy", async (req, res) => {

Dies ist der Code, der Anfragen zur Bereitstellung des Proxys verarbeitet. Beachten Sie, dass wir hier anfällig für Denial-of-Service (opens in a new tab)-Angriffe sind, da ein Angreifer uns mit Anfragen zur Bereitstellung des Proxys überfluten kann, bis unser ETH aufgebraucht ist. Auf einem Produktionssystem würden wir wahrscheinlich verlangen, dass die Anfrage zur Bereitstellung des Proxys signiert ist und dass der Unterzeichner ein bestehender Kunde ist.

    try {
      const ownerAddress = req.body.ownerAddress

Rufen Sie die Adresse des Eigentümers aus der Anfrage ab.

Stellen Sie den Vertrag bereit (opens in a new tab) und warten Sie, bis er bereitgestellt ist (opens in a new tab).

      res.json({ contractAddress: receipt.contractAddress })

Wenn alles in Ordnung ist, geben Sie die Proxy-Adresse an die Benutzeroberfläche zurück.

    } catch (err) {
      console.error(err)
      res.status(500).json({ error: err.message })
    }
  })

Wenn es ein Problem gibt, melden Sie es.

  app.post("/server/message", async (req, res) => {

Dies ist der Code, der Nutzernachrichten für den Vertrag UserProxy verarbeitet. Dies ist ein weiterer Punkt, der anfällig für einen Denial-of-Service-Angriff ist.

Rufen Sie die Anfragedaten ab und verwenden Sie sie, um signedAccess auf dem Proxy aufzurufen.

      console.log("Message transaction hash:", txHash)

      res.json({ txHash })

Melden Sie den Transaktions-Hash zurück. Dadurch kann die Benutzeroberfläche eine URL anzeigen, über die der Nutzer die Transaktion überprüfen kann.

    } catch (err) {
      console.error(err)
      res.status(500).json({ error: err.message })
    }
  })

Auch hier gilt: Wenn es ein Problem gibt, melden Sie es.

Verwenden Sie für alles andere Vite, das die Bereitstellung der Benutzeroberfläche für uns übernimmt.

Benutzeroberfläche

Dies ist der Code der Benutzeroberfläche (opens in a new tab). Der Großteil des Codes ist nahezu identisch mit dem in diesem Artikel dokumentierten Code, mit Ausnahme von Token.jsx (opens in a new tab).

Teile von Token.jsx (opens in a new tab) ähneln Greeter.jsx (opens in a new tab) in diesem Artikel. Hier sind die neuen Teile.

import {
   encodeFunctionData
       } from 'viem'

Diese Funktion (opens in a new tab) erstellt die Aufrufdaten für einen EVM-Funktionsaufruf. Dies ist notwendig, damit der Nutzer die Aufrufdaten signieren kann.

import UserProxy from '../../contracts/out/UserProxy.sol/UserProxy.json'

Der UserProxy, wie oben erklärt.

import Erc20 from '../../contracts/out/Faucet.sol/FaucetToken.json'

Dieser Vertrag (opens in a new tab) ist größtenteils ein normaler ERC-20-Vertrag, mit dem Zusatz einer wichtigen Funktion, faucet(). Diese Funktion gewährt jedem, der danach fragt, Token zu Testzwecken.

const erc20Addrs = {
  // Sepolia
    11155111: '0x4cBedDEDA88fDd9e116618a5cD71BB0E440C2A78'
}

Die Adresse für FaucetToken.

const Address = ({ address }) => {
   if (!address) return null
   return (
      <a href={`https://eth-sepolia.blockscout.com/address/${address}?tab=read_write_contract`} target="_blank">{address}</a>
   )
}

Diese Komponente gibt eine Adresse mit einem Link zum Vertrag auf einem Block-Explorer aus.

const Token = () => {
    ...

Dies ist die Hauptkomponente, die die meiste Arbeit erledigt.

  const [ balanceAmount, setBalanceAmount ] = useState("Loading...")

Der Token-Kontostand der Nutzeradresse.

  const [ proxyAddr, setProxyAddr ] = useState(null)

Die Adresse eines Proxys, der dem Nutzer gehört.

  const [ proxyBalanceAmount, setProxyBalanceAmount ] = useState("Loading...")

Der Token-Kontostand des Proxys.

  const [ newProxyAddr, setNewProxyAddr ] = useState("")

Dieses Feld wird verwendet, wenn der Nutzer die Proxy-Adresse manuell festlegt. Die Möglichkeit, die Proxy-Adresse manuell festzulegen, erlaubt es dem Nutzer, einen bestehenden Proxy zu verwenden, anstatt jedes Mal einen neuen bereitzustellen (und alle Token zu verlieren, die dem alten Proxy gehören).

  const [ txHash, setTxHash ] = useState(null)

Der Hash der letzten Transaktion, der verwendet wird, um einen Link zum Explorer anzuzeigen, damit der Nutzer diese Transaktion überprüfen kann.

  const [ transferToken, setTransferToken ] = useState("")
  const [ transferAmount, setTransferAmount ] = useState("")
  const [ transferTo, setTransferTo ] = useState("")

Diese Felder werden alle verwendet, um Token-Transfer-Befehle an einen ERC-20-Vertrag zu senden. Dies kann FaucetToken sein, muss es aber nicht. Die Funktion transfer ist Teil des ERC-20-Standards.

  const balance = useReadContract({
    ...
  })


  const proxyBalance = useReadContract({
    ...
  })

Lesen Sie die beiden Token-Kontostände, an denen wir interessiert sind: wie viel der Nutzer besitzt und wie viel der Proxy besitzt.

  const nonce = useReadContract({
      address: proxyAddr,
      abi: UserProxy.abi,
      functionName: 'nonce',
      args: [],
  })

Um Replay-Angriffe zu verhindern (zum Beispiel ein Verkäufer, der eine Transaktion wiederholt, die ihm Geld einbringt), verwenden wir eine Nonce (opens in a new tab). Wir müssen den aktuellen Wert kennen, um ihn den Daten hinzuzufügen, die wir signieren.

Verwenden Sie useEffect (opens in a new tab), um den dem Nutzer angezeigten Kontostand zu aktualisieren, wenn sich die aus der Blockchain gelesenen Informationen ändern.

  useEffect(() => {
    setTransferToken(faucetAddr)
  }, [faucetAddr])

  useEffect(() => {
    setTransferTo(account.address)
  }, [account.address])

Standardmäßig werden FaucetToken-Token auf das eigene Konto des Nutzers transferiert. Hier legen wir diese Werte fest, wenn wir sie von Viem erhalten.

  const proxyAddressChange = (evt) => setNewProxyAddr(evt.target.value)
  const transferTokenChange = (evt) => setTransferToken(evt.target.value)
  const transferToChange = (evt) => setTransferTo(evt.target.value)
  const transferAmountChange = (evt) => setTransferAmount(evt.target.value)

Ereignis-Handler für den Fall, dass sich die Textfelder ändern.

Bitten Sie den Server, einen Proxy für diesen Nutzer bereitzustellen.

  const signMessage = async(proxyAddr, target, calldata) => {

Signieren Sie eine Nachricht, bevor Sie sie an den Server senden, um sie Onchain an UserProxy zu senden. Dies wird hier erklärt. Wir müssen eine Nachricht sowohl mit der Zieladresse (der Adresse des Tokens, den wir aufrufen) als auch mit den zu sendenden Aufrufdaten signieren.

    const domain = {
      .
      .
      .
    return {v, r, s}
  }

  const messageUserProxy = async (proxy, target, data, v, r, s) => {

Senden Sie eine signierte Nachricht an UserProxy, der die Signatur verifiziert und sie dann an das target sendet.

Senden Sie eine Anfrage an den Server und rufen Sie den Transaktions-Hash ab, wenn Sie die Antwort erhalten.

  const faucetSimulation = useSimulateContract({
    address: faucetAddr,
    abi: Erc20.abi,
    functionName: 'faucet',
    account: account.address
  })

Simulieren Sie den Aufruf der Funktion faucet. Wir aktivieren den Faucet-Button nur, wenn dies erfolgreich ist.

Um eine Funktion über den Server und UserProxy aufzurufen, befolgen wir drei Schritte:

  1. Erstellen Sie die Aufrufdaten zum Signieren und Senden mithilfe von encodeFunctionData (opens in a new tab).

  2. Signieren Sie die Nachricht (Zieladresse, Aufrufdaten und Nonce).

  3. Senden Sie die Nachricht an den Server.

Dieser Teil der Komponente ermöglicht es Ihnen, FaucetToken direkt aus dem Browser zu verwenden. Sein Hauptzweck ist es, das Debugging zu erleichtern.

         <h4>UserProxy access <Address address={proxyAddr} /></h4>
         <button onClick={deployUserProxy}>
         Deploy UserProxy (slow process)
         </button>

Lassen Sie den Nutzer einen neuen UserProxy bereitstellen.

Lassen Sie Nutzer nur dann auf Set proxy address klicken, wenn sie eine legitime Adresse eingeben. Beachten Sie, dass dies nicht sicherstellt, dass es sich bei der betreffenden Adresse tatsächlich um einen UserProxy-Vertrag handelt. Es ist möglich, eine solche Überprüfung hinzuzufügen, aber sie wird viel langsamer sein (schlechtere Nutzererfahrung) und die Sicherheit nicht verbessern (Angreifer können immer ihren eigenen Code für die Benutzeroberfläche verwenden).

         <br /><br />
         { proxyAddr && (

Zeigen Sie den Rest nur an, wenn es eine legitime Proxy-Adresse gibt.

            <>
               Proxy balance: {proxyBalanceAmount}
               <br />
               Proxy nonce: {nonce?.data?.toString() ?? "Loading..."}

Der Nutzer muss die Nonce nicht kennen; dies dient nur zu Debugging-Zwecken.

               <br />
               <button disabled={!proxyAddr || proxyAddr === "Loading..." || nonce?.status !== 'success'}
                  onClick={proxyFaucet}
               >
                  Request more tokens for proxy
               </button>

Wir können keinen Aufruf von faucet() über den Proxy simulieren. Wir können jedoch zumindest sicherstellen, dass wir einen Proxy haben und dass der Proxy uns eine Nonce gemeldet hat.

Lassen Sie den Nutzer ERC-20-Transfer-Transaktionen ausgeben.

Wenn es einen letzten Transaktions-Hash gibt, zeigen Sie einen Link an, damit der Nutzer ihn in einem Block-Explorer anzeigen kann.

 
</div>
    </>
  )
}

export {Token}

Dies ist nur React-Boilerplate.

Schwachstellen

Unser Server ist anfällig für Denial-of-Service-Angriffe. Dieser Angriff wird im vorherigen Artikel der Serie erklärt.

Zusätzlich fördern wir schlechtes Nutzerverhalten. Dies ist es, was wir den Nutzer bitten zu signieren:

Screen capture with opaque calldata

Wir wissen, dass dies ein legitimer ERC-20-Transfer für den Token, den Betrag und die Zieladresse ist, die der Nutzer transferieren möchte. Aber die meisten Nutzer wissen nicht, wie sie Aufrufdaten interpretieren sollen, und haben keine Ahnung, was sie signieren. Das ist aus zwei Gründen ein schlechtes Design:

  • Einige Nutzer werden uns nicht nutzen, weil sie den Daten nicht vertrauen, die wir sie bitten zu signieren.
  • Andere Nutzer werden uns vertrauen und lernen, dass sie einfach Aufrufdaten signieren sollten, ohne zu verstehen, was sie sind. Das bedeutet, dass, wenn Adam Angreifer es schafft, sie auf seine Website umzuleiten, er sie eine Transaktion signieren lassen kann, die ihm alle USDC (oder DAI oder jeden anderen ERC-20) gewährt, die der Nutzer besitzt.

Die Lösung besteht darin, separate Funktionen in UserProxy für häufig verwendete Funktionen wie Transfer zu haben. Dann können Nutzer etwas signieren, das sie verstehen.

Screen capture with transfer details

Hinweis: Während Nutzer jede beliebige Wallet verwenden können, wird dringend empfohlen, dass Anwendungen, die EIP-712 verwenden, sie dazu ermutigen, eine Wallet zu verwenden, die die gesamten Signaturdaten anzeigt (opens in a new tab). Einige Wallets kürzen die Adresse ab, was unsicher ist. Ein Angreifer kann eine Adresse erstellen, die die gleichen Anfangs- und Endzeichen hat, sich aber in der Mitte unterscheidet.

Screen capture with truncated addresses

Fazit

Zusätzlich zu den oben genannten Schwachstellen hat die Lösung in diesem Tutorial einige Nachteile, bei deren Behebung Ethereum uns helfen kann.

  • Zensurresistenz. Derzeit können Nutzer Ihren Server oder einen konkurrierenden Server verwenden, der von jemand anderem eingerichtet wurde, oder sich direkt mit Ethereum verbinden, was Gaskosten verursacht. Die Verwendung von ERC-4337 (opens in a new tab) ermöglicht es Nutzern, ihre Transaktion einem großen Pool von Servern anzubieten, was die Wahrscheinlichkeit verringert, dass ihre Transaktionen zensiert werden.
  • Vermögenswerte im Besitz von EOAs. Wie oben angemerkt, kann EIP-7702 (opens in a new tab) verwendet werden, um Vermögenswerte zu verwalten, die sich bereits im Besitz einer EOA-Adresse befinden. Dies hat seine Schwierigkeiten, ist aber manchmal notwendig.

Ich hoffe, in naher Zukunft Tutorials zum Hinzufügen dieser Funktionen veröffentlichen zu können.

Weitere meiner Arbeiten finden Sie hier (opens in a new tab).