-
Notifications
You must be signed in to change notification settings - Fork 342
/
read-contract.ts
180 lines (167 loc) · 5.65 KB
/
read-contract.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import {
type Abi,
type AbiFunction,
type AbiParameter,
type AbiParametersToPrimitiveTypes,
type ExtractAbiFunctionNames,
parseAbiItem,
} from "abitype";
import { type TransactionRequest, decodeAbiParameters } from "viem";
import type { ThirdwebContract } from "../contract/contract.js";
import { isAbiFunction } from "./utils.js";
import type { PrepareTransactionOptions } from "./prepare-transaction.js";
import type {
BaseTransactionOptions,
ParamsOption,
ParseMethod,
} from "./types.js";
import { eth_call } from "../rpc/actions/eth_call.js";
import { getRpcClient } from "../rpc/rpc.js";
import { encodeAbiParameters } from "../utils/abi/encodeAbiParameters.js";
import {
type PreparedMethod,
prepareMethod,
} from "../utils/abi/prepare-method.js";
import type { Hex } from "../utils/encoding/hex.js";
export type ReadContractResult<outputs extends readonly AbiParameter[]> = // if the outputs are 0 length, return never, invalid case
outputs extends { length: 0 }
? never
: outputs extends { length: 1 }
? // if the outputs are 1 length, we'll always return the first element
AbiParametersToPrimitiveTypes<outputs>[0]
: // otherwise we'll return the array
AbiParametersToPrimitiveTypes<outputs>;
export type ReadContractOptions<
TAbi extends Abi = [],
TMethod extends
| AbiFunction
| string
| ((
contract: ThirdwebContract<TAbi>,
) => Promise<AbiFunction>) = TAbi extends { length: 0 }
? AbiFunction | string
: ExtractAbiFunctionNames<TAbi>,
TPreparedMethod extends PreparedMethod<
ParseMethod<TAbi, TMethod>
> = PreparedMethod<ParseMethod<TAbi, TMethod>>,
> = BaseTransactionOptions<
Omit<
TransactionRequest,
| "from"
| "to"
| "data"
| "value"
| "accessList"
| "gas"
| "gasPrice"
| "maxFeePerGas"
| "maxPriorityFeePerGas"
| "nonce"
> & {
method: TMethod | TPreparedMethod;
} & ParamsOption<TPreparedMethod[1]> &
Omit<PrepareTransactionOptions, "to" | "data" | "chain" | "client">,
TAbi
>;
/**
* Reads data from a smart contract.
* @param options - The transaction options.
* @returns A promise that resolves with the result of the read transaction.
* @transaction
* @example
* ```ts
* import { readContract } from "thirdweb";
* const result = await readContract({
* contract,
* method: "totalSupply",
* });
* ```
*/
export async function readContract<
const TAbi extends Abi,
const TMethod extends TAbi extends {
length: 0;
}
?
| AbiFunction
| `function ${string}`
| ((contract: ThirdwebContract<TAbi>) => Promise<AbiFunction>)
: ExtractAbiFunctionNames<TAbi>,
const TPreparedMethod extends PreparedMethod<
ParseMethod<TAbi, TMethod>
> = PreparedMethod<ParseMethod<TAbi, TMethod>>,
>(
options: ReadContractOptions<TAbi, TMethod, TPreparedMethod>,
): Promise<ReadContractResult<TPreparedMethod[2]>> {
type ParsedMethod_ = ParseMethod<TAbi, TMethod>;
type PreparedMethod_ = PreparedMethod<ParsedMethod_>;
const { contract, method, params } = options;
const resolvePreparedMethod = async () => {
if (Array.isArray(method)) {
return method as PreparedMethod_;
}
if (isAbiFunction(method)) {
return prepareMethod(method as ParsedMethod_) as PreparedMethod_;
}
if (typeof method === "function") {
return prepareMethod(
// @ts-expect-error - we're sure it's a function
(await method(contract)) as ParsedMethod_,
) as PreparedMethod_;
}
// if the method starts with the string `function ` we always will want to try to parse it
if (typeof method === "string" && method.startsWith("function ")) {
// @ts-expect-error - method *is* string in this case
const abiItem = parseAbiItem(method);
if (abiItem.type === "function") {
return prepareMethod(abiItem as ParsedMethod_) as PreparedMethod_;
}
throw new Error(`"method" passed is not of type "function"`);
}
// check if we have a "abi" on the contract
if (contract.abi && contract.abi?.length > 0) {
// extract the abiFunction from it
const abiFunction = contract.abi?.find(
(item) => item.type === "function" && item.name === method,
);
// if we were able to find it -> return it
if (abiFunction) {
return prepareMethod(abiFunction as ParsedMethod_) as PreparedMethod_;
}
}
throw new Error(`Could not resolve method "${method}".`);
};
// resolve in parallel
const [resolvedPreparedMethod, resolvedParams] = await Promise.all([
resolvePreparedMethod(),
typeof params === "function" ? params() : params,
]);
let encodedData: Hex;
// if we have no inputs, we know it's just the signature
if (resolvedPreparedMethod[1].length === 0) {
encodedData = resolvedPreparedMethod[0];
} else {
// we do a "manual" concat here to avoid the overhead of the "concatHex" function
// we can do this because we know the specific formats of the values
encodedData = (resolvedPreparedMethod[0] +
encodeAbiParameters(
resolvedPreparedMethod[1],
// @ts-expect-error - TODO: fix this type issue
resolvedParams,
).slice(2)) as `${(typeof resolvedPreparedMethod)[0]}${string}`;
}
const rpcRequest = getRpcClient({
chain: contract.chain,
client: contract.client,
});
const result = await eth_call(rpcRequest, {
data: encodedData,
to: contract.address,
});
// use the prepared method to decode the result
const decoded = decodeAbiParameters(resolvedPreparedMethod[2], result);
if (Array.isArray(decoded) && decoded.length === 1) {
return decoded[0];
}
return decoded as ReadContractResult<TPreparedMethod[2]>;
}