launchTestNode in 0.94.0 causes "Address already in use" errors and more

Migrated to TS SDK 0.94.0 and now all tests start failing with the following logs:

FuelError: 2024-08-20T07:45:17.400811Z  INFO new{name=fuel-core}:initialize_loop{service="SyncTask"}: fuel_core_services::service: 286: The service SyncTask is shut down
2024-08-20T07:45:17.400879Z  INFO new{name=fuel-core}:initialize_loop{service="ImportTask"}: fuel_core_services::service: 286: The service ImportTask is shut down
2024-08-20T07:45:17.401027Z  INFO new{name=fuel-core}:initialize_loop{service="GraphQL_Off_Chain_Worker"}: fuel_core_services::service: 286: The service GraphQL_Off_Chain_Worker is shut down
Error: Address already in use (os error 48)

I went through the Migration Log, but couldn’t find any required changes to the behaviour other than renaming the TTL param for provider.

Help appreciated.

Hi @xpluscal, thanks for the reports!

It would be ideal if you could share a test or two for us to see how it looks like. Are you perhaps specifying a nodeOptions.port value in your tests?

using launched = await launchTestNode({ nodeOptions: { port: "4000" }})`

This field was previously ignored but is now taken into consideration and, if you have it set, you can remove it and the tests should be unblocked from that aspect.

Sure, this is my setup function:

export async function setup(): Promise<
    {
        wallet1: WalletUnlocked;
        wallet2: WalletUnlocked;
        wallet3: WalletUnlocked;
        wallet4: WalletUnlocked;
        provider: Provider;
        feeSplitterContract: PropsFeeSplitterContract;
    }
> {
  const assets = TestAssetId.random(2);
  const message = new TestMessage({ amount: 1000 });

  const launched = await launchTestNode({
    walletsConfig: {
      count: 4, // Number of wallets to create
      assets, // Assets to use
      coinsPerAsset: 1, // Number of coins per asset
      amountPerCoin: 1_000_000, // Amount per coin
      messages: [message], // Initial messages
    },
    nodeOptions: {
      port: "4000",
    },
    launchNodeServerPort: "4000",
  });

  // Destructure the launched object to get wallets and contracts
  const {
    contracts: [],
    wallets: [wallet1, wallet2, wallet3, wallet4],
    provider,
  } = launched;

  console.log("PROVIDER: ", provider);

  const address = Address.fromDynamicInput(wallet1.address);
  const addressInput = { bits: address.toB256() };
  const addressIdentityInput = { Address: addressInput };

  const { waitForResult } =
    await PropsFeeSplitterContractFactory.deploy(
      wallet1,
      {
        salt: "0x0000000000000000000000000000000000000000000000000000000000000000" as BytesLike,
      }
    );

  const { contract: feeSplitterContract, transactionResult } =
    await waitForResult();

  // Initialize the Fee Splitter Contract
  const { waitForResult: waitForFeeSplitterConstructorResult } =
    await feeSplitterContract.functions
      .constructor(addressIdentityInput)
      .call();

  await waitForFeeSplitterConstructorResult();

  // Log hash of deployed fee splitter contract
  const feeSplitterContractId = feeSplitterContract.id;
  // console.log(
  //   "Props Fee Splitter Contract deployed at:",
  //   feeSplitterContractId.toB256()
  // );

  // Deploy Props Registry Contract
  const { waitForResult: waitForPropsRegistryResult } =
    await PropsRegistryContractFactory.deploy(
      wallet1,
      {
        salt: "0x0000000000000000000000000000000000000000000000000000000000000000" as BytesLike,
      }
    );

  const { contract: propsRegistryContract } =
    await waitForPropsRegistryResult();

  // Initialize the Props Registry Contract
  const { waitForResult: waitForPropsRegistryConstructorResult } =
    await propsRegistryContract.functions
      .constructor(addressIdentityInput)
      .call();

  await waitForPropsRegistryConstructorResult();

  // Log hash of deployed registry contract
  const registryContractId = propsRegistryContract.id;
  // console.log(
  //   "Props Registry Contract deployed at:",
  //   registryContractId.toB256()
  // );

  return { wallet1, wallet2, wallet3, wallet4, provider, feeSplitterContract };
}

Here’s a sample test:


  it("should create an edition instance", () => {
    expect(edition).toBeInstanceOf(Edition);
    expect(edition.id).toBe("edition-id");
    expect(edition.metadata?.name).toBe("Test Edition");
  });

  it("should connect an account to the edition", () => {
    edition.connect(wallets[1]);
    expect(edition.account).toBe(wallets[1]);
  });

@nedsalk will try removing that port now.

@nedsalk has anything changed regarding addContracts?
Getting contract not in inputs now, but running against frontend works fine.

Did you update your forc version to 0.63.1 and fuel-core to 0.33.0?

Assuming that needs to be done manually since there’s no nightly yet for that right?

That’s my current toolchain:

nightly-x86_64-apple-darwin (default)

forc : 0.63.1+nightly.20240820.169f91ae0a

  • forc-client

  • forc-deploy : 0.63.1+nightly.20240820.169f91ae0a

  • forc-run : 0.63.1+nightly.20240820.169f91ae0a

  • forc-crypto : 0.63.1+nightly.20240820.169f91ae0a

  • forc-debug : 0.63.1+nightly.20240820.169f91ae0a

  • forc-doc : 0.63.1+nightly.20240820.169f91ae0a

  • forc-fmt : 0.63.1+nightly.20240820.169f91ae0a

  • forc-lsp : 0.63.1+nightly.20240820.169f91ae0a

  • forc-tx : 0.63.1+nightly.20240820.169f91ae0a

  • forc-wallet : 0.9.0+nightly.20240820.29d9b25c2c

fuel-core : 0.32.1+nightly.20240816.a2d8d2dc62

fuel-core-keygen : not found

forc 0.63.1 and fuel-core 0.33.0 have been released so you can add them, best to do it via a custom toolchain as of now because the testnet toolchain hasn’t been updated yet.

fuelup toolchain new custom && fuelup component add forc@0.63.1 && fuelup component add fuel-core@0.33.0.

Thanks, all updated, similar issue.
I’m basically just passing the created contract directly back as input for the next call.

Could this be an issue with UTXO of the launchTestNode or anything else?

const salt: BytesLike = randomBytes(32);
const { waitForResult } =
  await EditionContractFactory.deploy(
    owner,
    {
      configurableConstants,
      salt,
    }
  );

const { contract } = await waitForResult();

const registryContract = new RegistryContract(
  registryContractAddress,
  owner
);

const { waitForResult: waitForResultConstructor } =
  await registryContract.functions
    .init_edition(
      { bits: contract.id.toB256() }
    )
    .addContracts([contract])
    .call();

Where do you get the registryContractAddress from? You need to deploy the RegistryContract via launchTestNode as well. That error happens when you try to execute a transaction with a non-existing contract.

Also, it seems to me that you’re not using the launchTestNode utility as intended, especially because you’re not returning a cleanup function anywhere, which means that you’re going to be having fuel-core nodes staying on even after tests are finished. I did a bit of a refactor based on my understanding of the code as well and the defaults as well besides adding a cleanup field in the return.

Two things to note:

  1. cleanup is returned
  2. contracts are directly deployed via the contractsConfigs field
export async function setup(): Promise<
    {
        wallet1: WalletUnlocked;
        wallet2: WalletUnlocked;
        wallet3: WalletUnlocked;
        wallet4: WalletUnlocked;
        provider: Provider;
        feeSplitterContract: PropsFeeSplitterContract;
        cleanup: () => void;
    }
> {

  const message = new TestMessage({ amount: 1000 });

  const launched = await launchTestNode({
    walletsConfig: {
      count: 4, // Number of wallets to create
      messages: [message], // Initial messages
    },
    contractsConfigs: [
        { factory: PropsFeeSplitterContractFactory },
        { factory: PropsRegistryContractFactory }
    ]
  });

  // Destructure the launched object to get wallets and contracts
  const {
    contracts: [feeSplitterContract, propsRegistryContract],
    wallets: [wallet1, wallet2, wallet3, wallet4],
    provider,
    cleanup
  } = launched;

  console.log("PROVIDER: ", provider);

  const addressIdentityInput = { Address: wallet1.address.toB56() };

  // Initialize the Fee Splitter Contract
  const { waitForResult: waitForFeeSplitterConstructorResult } =
    await feeSplitterContract.functions
      .constructor(addressIdentityInput)
      .call();

  await waitForFeeSplitterConstructorResult();

  // Log hash of deployed fee splitter contract
  const feeSplitterContractId = feeSplitterContract.id;
  // console.log(
  //   "Props Fee Splitter Contract deployed at:",
  //   feeSplitterContractId.toB256()
  // );

  // Initialize the Props Registry Contract
  const { waitForResult: waitForPropsRegistryConstructorResult } =
    await propsRegistryContract.functions
      .constructor(addressIdentityInput)
      .call();

  await waitForPropsRegistryConstructorResult();

  // Log hash of deployed registry contract
  const registryContractId = propsRegistryContract.id;
  // console.log(
  //   "Props Registry Contract deployed at:",
  //   registryContractId.toB256()
  // );

  return { wallet1, wallet2, wallet3, wallet4, provider, feeSplitterContract, cleanup };
}

And then you have to call cleanup at the end of each test:

const { cleanup, ...rest } = await setup();
// do stuff in test
// done with node?
cleanup();

If you want to make it using-compatible, you can do this to the return:

const returnObj = { wallet1, wallet2, wallet3, wallet4, provider, feeSplitterContract, cleanup };
return Object.assign(returnObj, {[Symbol.disposable]: cleanup});

and then you can write your tests like this:

using launched = await setup();
// do testing
// when launched variable goes out of scope,
// the cleanup function will be called automagically