stellar / js-stellar-sdk

Main Stellar client library for the JavaScript language.
https://stellar.github.io/js-stellar-sdk/
Apache License 2.0
640 stars 313 forks source link

Getting 'undefined' as a return value when invoking a getter smartcontract function #1049

Open bhupendra-chouhan opened 1 month ago

bhupendra-chouhan commented 1 month ago

To demonstrate the issue, I built a small full-stack dApp called "Anonymous Feedback dApp" using ReactJS, TailwindCSS, Stellar-SDK, Soroban-SDK, and Freighter Wallet.

GitHub : https://github.com/bhupendra-chouhan/Anonymous-Feedback-Soroban

Please refer to the project's README file for a detailed explanation of the issue, along with the installation/setup guide and steps to reproduce it (screenshots included).

Issue: In the code below, there is a function named contractInt(), which is used to invoke contract functions. While it successfully stores data on-chain when invoking a setter function, the problem occurs when I try to retrieve a return value using the returnValue() method from a getter function. Instead of the expected output, I receive 'undefined'.

/* Soroban.js */

import {
  Contract,
  SorobanRpc,
  TransactionBuilder,
  Networks,
  BASE_FEE,
  nativeToScVal,
} from "@stellar/stellar-sdk";
import { userSignTransaction } from "./Freighter";

let rpcUrl = "https://soroban-testnet.stellar.org";

let contractAddress =
  "CDAN4KQKD633XF6MCOHI7Q3DJQX4E7ENCGKUBHGQKIKJWI6DVDPX54XW";

// coverting String to ScVal form
const stringToScValString = (value) => {
  return nativeToScVal(value); // XDR format conversion
};

const numberToU64 = (value) => {
  return nativeToScVal(value, { type: "u64" });
};

let params = {
  fee: BASE_FEE,
  networkPassphrase: Networks.TESTNET,
};

// Transaction Builder Function:
async function contractInt(caller, functName, values) {
  const server = new SorobanRpc.Server(rpcUrl, { allowHttp: true });
  const sourceAccount = await server.getAccount(caller);
  const contract = new Contract(contractAddress);
  let builtTransaction;

  if (values == null) {
    builtTransaction = new TransactionBuilder(sourceAccount, params)
      .addOperation(contract.call(functName))
      .setTimeout(30)
      .build();
  } else if (Array.isArray(values)) {
    builtTransaction = new TransactionBuilder(sourceAccount, params)
      .addOperation(contract.call(functName, ...values))
      .setTimeout(30)
      .build();
  } else {
    builtTransaction = new TransactionBuilder(sourceAccount, params)
      .addOperation(contract.call(functName, values))
      .setTimeout(30)
      .build();
  }

  let _buildTx = await server.prepareTransaction(builtTransaction);

  let prepareTx = _buildTx.toXDR(); // pre-encoding (converting it to XDR format)

  let signedTx = await userSignTransaction(prepareTx, "TESTNET", caller);

  let tx = TransactionBuilder.fromXDR(signedTx, Networks.TESTNET);

  try {
    let sendResponse = await server.sendTransaction(tx).catch(function (err) {
      console.error("Catch-1", err);
      return err;
    });
    if (sendResponse.errorResult) {
      throw new Error("Unable to submit transaction");
    }
    if (sendResponse.status === "PENDING") {
      let getResponse = await server.getTransaction(sendResponse.hash);
      //   we will continously checking the transaction status until it gets successfull added to the blockchain ledger or it gets rejected
      while (getResponse.status === "NOT_FOUND") {
        getResponse = await server.getTransaction(sendResponse.hash);
        await new Promise((resolve) => setTimeout(resolve, 1000));
        }

        console.log(`getTransaction response: ${JSON.stringify(getResponse)}`);

      if (getResponse.status === "SUCCESS") {
        // Make sure the transaction's resultMetaXDR is not empty
        if (!getResponse.resultMetaXdr) {
          throw "Empty resultMetaXDR in getTransaction response";
        }

        // Find the return value from the contract and return it
        let transactionMeta = getResponse.resultMetaXdr;
        let returnValue = transactionMeta.v3().sorobanMeta().returnValue();
        console.log(`Transaction result: ${returnValue.value()}`);
      } else {
        throw `Transaction failed: ${getResponse.resultXdr}`;
      }
    } else {
        throw sendResponse.errorResultXdr;
    }
  } catch (err) {
    // Catch and report any errors we've thrown
    console.log("Sending transaction failed");
    console.log(JSON.stringify(err));
  }
}

// Interaction Functions: Built To interact with it's respective smart contract functions:

async function sendFeedback(caller, fbData) {  
  let value = stringToScValString(fbData); //XDR format  let result;

  try {
    let result = await contractInt(caller, "send_feedback", value);
    console.log("Your Feedback ID is: ", result); // ⚠️ 'result' should be an object, but getting 'undefined'
  } catch (error) {
    console.log("Unable to create Feedback!!, ", error);
  }

  //  Converting to regular Number type:
  // let fbId = Number(result?._value?._attributes?.val?._value)
  // return fbId;
}
async function fetchFeedback(caller, fb_id) {
  let value = numberToU64(fb_id);
  let result;

    try {
        result = await contractInt(caller, "fetch_feedback", value);
        console.log(`Fetched Feedback for the feedback-Id ${fb_id} is : ${result}`); // ⚠️ 'result' should be an object, but getting 'undefined'
    } catch (error) {
        console.log("Unable to fetch Feedback!!, ", error);
    }

    //  Converting to regular string type:
    // let feedback = result?._value?._attributes?.val?._value?.toString();
    // return feedback;
}

export {
    sendFeedback,
    fetchFeedback
};

Screenshots of Issue (You can reproduce the issue by following the screenshots):

  1. Creating a Feedback and Storing it onchain by invoking the send_feedback() smartcontract function: image

    Result:

    • Expected output: 4
    • Output got: Undefined image
  2. Fetching a feedback with feedback-id 4 by invoking the fetch_feedback() smartcontract function: image

    Result:

    • Expected output: Feedback Number 4
    • Output got: Undefined image
Shaptic commented 1 month ago

There's a few things I'd suggest here:

bhupendra-chouhan commented 1 month ago

Thank you for your response and suggestions, @Shaptic .

Replying to each of the points you mentioned:

This is the newly deployed smart contract address: CBG7QFA5CWUIJ6QQQSCWS33UNV6TN3EVQHRZLR5VYJWT5X73J6Y46U7A.

You can see all the function calls and successful transactions that have occurred under this deployed smart contract address by visiting this page: https://stellar.expert/explorer/testnet/contract/CBG7QFA5CWUIJ6QQQSCWS33UNV6TN3EVQHRZLR5VYJWT5X73J6Y46U7A

Shaptic commented 1 month ago

I see, thanks for the clarifications.

The contractInt function doesn't return anything right now because I was first trying to print the return value in the browser console.

But the bug report is about you getting undefined as a return value, and you would get that as the return value even if there was success! :laughing:

The reason why I said your transaction failed is because of the console output in your second screenshot (Sending transaction failed). Based on the code, this occurs in your main block of submission code, but it's unclear why its occurring. Keep in mind that catch statement will catch all sorts of other errors. I would recommend running the code without that try/catch block to see what the real error is.

For example, you use the Networks.TESTNET variable in one place and the string "TESTNET" in your invocation of userSignTransaction which may be causing issues. But that's just an example. It could be anything in that block and you just call it "failed to send." Based on what you said, it seems like the transaction actually does successfully send, but you're triggering the catch elsewhere which causes no return value.

Iwueseiter commented 1 month ago

I am applying to this issue via OnlyDust platform.

My background and how it can be leveraged

I'm a frontend and smart contract developer. I've contributed to Projects here on onlydust and with that experience, I would handle this task as expected. This would also be my first time contributing to this project.

How I plan on tackling this issue

To implement this,

  1. I will create a Feedback and Storing it onchain by invoking the send_feedback() smartcontract function. I will ensure it displays the expeted output which is 4.

  2. Fetch a feedback with feedback-id 4 by invoking the fetch_feedback() smartcontract function, and ensure it displays the expected Feedback Number 4.

ETA: 48hrs

martinvibes commented 1 month ago

I am applying to this issue via OnlyDust platform.

My background and how it can be leveraged

hello i am a frontend dev and blockchain developer please can i work on this issue :) and would love to be a contributor

How I plan on tackling this issue

please kindly assign me and i'll get straight to work