hyperledger-iroha/iroha-javascript

getting Unsupported Operation for transfers

Closed this issue · 4 comments

I'm getting the following error (from node logs):

2023-11-28T19:06:08.554860Z  WARN consensus: iroha_core::block::pending: Transaction validation failed
reason=Validation failed caused_by=Some(NotPermitted("TransferExpr { source_id:
AssetId(energy##matias@cognition), object: 12_u32, destination_id: AssetId(energy##pepe@cognition) }:
Unsupported operation"))

This is my dev setup:

  • Node: stable-2.0.0-pre-rc.20
  • iroha2/client: 7.0.0
  • iroha2/data-model: 7.0.0

This is my code:

const transfer = async (from, to, asset, quantity) => {
  logger.info(`[srv] transfer ${from} to ${to} ${quantity} of ${asset} ...`);

  const [senderAccName, senderDomainId] = from.split('@');
  const senderAccId = sugar.accountId(senderAccName, senderDomainId);

  const [receiverAccName, receiverDomainId] = to.split('@');
  const receiverAccId = sugar.accountId(receiverAccName, receiverDomainId);

  const [assetName, assetDomainId] = asset.split('#');
  const assetDefId = sugar.assetDefinitionId(assetName, assetDomainId);

  const { IdBox } = datamodel;
  const { assetId } = sugar;

  const senderIdBox = IdBox('AssetId', assetId(senderAccId, assetDefId));
  const receiverIdBox = IdBox('AssetId', assetId(receiverAccId, assetDefId));
  const value = sugar.value.numericU32(quantity);

  const transferAsset = sugar.instruction.transfer(
    senderIdBox,
    value,
    receiverIdBox
  );

  const instr = sugar.executable.instructions(transferAsset);
  const { pre, client } = _clientFactory('matias', 'cognition');
  try {
    const data = await client.submitExecutable(pre, instr);
    logger.info('[srv] transfer completed ...');
    return { data };
  } catch (error) {
    console.log(error);
    return { error: error.message };
  }
};

Additional information:

  • I get the same result without using the sugar package.
  • I can transfer with no problem using CLI, with the same config file, to the same node running on a local Docker instance.

The node is created from the following Dockerfile:

# Use the official hyperledger/iroha2 base image with the specified version
FROM hyperledger/iroha2:stable-2.0.0-pre-rc.20

# Set environment variables
ENV TORII_P2P_ADDR 0.0.0.0:1337
ENV TORII_API_URL 0.0.0.0:8080
ENV TORII_TELEMETRY_URL 0.0.0.0:8180
ENV IROHA_PUBLIC_KEY "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"
ENV IROHA_PRIVATE_KEY '{"digest_function": "ed25519", "payload": "282ED9F3CF92811C3818DBC4AE594ED59DC1A2F78E4241E31924E101D6B1FB831C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}'
ENV SUMERAGI_TRUSTED_PEERS '[{"address":"0.0.0.0:1337", "public_key": "ed01201C61FAF8FE94E253B93114240394F79A607B7FA55F9E5A41EBEC74B88055768B"}]'
ENV IROHA_GENESIS_ACCOUNT_PUBLIC_KEY "ed01203F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255"
ENV IROHA_GENESIS_ACCOUNT_PRIVATE_KEY '{"digest_function": "ed25519", "payload": "038AE16B219DA35AA036335ED0A43C28A2CC737150112C78A7B8034B9D99C9023F4E3E98571B55514EDC5CCF7E53CA7509D89B2868E62921180A6F57C2F4E255"}'
ENV IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_COUNT_LIMIT 100
ENV IROHA_GENESIS_WAIT_FOR_PEERS_RETRY_PERIOD_MS 500
ENV IROHA_GENESIS_GENESIS_SUBMISSION_DELAY_MS 1000

# Expose the required ports
EXPOSE 1337 8080 8180

# Set the working directory
WORKDIR /opt/iroha_data

# Copy the configuration files
COPY ./configs/peer/stable /config

# Command to run on container startup
CMD ["iroha", "--submit-genesis"]

And executed like this:

docker build -t local-iroha .
docker run -d -p 8080:8080 -p 1337:1337 -p 8180:8100  local-iroha
6r1d commented

@a1salimbene, please add:

  • the commit on the stable branch (git rev-parse --short HEAD output)
  • example code without the sugar class, as it adds an unpredictable factor we can not test yet

@6r1d information request below:

> git rev-parse --short HEAD
7acb8a44
> git branch
  iroha2-dev
  iroha2-lts
* iroha2-stable

Here's the code without using the sugar class. I'm getting exact same err:

const transfer = async (from, to, asset, quantity) => {
  logger.info(`[srv] transfer ${from} to ${to} ${quantity} of ${asset} ...`);

  const [senderAccName, senderDomainId] = from.split('@');
  const [receiverAccName, receiverDomainId] = to.split('@');
  const [assetName, assetDomainId] = asset.split('#');

  const {
    AccountId,
    AssetDefinitionId,
    AssetId,
    DomainId,
    InstructionExpr,
    Expression,
    Executable,
    IdBox,
    NumericValue,
    TransferExpr,
    Value,
    VecInstructionExpr,
  } = datamodel;

  const senderDomain = DomainId({
    name: senderDomainId,
  });
  const receiverDomain = DomainId({
    name: receiverDomainId,
  });
  const assetDomain = DomainId({
    name: assetDomainId,
  });

  const assetDefinitionId = AssetDefinitionId({
    name: assetName,
    domain_id: assetDomain,
  });

  const fromAccount = AccountId({
    name: senderAccName,
    domain_id: senderDomain,
  });

  const toAccount = AccountId({
    name: receiverAccName,
    domain_id: receiverDomain,
  });

  const amountToTransfer = Value('Numeric', NumericValue('U32', quantity));

  const evaluatesToAssetId = (assetId) =>
    Expression('Raw', Value('Id', IdBox('AssetId', assetId)));

  const transferAssetInstruction = TransferExpr({
    source_id: evaluatesToAssetId(
      AssetId({
        definition_id: assetDefinitionId,
        account_id: fromAccount,
      })
    ),
    destination_id: evaluatesToAssetId(
      AssetId({
        definition_id: assetDefinitionId,
        account_id: toAccount,
      })
    ),
    object: Expression('Raw', amountToTransfer),
  });

  const instr = InstructionExpr('Transfer', transferAssetInstruction);
  const exec = Executable('Instructions', VecInstructionExpr([instr]));

  const { pre, client } = _clientFactory('matias', 'cognition');
  try {
    const data = await client.submitExecutable(pre, exec);
    logger.info('[srv] transfer completed ...');
    return { data };
  } catch (error) {
    console.log(error);
    return { error: error.message };
  }
};

I found the problem looking at a rust test, see below:

fn transfer_isi_should_be_valid() {
    let _instruction = TransferExpr::new(
        IdBox::AssetId("btc##seller@crypto".parse().expect("Valid")),
        12_u32,
        IdBox::AccountId("buyer@crypto".parse().expect("Valid")),
    );
}

The third param is an AccountId (destination target), but the first param is an AssetId (which already includes the source AccountId.

Thus, this works:

  const toAccount = sugar.accountId(receiverAccName, receiverDomainId);
  const transferIsi = sugar.instruction.transfer(
    datamodel.IdBox('AssetId', sugar.assetId(fromAccount, assetDefinitionId)),
    amountToTransfer,
    datamodel.IdBox('AccountId', toAccount)
  );

but this doesn't

  const transferAsset = sugar.instruction.transfer(
    datamodel.IdBox('AssetId', sugar.assetId(senderAccId, assetDefId)),
    amountToTransfer,
    datamodel.IdBox('AssetId', sugar.assetId(receiverAccId, assetDefId))
  );

Is worth mentioning that the test case from the JS code appears to be wrong then. In all js samples I found, source and destination IDs are AssetId. For instance, this is from the JS docs:

const transferAssetInstruction = Instruction(
  'Transfer',
  TransferBox({
    source_id: evaluatesToAssetId(
      AssetId({
        definition_id: assetDefinitionId,
        account_id: fromAccount,
      }),
    ),
    destination_id: evaluatesToAssetId(
      AssetId({
        definition_id: assetDefinitionId,
        account_id: toAccount,
      }),
    ),
    object: EvaluatesToValue({
      expression: Expression('Raw', amountToTransfer),
    }),
  }),
)

However, this follows the logic from the cli that implements "from" → "to" → "asset", instead of "asset" → "to" like Rust:

iroha_client_cli asset transfer --from matias@cognition --to pepe@cognition --asset-id energy#cognition --quantity 5

So it's all a bit confusing but hopefully, it helps. Now I don't think there's a bug or anything like it. I think this is a case of outdated documentation or lack thereof.

6r1d commented

Thank you. I'm closing this issue and will raise the topic with our documentation team.