Skip to content

Commit 6ef32df

Browse files
committed
added a async handler for the batch module
1 parent 2056253 commit 6ef32df

7 files changed

Lines changed: 103 additions & 103 deletions

File tree

src/core/batch/batch.test.ts

Lines changed: 43 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -130,126 +130,111 @@ describe('ComposableBatch — add, length, clear, toCalldata', () => {
130130
expect(batch.length).toBe(0);
131131
});
132132

133-
it('add(single call) increments length by 1', async () => {
133+
it('add(single call) increments length by 1', () => {
134134
const batch = createComposableBatch(publicClient, ACCOUNT);
135-
const call = await batch
136-
.contract(USDC, erc20Abi)
137-
.write({ functionName: 'transfer', args: [WETH, 1n] });
138-
batch.add(call);
135+
batch.add(batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }));
139136
expect(batch.length).toBe(1);
140137
});
141138

142-
it('add(array of calls) increments length by the array size', async () => {
139+
it('add(array of calls) increments length by the array size', () => {
143140
const batch = createComposableBatch(publicClient, ACCOUNT);
144141
const token = batch.contract(USDC, erc20Abi);
145-
const calls = await Promise.all([
142+
batch.add([
146143
token.write({ functionName: 'transfer', args: [WETH, 1n] }),
147144
token.write({ functionName: 'approve', args: [WETH, 1n] }),
148145
]);
149-
batch.add(calls);
150146
expect(batch.length).toBe(2);
151147
});
152148

153-
it('add() can be called multiple times and accumulates calls', async () => {
149+
it('add() can be called multiple times and accumulates calls', () => {
154150
const batch = createComposableBatch(publicClient, ACCOUNT);
155151
const token = batch.contract(USDC, erc20Abi);
156-
batch.add(await token.write({ functionName: 'transfer', args: [WETH, 1n] }));
157-
batch.add(await token.write({ functionName: 'approve', args: [WETH, 1n] }));
152+
batch.add(token.write({ functionName: 'transfer', args: [WETH, 1n] }));
153+
batch.add(token.write({ functionName: 'approve', args: [WETH, 1n] }));
158154
expect(batch.length).toBe(2);
159155
});
160156

161-
it('clear() resets length to 0', async () => {
157+
it('clear() resets length to 0', () => {
162158
const batch = createComposableBatch(publicClient, ACCOUNT);
163-
const call = await batch
164-
.contract(USDC, erc20Abi)
165-
.write({ functionName: 'transfer', args: [WETH, 1n] });
166-
batch.add(call);
159+
batch.add(batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }));
167160
batch.clear();
168161
expect(batch.length).toBe(0);
169162
});
170163

171164
it('toCalldata() returns a hex string', async () => {
172165
const batch = createComposableBatch(publicClient, ACCOUNT);
173-
const call = await batch
174-
.contract(USDC, erc20Abi)
175-
.write({ functionName: 'transfer', args: [WETH, 1n] });
176-
batch.add(call);
177-
const calldata = await batch.toCalldata();
178-
expect(calldata).toMatch(/^0x[0-9a-fA-F]+$/);
166+
batch.add(batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }));
167+
168+
expect(await batch.toCalldata()).toMatch(/^0x[0-9a-fA-F]+$/);
179169
});
180170

181171
it('toCalldata() with multiple calls returns a hex string', async () => {
182172
const batch = createComposableBatch(publicClient, ACCOUNT);
183173
const token = batch.contract(USDC, erc20Abi);
184-
batch.add(await token.write({ functionName: 'transfer', args: [WETH, 1n] }));
185-
batch.add(await token.write({ functionName: 'approve', args: [WETH, 1_000_000n] }));
186-
const calldata = await batch.toCalldata();
187-
expect(calldata).toMatch(/^0x[0-9a-fA-F]+$/);
174+
batch.add(token.write({ functionName: 'transfer', args: [WETH, 1n] }));
175+
batch.add(token.write({ functionName: 'approve', args: [WETH, 1_000_000n] }));
176+
177+
expect(await batch.toCalldata()).toMatch(/^0x[0-9a-fA-F]+$/);
188178
});
189179

190180
it('toCalldata() produces different output for different calls', async () => {
191181
const batchA = createComposableBatch(publicClient, ACCOUNT);
192182
const batchB = createComposableBatch(publicClient, ACCOUNT);
193-
const [transferCall, approveCall] = await Promise.all([
183+
batchA.add(
194184
batchA.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }),
185+
);
186+
batchB.add(
195187
batchB.contract(USDC, erc20Abi).write({ functionName: 'approve', args: [WETH, 1n] }),
196-
]);
197-
batchA.add(transferCall);
198-
batchB.add(approveCall);
188+
);
199189
const [a, b] = await Promise.all([batchA.toCalldata(), batchB.toCalldata()]);
200190
expect(a).not.toBe(b);
201191
});
202192
});
203193

204194
// ---------------------------------------------------------------------------
205-
// ComposableBatch — calls getter
195+
// ComposableBatch — toCalls
206196
// ---------------------------------------------------------------------------
207197

208-
describe('ComposableBatch — calls getter', () => {
209-
it('calls is empty on a fresh batch', () => {
198+
describe('ComposableBatch — toCalls', () => {
199+
it('toCalls() returns empty array on a fresh batch', async () => {
210200
const batch = createComposableBatch(publicClient, ACCOUNT);
211-
expect(batch.calls).toHaveLength(0);
201+
expect(await batch.toCalls()).toHaveLength(0);
212202
});
213203

214-
it('calls reflects added single call', async () => {
204+
it('toCalls() reflects a single added call', async () => {
215205
const batch = createComposableBatch(publicClient, ACCOUNT);
216-
const call = await batch
206+
const writePromise = batch
217207
.contract(USDC, erc20Abi)
218208
.write({ functionName: 'transfer', args: [WETH, 1n] });
219-
batch.add(call);
220-
expect(batch.calls).toHaveLength(1);
221-
expect(batch.calls[0].functionSig).toBe(call.functionSig);
209+
batch.add(writePromise);
210+
const calls = await batch.toCalls();
211+
expect(calls).toHaveLength(1);
212+
expect(calls[0].functionSig).toBe((await writePromise).functionSig);
222213
});
223214

224-
it('calls reflects added array of calls', async () => {
215+
it('toCalls() reflects an added array of calls', async () => {
225216
const batch = createComposableBatch(publicClient, ACCOUNT);
226217
const token = batch.contract(USDC, erc20Abi);
227-
batch.add(
228-
await Promise.all([
229-
token.write({ functionName: 'transfer', args: [WETH, 1n] }),
230-
token.write({ functionName: 'approve', args: [WETH, 1n] }),
231-
]),
232-
);
233-
expect(batch.calls).toHaveLength(2);
218+
batch.add([
219+
token.write({ functionName: 'transfer', args: [WETH, 1n] }),
220+
token.write({ functionName: 'approve', args: [WETH, 1n] }),
221+
]);
222+
expect(await batch.toCalls()).toHaveLength(2);
234223
});
235224

236-
it('calls is empty after clear()', async () => {
225+
it('toCalls() returns empty array after clear()', async () => {
237226
const batch = createComposableBatch(publicClient, ACCOUNT);
238-
batch.add(
239-
await batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }),
240-
);
227+
batch.add(batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }));
241228
batch.clear();
242-
expect(batch.calls).toHaveLength(0);
229+
expect(await batch.toCalls()).toHaveLength(0);
243230
});
244231

245-
it('calls returns a copy — mutating it does not affect the batch', async () => {
232+
it('toCalls() returns a copy — mutating it does not affect the batch', async () => {
246233
const batch = createComposableBatch(publicClient, ACCOUNT);
247-
batch.add(
248-
await batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }),
249-
);
250-
const snapshot = batch.calls;
234+
batch.add(batch.contract(USDC, erc20Abi).write({ functionName: 'transfer', args: [WETH, 1n] }));
235+
const snapshot = await batch.toCalls();
251236
snapshot.pop();
252-
expect(batch.calls).toHaveLength(1);
237+
expect(await batch.toCalls()).toHaveLength(1);
253238
});
254239
});
255240

src/core/batch/batch.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,17 @@ export function createComposableBatch<
1212
publicClient: PublicClient<TTransport, TChain>,
1313
accountAddress: Address,
1414
): ComposableBatchInstance<TTransport, TChain> {
15-
const calls: ComposableCall[] = [];
15+
const pendingCalls: (ComposableCall | Promise<ComposableCall>)[] = [];
16+
17+
async function build(): Promise<ComposableCall[]> {
18+
return Promise.all(pendingCalls);
19+
}
1620

1721
return {
1822
publicClient,
1923
accountAddress,
2024
get length() {
21-
return calls.length;
25+
return pendingCalls.length;
2226
},
2327
erc20Token(tokenAddress) {
2428
return createERC20Token(publicClient, tokenAddress, accountAddress);
@@ -32,21 +36,21 @@ export function createComposableBatch<
3236
storage() {
3337
return createStorage(publicClient, accountAddress);
3438
},
35-
get calls() {
36-
return [...calls];
37-
},
3839
add(call) {
3940
if (Array.isArray(call)) {
40-
calls.push(...call);
41+
pendingCalls.push(...call);
4142
} else {
42-
calls.push(call);
43+
pendingCalls.push(call);
4344
}
4445
},
4546
clear() {
46-
calls.length = 0;
47+
pendingCalls.length = 0;
48+
},
49+
async toCalls() {
50+
return build();
4751
},
48-
toCalldata() {
49-
return encodeExecuteComposable(calls);
52+
async toCalldata() {
53+
return encodeExecuteComposable(await build());
5054
},
5155
};
5256
}

src/core/batch/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ export interface ComposableBatchInstance<
1818
abi: TAbi,
1919
): ContractInstance<TAbi>;
2020
storage(): StorageInstance;
21-
readonly calls: ComposableCall[];
22-
add(call: ComposableCall | ComposableCall[]): void;
21+
add(
22+
calls: ComposableCall | Promise<ComposableCall> | (ComposableCall | Promise<ComposableCall>)[],
23+
): void;
2324
clear(): void;
25+
toCalls(): Promise<ComposableCall[]>;
2426
toCalldata(): Promise<Hex>;
2527
}

src/core/encoding/encoding.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ export const prepareComposableOutputCalldataParams = async <
559559
* @param call - The calls to encode
560560
* @returns The encoded composable compatible call
561561
*/
562-
export const encodeExecuteComposable = async (calls: ComposableCall[]): Promise<Hex> => {
562+
export const encodeExecuteComposable = (calls: ComposableCall[]): Hex => {
563563
const composableCalls = calls.map((call) => {
564564
return {
565565
functionSig: call.functionSig,

src/test/integration/batch-execution.test.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
3636
constraints: [{ gte: FUND_AMOUNT }],
3737
}),
3838
// Sweep: transfer the SCA's full runtime balance to the EOA
39-
await usdc.write({
39+
usdc.write({
4040
functionName: 'transfer',
4141
args: [_account.address, usdc.runtimeBalance()],
4242
}),
@@ -51,15 +51,16 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
5151
expect(Number(scaBalanceBefore)).to.greaterThanOrEqual(Number(FUND_AMOUNT));
5252

5353
// 5. Get a quote for the composable instruction, then sign and submit it via MEE
54+
5455
const quote = await meeClient.getQuote({
55-
instructions: [{ calls: batch.calls, chainId: baseSepolia.id, isComposable: true }],
56+
instructions: [{ calls: await batch.toCalls(), chainId: baseSepolia.id, isComposable: true }],
5657
simulation: { simulate: true },
5758
feeToken: { address: USDC, chainId: baseSepolia.id },
5859
});
5960

6061
// 6. Execute the signed quote and wait for the supertransaction to settle
6162
const { hash } = await meeClient.executeQuote({ quote });
62-
await meeClient.waitForSupertransactionReceipt({ hash });
63+
await meeClient.waitForSupertransactionReceipt({ hash, mode: 'fast-block' });
6364

6465
// 7. Assert SCA balance has been swept to zero (minus fees)
6566
const scaBalanceAfter = await usdc.read({ functionName: 'balanceOf', args: [scaAddress] });
@@ -82,7 +83,7 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
8283
constraints: [{ gte: 2n * FUND_AMOUNT }],
8384
}),
8485
// Sweep: would transfer runtime balance to EOA (never reached due to revert)
85-
await usdc.write({
86+
usdc.write({
8687
functionName: 'transfer',
8788
args: [_account.address, usdc.runtimeBalance()],
8889
}),
@@ -91,9 +92,12 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
9192
expect(batch.length).toBe(2);
9293

9394
// 3. Submit quote — expect simulation to revert because pre-check constraint is not satisfied
95+
9496
await expect(
9597
meeClient.getQuote({
96-
instructions: [{ calls: batch.calls, chainId: baseSepolia.id, isComposable: true }],
98+
instructions: [
99+
{ calls: await batch.toCalls(), chainId: baseSepolia.id, isComposable: true },
100+
],
97101
simulation: { simulate: true },
98102
feeToken: { address: USDC, chainId: baseSepolia.id },
99103
}),
@@ -112,7 +116,7 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
112116

113117
batch.add([
114118
// Sweep: transfer runtime balance from SCA to EOA
115-
await usdc.write({
119+
usdc.write({
116120
functionName: 'transfer',
117121
args: [_account.address, usdc.runtimeBalance()],
118122
}),
@@ -127,9 +131,12 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
127131
expect(batch.length).toBe(2);
128132

129133
// 3. Submit quote — expect simulation to revert because post-check constraint is not satisfied
134+
130135
await expect(
131136
meeClient.getQuote({
132-
instructions: [{ calls: batch.calls, chainId: baseSepolia.id, isComposable: true }],
137+
instructions: [
138+
{ calls: await batch.toCalls(), chainId: baseSepolia.id, isComposable: true },
139+
],
133140
simulation: { simulate: true },
134141
feeToken: { address: USDC, chainId: baseSepolia.id },
135142
}),
@@ -156,16 +163,16 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
156163

157164
batch.add([
158165
// Step A: write FUND_AMOUNT/2 into the shared namespace storage slot
159-
await storage.write({ value: storageValue, storageKey }),
166+
storage.write({ value: storageValue, storageKey }),
160167
// Step B: assert the stored value equals what was just written before proceeding
161-
await storage.check({ storageKey, constraints: [{ eq: storageValue }] }),
168+
storage.check({ storageKey, constraints: [{ eq: storageValue }] }),
162169
// Step C: transfer the runtime-resolved storage value (FUND_AMOUNT/2) from SCA to EOA
163-
await usdc.write({
170+
usdc.write({
164171
functionName: 'transfer',
165172
args: [_account.address, await storage.runtimeValue({ storageKey })],
166173
}),
167174
// Step D: sweep any remaining SCA balance (the other half) to the EOA
168-
await usdc.write({
175+
usdc.write({
169176
functionName: 'transfer',
170177
args: [_account.address, usdc.runtimeBalance()],
171178
}),
@@ -174,15 +181,16 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
174181
expect(batch.length).toBe(4);
175182

176183
// 4. Get a quote for the composable instruction, then sign and submit it via MEE
184+
177185
const quote = await meeClient.getQuote({
178-
instructions: [{ calls: batch.calls, chainId: baseSepolia.id, isComposable: true }],
186+
instructions: [{ calls: await batch.toCalls(), chainId: baseSepolia.id, isComposable: true }],
179187
simulation: { simulate: true },
180188
feeToken: { address: USDC, chainId: baseSepolia.id },
181189
});
182190

183191
// 5. Execute the signed quote and wait for the supertransaction to settle
184192
const { hash } = await meeClient.executeQuote({ quote });
185-
await meeClient.waitForSupertransactionReceipt({ hash });
193+
await meeClient.waitForSupertransactionReceipt({ hash, mode: 'fast-block' });
186194

187195
// 6. Assert the on-chain storage slot holds the value that was written in step A
188196
expect(await storage.read({ storageKey })).to.eq(toBytes32(storageValue));

0 commit comments

Comments
 (0)