Skip to content

Commit f117840

Browse files
committed
refactor: split index.ts into types, encoding, signing, and client modules
1 parent 59a713b commit f117840

5 files changed

Lines changed: 750 additions & 646 deletions

File tree

clients/src/client.ts

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
import {
2+
type AccessList,
3+
type Address,
4+
type Client,
5+
type Hex,
6+
hexToBigInt,
7+
isHex,
8+
} from 'viem';
9+
10+
import type {
11+
Call,
12+
EvNodeTransaction,
13+
EvNodeSignedTransaction,
14+
SponsorableIntent,
15+
HashSigner,
16+
EvnodeClientOptions,
17+
EvnodeSendArgs,
18+
EvnodeSendArgsWithExecutor,
19+
EvnodeSponsorArgs,
20+
} from './types.js';
21+
22+
import {
23+
encodeSignedTransaction,
24+
decodeEvNodeTransaction,
25+
estimateIntrinsicGas,
26+
validateEvNodeTx,
27+
} from './encoding.js';
28+
29+
import { signAsExecutor, signAsSponsor } from './signing.js';
30+
31+
export function evnodeActions(client: Client) {
32+
return {
33+
async sendEvNodeTransaction(args: EvnodeSendArgsWithExecutor): Promise<Hex> {
34+
const base = await resolveBaseFields(client, args.executor.address, {
35+
chainId: args.chainId,
36+
nonce: args.nonce,
37+
maxFeePerGas: args.maxFeePerGas,
38+
maxPriorityFeePerGas: args.maxPriorityFeePerGas,
39+
gasLimit: args.gasLimit,
40+
accessList: args.accessList,
41+
}, args.calls);
42+
43+
const tx: EvNodeTransaction = {
44+
...base,
45+
calls: args.calls,
46+
feePayerSignature: undefined,
47+
};
48+
49+
validateEvNodeTx(tx);
50+
51+
const executorSignature = await signAsExecutor(tx, args.executor);
52+
const signedTx: EvNodeSignedTransaction = {
53+
transaction: tx,
54+
executorSignature,
55+
};
56+
57+
const serialized = encodeSignedTransaction(signedTx);
58+
return client.request({
59+
method: 'eth_sendRawTransaction',
60+
params: [serialized],
61+
}) as Promise<Hex>;
62+
},
63+
64+
async createSponsorableIntent(args: EvnodeSendArgsWithExecutor): Promise<SponsorableIntent> {
65+
const base = await resolveBaseFields(client, args.executor.address, {
66+
chainId: args.chainId,
67+
nonce: args.nonce,
68+
maxFeePerGas: args.maxFeePerGas,
69+
maxPriorityFeePerGas: args.maxPriorityFeePerGas,
70+
gasLimit: args.gasLimit,
71+
accessList: args.accessList,
72+
}, args.calls);
73+
74+
const tx: EvNodeTransaction = {
75+
...base,
76+
calls: args.calls,
77+
feePayerSignature: undefined,
78+
};
79+
80+
validateEvNodeTx(tx);
81+
82+
const executorSignature = await signAsExecutor(tx, args.executor);
83+
84+
return {
85+
tx,
86+
executorSignature,
87+
executorAddress: args.executor.address,
88+
};
89+
},
90+
91+
async sponsorIntent(args: {
92+
intent: SponsorableIntent;
93+
sponsor: HashSigner;
94+
}): Promise<EvNodeSignedTransaction> {
95+
const sponsorSignature = await signAsSponsor(
96+
args.intent.tx,
97+
args.intent.executorAddress,
98+
args.sponsor,
99+
);
100+
101+
return {
102+
transaction: {
103+
...args.intent.tx,
104+
feePayerSignature: sponsorSignature,
105+
},
106+
executorSignature: args.intent.executorSignature,
107+
};
108+
},
109+
110+
serializeEvNodeTransaction(signedTx: EvNodeSignedTransaction): Hex {
111+
return encodeSignedTransaction(signedTx);
112+
},
113+
114+
deserializeEvNodeTransaction(encoded: Hex): EvNodeSignedTransaction {
115+
return decodeEvNodeTransaction(encoded);
116+
},
117+
};
118+
}
119+
120+
export function createEvnodeClient(options: EvnodeClientOptions) {
121+
const actions = evnodeActions(options.client);
122+
let defaultExecutor = options.executor;
123+
let defaultSponsor = options.sponsor;
124+
125+
const requireExecutor = (executor?: HashSigner) => {
126+
const resolved = executor ?? defaultExecutor;
127+
if (!resolved) throw new Error('Executor signer is required');
128+
return resolved;
129+
};
130+
131+
const requireSponsor = (sponsor?: HashSigner) => {
132+
const resolved = sponsor ?? defaultSponsor;
133+
if (!resolved) throw new Error('Sponsor signer is required');
134+
return resolved;
135+
};
136+
137+
return {
138+
client: options.client,
139+
actions,
140+
setDefaultExecutor(executor: HashSigner) {
141+
defaultExecutor = executor;
142+
},
143+
setDefaultSponsor(sponsor: HashSigner) {
144+
defaultSponsor = sponsor;
145+
},
146+
send(args: EvnodeSendArgs): Promise<Hex> {
147+
return actions.sendEvNodeTransaction({
148+
...args,
149+
executor: requireExecutor(args.executor),
150+
});
151+
},
152+
createIntent(args: EvnodeSendArgs): Promise<SponsorableIntent> {
153+
return actions.createSponsorableIntent({
154+
...args,
155+
executor: requireExecutor(args.executor),
156+
});
157+
},
158+
sponsorIntent(args: EvnodeSponsorArgs): Promise<EvNodeSignedTransaction> {
159+
return actions.sponsorIntent({
160+
intent: args.intent,
161+
sponsor: requireSponsor(args.sponsor),
162+
});
163+
},
164+
async sponsorAndSend(args: EvnodeSponsorArgs): Promise<Hex> {
165+
const signed = await actions.sponsorIntent({
166+
intent: args.intent,
167+
sponsor: requireSponsor(args.sponsor),
168+
});
169+
const serialized = actions.serializeEvNodeTransaction(signed);
170+
return options.client.request({
171+
method: 'eth_sendRawTransaction',
172+
params: [serialized],
173+
}) as Promise<Hex>;
174+
},
175+
serialize: actions.serializeEvNodeTransaction,
176+
deserialize: actions.deserializeEvNodeTransaction,
177+
};
178+
}
179+
180+
// --- internal helpers ---
181+
182+
async function resolveBaseFields(
183+
client: Client,
184+
address: Address,
185+
overrides: {
186+
chainId?: bigint;
187+
nonce?: bigint;
188+
maxFeePerGas?: bigint;
189+
maxPriorityFeePerGas?: bigint;
190+
gasLimit?: bigint;
191+
accessList?: AccessList;
192+
},
193+
calls: Call[],
194+
): Promise<Omit<EvNodeTransaction, 'calls' | 'feePayerSignature'>> {
195+
const chainId = overrides.chainId ?? (await fetchChainId(client));
196+
const nonce = overrides.nonce ?? (await fetchNonce(client, address));
197+
const maxPriorityFeePerGas =
198+
overrides.maxPriorityFeePerGas ?? (await fetchMaxPriorityFee(client));
199+
const maxFeePerGas = overrides.maxFeePerGas ?? (await fetchGasPrice(client));
200+
const gasLimit = overrides.gasLimit ?? estimateIntrinsicGas(calls);
201+
const accessList = overrides.accessList ?? [];
202+
203+
return {
204+
chainId,
205+
nonce,
206+
maxPriorityFeePerGas,
207+
maxFeePerGas,
208+
gasLimit,
209+
accessList,
210+
};
211+
}
212+
213+
async function fetchChainId(client: Client): Promise<bigint> {
214+
const result = await client.request({ method: 'eth_chainId' });
215+
if (!isHex(result)) throw new Error('eth_chainId returned non-hex');
216+
return hexToBigIntSafe(result);
217+
}
218+
219+
async function fetchNonce(client: Client, address: Address): Promise<bigint> {
220+
const result = await client.request({
221+
method: 'eth_getTransactionCount',
222+
params: [address, 'pending'],
223+
});
224+
if (!isHex(result)) throw new Error('eth_getTransactionCount returned non-hex');
225+
return hexToBigIntSafe(result);
226+
}
227+
228+
async function fetchMaxPriorityFee(client: Client): Promise<bigint> {
229+
try {
230+
const result = await client.request({ method: 'eth_maxPriorityFeePerGas' });
231+
if (!isHex(result)) throw new Error('eth_maxPriorityFeePerGas returned non-hex');
232+
return hexToBigIntSafe(result);
233+
} catch {
234+
return 0n;
235+
}
236+
}
237+
238+
async function fetchGasPrice(client: Client): Promise<bigint> {
239+
const result = await client.request({ method: 'eth_gasPrice' });
240+
if (!isHex(result)) throw new Error('eth_gasPrice returned non-hex');
241+
return hexToBigIntSafe(result);
242+
}
243+
244+
function hexToBigIntSafe(value: unknown): bigint {
245+
if (typeof value !== 'string' || !value.startsWith('0x')) {
246+
throw new Error('Invalid hex value');
247+
}
248+
return value === '0x' ? 0n : hexToBigInt(value as `0x${string}`);
249+
}

0 commit comments

Comments
 (0)