Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using Invisible Signers #28

Closed
Tracked by #13
manuelbarbas opened this issue Nov 21, 2023 · 1 comment
Closed
Tracked by #13

Using Invisible Signers #28

manuelbarbas opened this issue Nov 21, 2023 · 1 comment
Labels
documentation Improvements or additions to documentation

Comments

@manuelbarbas
Copy link
Collaborator

manuelbarbas commented Nov 21, 2023

Creating a seamless user onboarding experience is essential for the success of any dApp. A well-designed onboarding process can increase user retention and foster a positive first impression, which is crucial for encouraging users to come back for more.

Thanks to the zero gas fees nature of the SKALE chains projects can perform transactions on behalf of the users without compromising the company sustainability by covering huge gas fees costs.

In order to achieve the described above, the application can generate on the background a wallet for each user, distribute the free gas token to it and store it on the backend. Every time a user performs a transaction, the background wallet signs the transaction without the user having idea he just made a on-chain transaction.

Implementation Example

This codebase uses the Typescript language along with the Viem library to showcase a proof of concept on how to utilize background signers within an API or Server based environment.

This example also uses a sticky session per userId meaning that the randomly generated accounts are mapped 1:1 with a userId. This will persist only for the duration of the service liftetime. On application crash or restart new wallets will be created. To resolve these types of issues you can encrypt the private keys and store them in something like Redis to make a more sophisticated service that would also allow for multiple AZ usage.

1- Custodian

import { initializeCustodian } from "./utils";
import { createClient } from "./utils";
import { CUSTODIAN_PRIVATE_KEY, WSS_URL } from "./config";
import { parseEther } from "viem";

const DEFAULT_FILL_UP_VALUE: bigint = parseEther("0.00000002");

class Custodian {
    #nonce = 0;
    #custodian;
    #client;

    constructor() {
        this.#custodian = initializeCustodian(CUSTODIAN_PRIVATE_KEY as `0x${string}`);
        this.#client = createClient(WSS_URL);
    }

    public get custodian() {
        return this.#custodian;
    }

    public get client() {
        return this.#client;
    }

    public async isValidCustodian() {
        const balance = await this.#client.getBalance({
            address: this.#custodian.account.address
        });

        if (balance < parseEther("0.00005")) {
            throw new Error("Custodian Balance must be > 0.00005");
        }

        this.#nonce = await this.#client.getTransactionCount({
            address: this.#custodian.account.address
        });
    }

    public async distribute(to: `0x${string}`) {
        const hash = await this.#custodian.sendTransaction({
            to,
            value: DEFAULT_FILL_UP_VALUE,
            nonce: this.#nonce++
        });
        const tx = await this.#client.waitForTransactionReceipt({
            hash
        });
    }
}
export default new Custodian();

2- Background Signers

import { WalletClient, getAddress, parseAbi } from "viem";
import Custodian from "./custodian";
import { createSigner } from "./utils";
import { skaleChaosTestnet } from "viem/chains";
import { Contract } from "./contract";

class BackgroundSigners {
    #custodian: typeof Custodian;
    #signers: {[key: string]: WalletClient} = {};

    constructor() {
        this.#custodian = Custodian;
    }

    
    public async getUser(userId: string) {
        if (this.#signers[userId] === undefined) {
            const signer = createSigner();
            this.#signers[userId] = signer;
            await this.#custodian.distribute(signer.account.address);
            
        }
        
        return this.#signers[userId].account?.address as `0x${string}`;
    }

    public async remove(userId: string) {
        const account = this.#signers[userId].account;
        if (!account) return;
        this.#signers[userId].sendTransaction({
            to: this.#custodian.custodian.account.address,
            value: BigInt(1),
            type: "legacy",
            account,
            chain: skaleChaosTestnet
        });
    }

    public async backgroundSignerAction(userId: string, args: any[], functionName: "mint" | "burn") {
        const account = this.#signers[userId].account;
        if (!account) throw new Error("Account Not Found");

        await this.#signers[userId].writeContract({
            abi: Contract.abi,
            address: getAddress(Contract.address),
            functionName,
            args,
            account,
            chain: skaleChaosTestnet
        })
    }
}

export default new BackgroundSigners();

3- API

import { Router } from "express";
import BackgroundSigners from "./background_signers";
import { parseEther } from "viem";

const router = Router();

router.post("/mint", async (req, res) => {
    const userId: string = req.body.userId;
    const address = await BackgroundSigners.getUser(userId);
    
    try {
        await BackgroundSigners.backgroundSignerAction(userId, [address, parseEther("1")], "mint");
        return res.status(200).send("Minted Successfully");
    } catch (err) {
        return res.status(500).send("Error Minting");
    }
});

router.post("/burn", async (req, res) => {
    const userId: string = req.body.userId;
    const address = await BackgroundSigners.getUser(userId);
    
    try {
        await BackgroundSigners.backgroundSignerAction(userId, [parseEther("1")], "burn");
        return res.status(200).send("Burned Successfully");
    } catch (err) {
        return res.status(500).send("Error Burning");
    }
});

export default router;
@manuelbarbas
Copy link
Collaborator Author

What is: Wallet
Language: Javascript/Typescript, C#
Target: Web, Unity

@manuelbarbas manuelbarbas added the documentation Improvements or additions to documentation label Feb 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

1 participant