Closed nedsalk closed 1 month ago
@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,
});
}
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.
[!IMPORTANT] All pseudo code.
We can use prefixes/sufixes to avoid naming collisions.
counter
Bytecodecounter
Abicounter
StorageSlotsCounter
Counter
Interface ()Counter
()Counter
Factory {}Counter
()Counter
()// 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,
});
}
It seems users should mainly be concerned with:
Counter
(id, provider)Counter
(wallet)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.
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:The generated outputs are
CounterAbi.d.ts
andCounterAbi__factory.ts
, respectively:The idea is for
CounterAbi.d.ts
to contain an appropriate type representation of the ABI which would be passed toInterface<TAbi>
andContract<TAbi>
classes, removing the need for theas unknown as CounterAbi
casts. Below is an idea of the updatedCounterAbi.d.ts
andCounterAbi__factory.ts
contents, respectively:This same interface can be generated for predicate and script abis:
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: