Creating a new contract client

In this tutorial, we'll demonstrate how to create a client of any contract, which can be used to simplify and automate deployments and calls to contract methods.

To illustrate the process, we'll use the liquidity pool contract available in this repository.

Prerequisites

  • Basic Understanding of Stellar Concepts: Familiarize yourself with Stellar network fundamentals, including assets, accounts, trustlines, and transactions. For more in-depth information, refer to Stellar's official documentation.

  • Basic understanding of Stellar's Soroban: Familiarize yourself with Soroban and how smart contracts integrate with the Stellar network. For more in-depth information refer to Soroban's official documentation.

  • Node.js Environment: Set up a Node.js environment to run your JavaScript code.

  • StellarPlus Library: Ensure that the StellarPlus library is installed in your project. For the installation steps, refer to Quick Start.

Step-by-Step Guide

Defining the ContractSpec

The first step is to define the XDR spec entries for the contract. To do this, we'll create a constants.ts file and export this data as a constant.

import { ContractSpec } from "@stellar/stellar-sdk";

export const poolSpec = new ContractSpec([
  "AAAAAAAAAAAAAAAKaW5pdGlhbGl6ZQAAAAAAAwAAAAAAAAAPdG9rZW5fd2FzbV9oYXNoAAAAA+4AAAAgAAAAAAAAAAd0b2tlbl9hAAAAABMAAAAAAAAAB3Rva2VuX2IAAAAAEwAAAAA=",
    "AAAAAAAAAAAAAAAIc2hhcmVfaWQAAAAAAAAAAQAAABM=",
    "AAAAAAAAAAAAAAAHZGVwb3NpdAAAAAAFAAAAAAAAAAJ0bwAAAAAAEwAAAAAAAAAJZGVzaXJlZF9hAAAAAAAACwAAAAAAAAAFbWluX2EAAAAAAAALAAAAAAAAAAlkZXNpcmVkX2IAAAAAAAALAAAAAAAAAAVtaW5fYgAAAAAAAAsAAAAA",
    "AAAAAAAAAAAAAAAEc3dhcAAAAAQAAAAAAAAAAnRvAAAAAAATAAAAAAAAAAVidXlfYQAAAAAAAAEAAAAAAAAAA291dAAAAAALAAAAAAAAAAZpbl9tYXgAAAAAAAsAAAAA",
    "AAAAAAAAAAAAAAAId2l0aGRyYXcAAAAEAAAAAAAAAAJ0bwAAAAAAEwAAAAAAAAAMc2hhcmVfYW1vdW50AAAACwAAAAAAAAAFbWluX2EAAAAAAAALAAAAAAAAAAVtaW5fYgAAAAAAAAsAAAABAAAD7QAAAAIAAAALAAAACw==",
    "AAAAAAAAAAAAAAAJZ2V0X3JzcnZzAAAAAAAAAAAAAAEAAAPtAAAAAgAAAAsAAAAL",
    "AAAAAAAAAAAAAAAKZ2V0X3NoYXJlcwAAAAAAAAAAAAEAAAAL"
]);

One of the ways to get the spec entries of your contract is to take this data from the library generated by the bindings in Typescript. Refer to the Soroban official documentation for more details.

Creating the client

Let's start building the client of our contract by creating a client.ts file. It's a good practice to enumerate the contract methods names to use them in camel case, so let's start by doing this.

enum methods {
    initialize = "initialize",
    shareId = "share_id",
    deposit = "deposit",
    getReserves = "get_rsrvs",
    swap = "swap",
    withdraw = "withdraw"
}

Now we need to build the methods for invoking our contract. To do this, we'll use a class that extends ContractEngine.

import { StellarPlus } from "stellar-plus";
import { ContractEngineConstructorArgs } from "stellar-plus/core/contract-engine/types";

export class PoolClient extends StellarPlus.ContractEngine {
  constructor(args: ContractEngineConstructorArgs) {
    super(args);
  }
}

Each method needs to be declared individually, with the parameters according to those defined in the contract. The main logic for invoking the contract is done through the invokeContract method, but you can include any additional logic you think is necessary based on your application's rules.

Let's start by implementing the deposit method, structured as follows in the contract:

fn deposit(
    e: Env, 
    to: Address, 
    desired_a: i128, 
    min_a: i128, 
    desired_b: i128, 
    min_b: i128
);

The function for invoking this method on our client needs to contain the same name as the method, the name of the parameters, and types as the contract.

import { Address, i128 } from "stellar-plus/types";

export class PoolClient extends StellarPlus.ContractEngine {
    ...

    async deposit(args: {
            to: Address;
            desiredA: i128;
            minA: i128;
            desiredB: i128;
            minB: i128;
        },
        txInvocation: TransactionInvocation;
    ): Promise<void> {
        const methodArgs = {
            to: to,
            desired_a: desiredA,
            min_a: minA,
            desired_b: desiredB,
            min_b: minB
        }
        await this.invokeContract({
            method: methods.deposit,
            methodArgs: methodArgs,
            ...txInvocation,
        });
    }
}

To help convert types between Rust and Typescript, import the necessary types from Stellar-plus library.

Methods that exclusively perform reads without ledger modifications can adopt the same structure shown above. However, for these cases, it is possible to use the readFromContract method instead. This method only simulates the transaction in the ledger without actually executing it, making the function faster and free of fees.

export class PoolClient extends StellarPlus.ContractEngine {
    ...
    
    async shareId(txInvocation: TransactionInvocation): Promise<Address> {
        return (await this.readFromContract({
            method: methods.shareId,
            methodArgs: {},
            header: txInvocation.header,
        })) as Address;
    }
}

Client usage

After completing the implementation of all contract methods intended for your application, your client is ready for use. Simply initialize it with the previously defined spec and the desired network.

import { StellarPlus } from "stellar-plus";
import { PoolClient } from "./client"
import { poolSpec } from "./constants"

const network = StellarPlus.Constants.testnet

const poolClient = new PoolClient({
    network,
    spec: poolSpec,
    contractId: "CBK..."
});

// Invoke the transactions using your client
poolClient.deposit({
        to: "G...";
        desiredA: BigInt(10000);
        minA: BigInt(9000);
        desiredB: BigInt(7000);
        minB: BigInt(5000);
    },
    txInvocation
)

The txInvocation defines the parameters that will be included in the transactions. You will need an account to define it:

const acc = new StellarPlus.Account.DefaultAccountHandler({
  network,
  secretKey: "S...",
});
  
const txInvocation = {
  header: {
    source: acc.getPublicKey(),
    fee: "100000000",
    timeout: 30,
  },
  signers: [acc],
};

It is also possible to create a contract deployment process using your client with the wasm file.

import { StellarPlus } from "stellar-plus";
import { PoolClient } from "./client"
import { poolSpec } from "./constants"

const network = StellarPlus.Constants.testnet

const poolClient = new PoolClient({
    network,
    spec: poolSpec,
    wasm: "path-to-contract-wasm"
});

poolClient.uploadWasm(txInvocation)
poolClient.deploy(txInvocation)

If you already have the hash of the contract, it is possible to use it directly.

const poolClient = new PoolClient({
    network,
    spec: poolSpec,
    wasmHash: "your-wasm-hash"
});

poolClient.deploy(txInvocation)

If you find it more convenient, you can add methods to your client that handle instantiation, deployment, and other tasks. This makes the overall process more user-friendly and reusable.

Last updated