Skip to content

Commit 56f67a4

Browse files
authored
Merge pull request #91 from FuelLabs/ag/feat/useContractRead
feat: create useContractRead hook
2 parents 497fb06 + 255478b commit 56f67a4

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed

.changeset/brave-kiwis-punch.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@fuels/react': minor
3+
---
4+
5+
Created useContractRead hook to read contract data from a contract instance or from a contract id.
6+
If provided Abi is declared with `as const`, hook will dynamically infer possible method names, as well as the arguments quantity and types for a selected method.

packages/react/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './useAssets';
66
export * from './useBalance';
77
export * from './useChain';
88
export * from './useConnect';
9+
export * from './useContractRead';
910
export * from './useConnectors';
1011
export * from './useDisconnect';
1112
export * from './useIsConnected';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { Contract, type Address, type JsonAbi, type Provider } from 'fuels';
2+
import type { FunctionNames, InputsForFunctionName } from 'src/types';
3+
4+
import { useNamedQuery } from '../core/useNamedQuery';
5+
import { QUERY_KEYS } from '../utils';
6+
7+
type ContractData<TAbi extends JsonAbi> = {
8+
address: Address;
9+
abi: TAbi;
10+
provider: Provider;
11+
};
12+
13+
type ContractReadProps<
14+
TAbi extends JsonAbi,
15+
TFunctionName extends FunctionNames<TAbi>,
16+
> = {
17+
contract: Contract | ContractData<TAbi>;
18+
functionName: TFunctionName;
19+
args: InputsForFunctionName<TAbi, TFunctionName>;
20+
};
21+
22+
export const useContractRead = <
23+
TAbi extends JsonAbi,
24+
TFunctionName extends FunctionNames<TAbi>,
25+
>({
26+
functionName,
27+
args,
28+
contract: _contract,
29+
}: ContractReadProps<TAbi, TFunctionName>) => {
30+
const isContractData =
31+
_contract && 'abi' in _contract && 'address' in _contract;
32+
const { abi, address, provider } = (_contract as ContractData<TAbi>) ?? {};
33+
const chainId = _contract?.provider?.getChainId();
34+
35+
return useNamedQuery('contractRead', {
36+
queryKey: QUERY_KEYS.contract(
37+
isContractData ? address?.toString() : _contract?.id?.toString(),
38+
chainId,
39+
args?.toString(),
40+
),
41+
queryFn: async () => {
42+
const isValid = isContractData
43+
? !!abi && !!address && !!provider
44+
: !!_contract && 'provider' in _contract;
45+
46+
if (!isValid) {
47+
throw new Error(
48+
'Valind input `contract` is required to read the contract',
49+
);
50+
}
51+
const contract = isContractData
52+
? new Contract(address, abi, provider)
53+
: (_contract as Contract);
54+
55+
if (!contract?.functions?.[functionName]) {
56+
throw new Error(`Function ${functionName || ''} not found on contract`);
57+
}
58+
59+
const wouldWriteToStorage =
60+
contract.functions[functionName].isReadOnly?.() !== undefined
61+
? !contract.functions[functionName].isReadOnly()
62+
: Object.values(contract.interface.functions)
63+
.find((f) => f.name === functionName)
64+
?.attributes?.find((attr) => attr.name === 'storage')
65+
?.arguments?.includes('write');
66+
67+
if (wouldWriteToStorage) {
68+
throw new Error(
69+
'Methods that write to storage should not be called with useContractRead',
70+
);
71+
}
72+
73+
return args !== undefined
74+
? contract.functions[functionName](args)
75+
: contract.functions[functionName]();
76+
},
77+
});
78+
};

packages/react/src/types.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { AbstractAddress, JsonAbi } from 'fuels';
2+
13
export type Connector = {
24
name: string;
35
image:
@@ -16,3 +18,102 @@ export type Connector = {
1618
};
1719

1820
export type ConnectorList = Array<Connector>;
21+
22+
type JsonAbiArgument = JsonAbi['functions'][number]['inputs'][number];
23+
type JsonAbiType = JsonAbi['types'][number];
24+
25+
type GetAbiTypeById<TAbi extends JsonAbi, Id extends number> = Extract<
26+
TAbi['types'][number],
27+
{ typeId: Id }
28+
>;
29+
30+
type ExtractTypeFromString<
31+
TAbi extends JsonAbi,
32+
T extends string,
33+
> = T extends `str[${infer _Length}]`
34+
? string
35+
: T extends `[${infer Item}; ${infer _Length}]`
36+
? ResolveAbiType<TAbi, GetAbiTypeById<TAbi, ExtractTypeIdFromString<Item>>>[]
37+
: T extends `struct ${infer Name}`
38+
? { [K in Name]: unknown }
39+
: T extends `enum ${infer Name}`
40+
? { [K in Name]: unknown }
41+
: T extends `(${infer Items})`
42+
? [ExtractTypeFromString<TAbi, Items>]
43+
: T extends `generic ${infer _Name}`
44+
? unknown
45+
: unknown;
46+
47+
type ResolveEnumType<
48+
TAbi extends JsonAbi,
49+
T extends JsonAbiType,
50+
> = T['components'] extends { type: infer TypeId }
51+
? TypeId extends number
52+
? ResolveAbiType<TAbi, GetAbiTypeById<TAbi, TypeId>>
53+
: unknown
54+
: unknown;
55+
56+
type ResolveAbiType<
57+
TAbi extends JsonAbi,
58+
T extends JsonAbiType,
59+
> = T['type'] extends 'struct'
60+
? T['components'] extends null
61+
? unknown
62+
: {
63+
[K in NonNullable<
64+
T['components']
65+
>[number] as K['name']]: ResolveAbiType<
66+
TAbi,
67+
GetAbiTypeById<TAbi, K['type']>
68+
>;
69+
}
70+
: T['type'] extends 'enum'
71+
? ResolveEnumType<TAbi, T>
72+
: T['type'] extends 'u64' | 'u256' | 'uint256'
73+
? bigint
74+
: T['type'] extends 'u32' | 'u16' | 'u8'
75+
? number
76+
: T['type'] extends 'bool'
77+
? boolean
78+
: T['type'] extends 'b256' | `str[${string}]`
79+
? string
80+
: T['type'] extends 'struct Address' | 'struct ContractId'
81+
? AbstractAddress
82+
: T['type'] extends '()'
83+
? unknown
84+
: T['type'] extends `array<${infer U}>`
85+
? ResolveAbiType<TAbi, GetAbiTypeById<TAbi, ExtractTypeIdFromString<U>>>[]
86+
: T['type'] extends `${string} ${string}`
87+
? ExtractTypeFromString<TAbi, T['type']>
88+
: unknown;
89+
90+
type ExtractTypeIdFromString<S extends string> = S extends `${infer Id}`
91+
? NumberStringToNumber<Id>
92+
: never;
93+
type NumberStringToNumber<S extends string> = S extends `${infer D extends
94+
number}`
95+
? D
96+
: never;
97+
98+
type ResolveInputs<
99+
TAbi extends JsonAbi,
100+
Inputs extends ReadonlyArray<JsonAbiArgument>,
101+
> = Inputs['length'] extends 1
102+
? ResolveAbiType<TAbi, GetAbiTypeById<TAbi, Inputs[0]['type']>>
103+
: {
104+
[K in keyof Inputs]: ResolveAbiType<
105+
TAbi,
106+
GetAbiTypeById<TAbi, Inputs[K]['type']>
107+
>;
108+
};
109+
110+
export type InputsForFunctionName<
111+
TAbi extends JsonAbi,
112+
N extends FunctionNames<TAbi>,
113+
> = ResolveInputs<
114+
TAbi,
115+
Extract<TAbi['functions'][number], { name: N }>['inputs']
116+
>;
117+
118+
export type FunctionNames<TAbi extends JsonAbi> =
119+
TAbi['functions'][number]['name'];

packages/react/src/utils/queryKeys.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ export const QUERY_KEYS = {
1212
assets: (): QueryKey => {
1313
return QUERY_KEYS.base.concat('assets');
1414
},
15+
contract: (
16+
address: string,
17+
chainId: number | undefined,
18+
args: string | undefined,
19+
): QueryKey => {
20+
const queryKey = QUERY_KEYS.base.concat('contract').concat(address);
21+
if (typeof args !== 'undefined') queryKey.push(args);
22+
if (typeof chainId !== 'undefined') queryKey.push(chainId);
23+
return queryKey;
24+
},
1525
chain: (): QueryKey => {
1626
return QUERY_KEYS.base.concat('chain');
1727
},

0 commit comments

Comments
 (0)