FuelLabs / fuels-ts

Fuel Network Typescript SDK
https://docs.fuel.network/docs/fuels-ts/
Apache License 2.0
44.04k stars 1.36k forks source link

fuel-ts UX Assesment #1093

Closed SilentCicero closed 11 months ago

SilentCicero commented 1 year ago

Motivation

Abstract

I've been using the fuels-ts SDK to test a simple counter contract. I've found numerous areas we can improve UX and some possible bugs as well.

High Level Assesment

Areas of Improvement: Docs

Areas of Improvement: General DevEx

Areas of Improvement: Typegen

Bugs

Usage example

Below is the cleanest example of testing smart contracts with the typescript SDK.

As you can see, if the typegen was smarter and included the bin and storage-slots into the factory, the primary deployment method could be reduced to a one line command (greatly simplifying DevEx).

Second, if you uncomment the storageSlots and storage ReadFileSync, you will see the Panic that occurs afterward. This does not happen in the Rust SDK with similar code.

You will notice the launch_node_get_wallets function greatly simplifies the boilerplate and is extremely useful for typescript testing of smart contracts. This can also be extended for more complex frontend setups as well (which have a node backend or setup services).

import { ContractFactory } from 'fuels';
import { readFileSync } from 'fs';
import { FuelCounterAbi__factory } from '../types';
import { launch_node_get_wallets } from './lib/helpers';

async function main() {
    // Setup a test environment.
    const { cleanup, wallet } = await launch_node_get_wallets();

    // Contract data.
    const bytecode = readFileSync(`./out/debug/fuel-counter.bin`);
    // const storage = JSON.parse(readFileSync(`./out/debug/fuel-counter-storage_slots.json`).toString());

    // Contract factory.
    const factory = new ContractFactory(bytecode, FuelCounterAbi__factory.abi, wallet);
    const instance = await factory.deployContract({
        // storageSlots: storage, // PANIC this throws.
    });
    const contract = FuelCounterAbi__factory.connect(instance.id, wallet);

    // The above code should eventually be simplified to -> deploy(config time consts, storage slots, wallet, config = {}) like so:
    // The abi, bin, storage slots and default config time constants could all be packed into the type directly by the typegen
    // const contract = await FuelCounter__factory.deploy({}, [], wallet);

    // Increment counter.
    await contract
        .functions
        .increment(1)
        .call();

    // Check the value.
    console.assert((await contract
        .functions
        .get() // should be .simulate().
        .get())
        .value
        .toNumber() == 1);

    // Cleanup the environment.
    await cleanup();
}

main()
.catch(console.error);

Possible implementations

Launch Node and Get Wallets (with fuel_core helpers)

This code launches the fuel-core instance and provides some handy chain configuration helpers to faucet a bunch of wallets. Then the method returns a bunch of test wallets ready to go, and pre-configured with the correct provider.

The cleanup method closes out the child fuel-core process at the end of the async function.

import { Provider, NativeAssetId, WalletUnlocked, Wallet } from 'fuels';
const { spawn } = require('child_process');
const fs = require('fs').promises;

// Run fuel-core and stop it when the process has ended.
export function fuel_core(
    chainConfig: String,
    params:String[] = ['--db-type', 'in-memory']
    ): Promise<any> {
    return new Promise(resolve => {
        // The string to search for GraphQL service start.
        const graph_ql_start = 'graphql_api::service';

        // Spawn a fuel-core instance with a specific chainConfig.
        const child = spawn('fuel-core', [
            'run', 
            '--chain', chainConfig,
            ...params
        ]);

        // Set the std encoding to UTF8.
        child.stderr.setEncoding('utf8');

        // Cleanup function where fuel-core is stopped.
        const cleanup = function() {
            child.kill('SIGINT');
        };

        // Look for a specific graphql start point in the output.
        child.stderr.on('data', function(chunk:any) {
            // Look for the graphql service start.
            if (chunk.indexOf(graph_ql_start) !== -1) {
                // Resolve with the cleanup method.
                resolve(cleanup);
            }
        });

        // Process exit.
        process.on('exit', cleanup);

        // Catches ctrl+c event.
        process.on('SIGINT', cleanup);

        // Catches "kill pid" (for example: nodemon restart).
        process.on('SIGUSR1', cleanup);
        process.on('SIGUSR2', cleanup);

        // Catches uncaught exceptions.
        process.on('uncaughtException', cleanup);
    });
}

// Build a test environment.
export async function launch_node_get_wallets(inputConfig = {}): Promise<{
    cleanup: Function,
    wallet: WalletUnlocked,
    wallets: WalletUnlocked[],
    provider: Provider }> {

    // Handle config object. 
    const config = Object.assign({
        numWallets: 10,
        chainConfig: null,
        chainConfigPath: '.chainConfig.json',
        fuelCoreParams: undefined,
        providerURL: 'http://127.0.0.1:4000/graphql',
    }, inputConfig);

    // Create a number of wallets.
    const signers = (new Array(config.numWallets))
        .fill(0)
        .map(() => Wallet.generate())
        .map(v => v.signer());

    // Default chian config.
    const defaultChainConfig = {
        "chain_name": "local_testnet",
        "block_gas_limit": 5000000000,
        "initial_state": {
            "coins": signers.map(signer => {
                return {
                    "owner": signer.address.toHexString(),
                    "amount": "0xFFFFFFFFFFFFFFFF",
                    "asset_id": NativeAssetId,
                };
            }),
        },
        "transaction_parameters": {
            "contract_max_size": 16777216,
            "max_inputs": 255,
            "max_outputs": 255,
            "max_witnesses": 255,
            "max_gas_per_tx": 500000000,
            "max_script_length": 1048576,
            "max_script_data_length": 1048576,
            "max_static_contracts": 255,
            "max_storage_slots": 255,
            "max_predicate_length": 1048576,
            "max_predicate_data_length": 1048576,
            "gas_price_factor": 1000000000,
            "gas_per_byte": 4,
            "max_message_data_length": 1048576
        },
        "consensus": {
            "PoA": {
                "signing_key": "0x94ffcc53b892684acefaebc8a3d4a595e528a8cf664eeb3ef36f1020b0809d0d"
            }
        }
    };

    // Write a temporary chain configuration file.
    await fs.writeFile(
        config.chainConfigPath,
        JSON.stringify(config.chainConfig ? config.chainConfig : defaultChainConfig), 
        'utf8'
    );

    // Run the fuel-core node.
    const close = await fuel_core(config.chainConfigPath, config.fuelCoreParams);

    // Create a provider object.
    const provider = new Provider(config.providerURL);

    // Connect all the wallets.
    const wallets = signers.map(signer => Wallet.fromPrivateKey(signer.privateKey, provider));

    // Define cleanup method.
    async function cleanup() {
        await fs.unlink(config.chainConfigPath);
        close();
    }

    // Return the environment.
    return {
        cleanup,
        wallet: wallets[0],
        wallets,
        provider,
    };
}
arboleya commented 11 months ago

@Dhaiwat10 Did we address everything from this issue? 🤔

Dhaiwat10 commented 11 months ago

@arboleya I think so, yes. I don't see anything that we haven't eventually worked on. All the issues referencing this one have been closed.