Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prettify typegen API #2686

Closed
nedsalk opened this issue Jul 3, 2024 · 5 comments · Fixed by #2824
Closed

Prettify typegen API #2686

nedsalk opened this issue Jul 3, 2024 · 5 comments · Fixed by #2824
Assignees
Labels
feat Issue is a feature

Comments

@nedsalk
Copy link
Contributor

nedsalk commented Jul 3, 2024

The end goal is to have typegen only output types and integrate those types with the real classes via generic type parameters. typegen currently outputs code which casts programs as the types it generates. For example, for a simple counter contract which looks like this:

abi Counter {
    #[storage(read)]
    fn get_count() -> u64;

    #[storage(write, read)]
    fn increment_count(amount: u64) -> u64;
}

The generated outputs are CounterAbi.d.ts and CounterAbi__factory.ts, respectively:

// CounterAbi.d.ts
interface CounterAbiInterface extends Interface {
  functions: {
    get_count: FunctionFragment;
    increment_count: FunctionFragment;
  };
}

export class CounterAbi extends Contract {
  interface: CounterAbiInterface;
  functions: {
    get_count: InvokeFunction<[], BN>;
    increment_count: InvokeFunction<[amount: BigNumberish], BN>;
  };
}
// CounterAbi__factory.ts
export const CounterAbi__factory = {
  abi: _abi,

  storageSlots: _storageSlots,

  createInterface(): CounterAbiInterface {
    return new Interface(_abi) as unknown as CounterAbiInterface
  },

  connect(
    id: string | AbstractAddress,
    accountOrProvider: Account | Provider
  ): CounterAbi {
    return new Contract(id, _abi, accountOrProvider) as unknown as CounterAbi
  },

  async deployContract(
    bytecode: BytesLike,
    wallet: Account,
    options: DeployContractOptions = {}
  ): Promise<CounterAbi> {
    const factory = new ContractFactory(bytecode, _abi, wallet);

    const contract = await factory.deployContract({
      storageSlots: _storageSlots,
      ...options,
    });

    return contract as unknown as CounterAbi;
  }
}

The idea is for CounterAbi.d.ts to contain an appropriate type representation of the ABI which would be passed to Interface<TAbi> and Contract<TAbi> classes, removing the need for the as unknown as CounterAbi casts. Below is an idea of the updated CounterAbi.d.ts and CounterAbi__factory.ts contents, respectively:

// CounterAbi.d.ts
export interface CounterAbi {
  functions: {
    get_count: {
      inputs: [];
      output: BN;
    };
    increment_count: {
      inputs: [amount: BigNumberish];
      output: BN;
    };
  };
  configurableConstants: { };
}
// CounterAbi__factory.ts
export const CounterAbi__factory = {
  abi: _abi,

  storageSlots: _storageSlots,

  createInterface() {
    return new Interface<CounterAbi>(_abi);
  },

  connect(
    id: string | AbstractAddress,
    accountOrProvider: Account | Provider
  ) {
    return new Contract<CounterAbi>(id, _abi, accountOrProvider)
  },

  async deployContract(
    bytecode: BytesLike,
    wallet: Account,
    options: DeployContractOptions = {}
  ) {
    const factory = new ContractFactory<CounterAbi>(bytecode, _abi, wallet);

    const contract = await factory.deployContract({
      storageSlots: _storageSlots,
      ...options,
    });

    return contract;
  }
}

This same interface can be generated for predicate and script abis:

export interface PredicateAbi {
  functions: {
    main: {
      inputs: [myPredicateInput: boolean];
      output: boolean;
    };
  configurableConstants: { };
}

export interface ScriptAbi {
  functions: {
    main: {
      inputs: [myScriptInput: BN];
      output: boolean;
    };
  configurableConstants: { };
}

This approach allows clean integration of logs (#2330) into the same interface as well later down the line, although that issue is not blocked by this one:

export interface SomeContractAbi {
  functions: { ... };
  configurableConstants: {...};
  logs: {...};
}
@nedsalk nedsalk added the chore Issue is a chore label Jul 3, 2024
@nedsalk nedsalk added this to the 1.0 caterpillar milestone Jul 3, 2024
@arboleya arboleya added the p1 label Jul 3, 2024
@arboleya arboleya removed this from the 1.0 caterpillar milestone Jul 19, 2024
@arboleya
Copy link
Member

arboleya commented Jul 20, 2024

@nedsalk What do you think about separating deployment stuff from the rest?

I've been questioning our weird suffixes for these files; maybe they could go.

Related:

import { Counter, deployCounter } from './typegend';

Counter.ts

export interface Counter {
  functions: {
    get_count: {
      inputs: [];
      output: BN;
    };
    increment_count: {
      inputs: [amount: BigNumberish];
      output: BN;
    };
  };
  configurableConstants: { };
}

export const counterAbi = "...";

export const counterStorageSlots = [];

export function createInterface() {
  return new Interface<Counter>(counterAbi);
}

export function create(
  id: string | AbstractAddress,
  accountOrProvider: Account | Provider
) {
  return new Contract<Counter>(id, counterAbi, accountOrProvider);
}

deployCounter.ts

// deployCounter.ts
import { Account, DeployContractOptions, DeployContractResult, ContractFactory } from 'fuels';
import { Counter, counterAbi, counterStorageSlots } from './Counter'

export const counterBytecode = '0x1af03..';

export async function deployCounter(
  wallet: Account,
  options: DeployContractOptions = {}
): Promise<DeployContractResult<Counter>> {
  const factory = new ContractFactory(counterBytecode, counterAbi, wallet);

  return factory.deployContract<Counter>({
    storageSlots: counterStorageSlots,
    ...options,
  });
}

@nedsalk
Copy link
Contributor Author

nedsalk commented Jul 20, 2024

I wholeheartedly agree with the direction.

I think we could give the users the full factory and have them do whatever they want with it:

// counter-factory.ts
import { Account, Provider, ContractFactory } from 'fuels';
import { Counter, abi } from './Counter'

export const counterBytecode = '0x1af03..';


// as a class
export class CounterFactory extends ContractFactory {
  constructor(accountOrProvider: Account | Provider | null) {
    super(counterBytecode, abi, accountOrProvider);
  }
}

// or as a factory function, which is nicer for importing,
// but which could give import problems to those who use multiple factories.
// although who uses multiple factories in their code anyways ... except us in tests :sweat_smile: 
export function factory(accountOrProvider: Account | Provider | null = null) {
  return new ContractFactory<Counter>(counterBytecode, abi, accountOrProvider);
}

This way we wouldn't have any logic in typegen outputs and it'd just be a type wrapper/implementer of the real classes.

@arboleya
Copy link
Member

arboleya commented Jul 20, 2024

Important

All pseudo code.

We can use prefixes/sufixes to avoid naming collisions.

  • counterBytecode
  • counterAbi
  • counterStorageSlots
  • Counter
  • createCounterInterface ()
  • createCounter ()
  • CounterFactory {}
  • factoryCounter ()
  • deployCounter ()
// Counter.ts
export interface Counter {
  functions: {
    get_count: {
      inputs: [];
      output: BN;
    };
    increment_count: {
      inputs: [amount: BigNumberish];
      output: BN;
    };
  };
  configurableConstants: { };
}

export const counterAbi = "...";

export const counterStorageSlots = [];

export function createCounterInterface() {
  return new Interface<Counter>(counterAbi);
}

export function createCounter(
  id: string | AbstractAddress,
  accountOrProvider: Account | Provider
) {
  return new Contract<Counter>(id, counterAbi, accountOrProvider);
}
// CounterFactory.ts
import { Counter, counterAbi, counterStorageSlots } from './Counter'

export const counterBytecode = '0x1af03..';

export class CounterFactory extends ContractFactory {
  constructor(accountOrProvider: Account | Provider | null) {
    super(counterBytecode, counterAbi, accountOrProvider);
  }
}

export async function factoryCounter(wallet: Account): DeployContractResult {
  return new CounterFactory(wallet);
}

export async function deployCounter(
  wallet: Account,
  options: DeployContractOptions = {}
): Promise<DeployContractResult> {
  return factoryCounter(wallet).deployContract({
    storageSlots: counterStorageSlots,
    ...options,
  });
}

@arboleya
Copy link
Member

It seems users should mainly be concerned with:

  1. createCounter(id, provider)
  2. deployCounter(wallet)

@arboleya arboleya added feat Issue is a feature and removed chore Issue is a chore labels Jul 20, 2024
@arboleya
Copy link
Member

This is how it should be used:

const deploy = await deployCounter(wallet);
const { contract } = await deploy.waitForResult();

Full snippet (with hypothetical unified API):

import type { TxParams } from 'fuels';
import { LOCAL_NETWORK_URL, fuels, bn } from 'fuels';

import { WALLET_PVT_KEY } from './env';
import { deployCounter } from './typegend';

const client = await fuels(LOCAL_NETWORK_URL);
const wallet = client.wallet(WALLET_PVT_KEY);

const deploy = await deployCounter(wallet);
const { contract } = await deploy.waitForResult();

const txParams: TxParams = {
  gasLimit: bn(69242),
  maxFee: bn(69242),
  tip: bn(100),
  maturity: 1,
  witnessLimit: bn(5000),
};

const { waitForResult } = await contract.functions
  .increment_count(15) //
  .txParams(txParams)
  .call();

const {
  value,
  transactionResult: { isStatusSuccess },
} = await waitForResult();

console.log({ value, isStatusSuccess });

Note

@danielbate This is also a port of the Transaction Parameters snippet.

@arboleya arboleya changed the title Integrate typegen outputs and programs via type generic parameters Prettify typegen API Jul 21, 2024
@arboleya arboleya self-assigned this Jul 23, 2024
@arboleya arboleya removed the p1 label Aug 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat Issue is a feature
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants