Skip to content

Commit 88a23d7

Browse files
committed
feat: added a integration test for storage flows
1 parent cef524f commit 88a23d7

5 files changed

Lines changed: 90 additions & 1 deletion

File tree

src/core/batch/batch.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Address, Chain, PublicClient, Transport } from 'viem';
22
import { contract } from '../contract';
33
import { type ComposableCall, encodeExecuteComposable } from '../encoding';
4+
import { storage } from '../storage';
45
import { ERC20Token, NativeToken } from '../token';
56
import type { ComposableBatchInstance } from './types';
67

@@ -28,6 +29,9 @@ export function ComposableBatch<
2829
contract(address, abi) {
2930
return contract(publicClient, address, abi);
3031
},
32+
storage() {
33+
return storage(publicClient, accountAddress);
34+
},
3135
get calls() {
3236
return [...calls];
3337
},

src/core/batch/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Address, Chain, Hex, PublicClient, Transport } from 'viem';
22
import type { ContractInstance } from '../contract';
33
import type { ComposableCall } from '../encoding';
4+
import type { StorageInstance } from '../storage';
45
import type { ERC20TokenInstance, NativeTokenInstance } from '../token';
56

67
export interface ComposableBatchInstance<
@@ -16,6 +17,7 @@ export interface ComposableBatchInstance<
1617
address: Address,
1718
abi: TAbi,
1819
): ContractInstance<TAbi>;
20+
storage(): StorageInstance;
1921
readonly calls: ComposableCall[];
2022
add(call: ComposableCall | ComposableCall[]): void;
2123
clear(): void;

src/core/storage/storage.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { NAMESPACE_STORAGE_CONTRACT_ADDRESS } from './constants';
1414
import type {
1515
CheckStorageParams,
1616
GetStorageKeyParams,
17+
ReadStorageParams,
1718
RuntimeValueStorageParams,
1819
StorageInstance,
1920
WriteStorageParams,
@@ -110,6 +111,23 @@ export function storage<
110111
return manager.getDefaultStorageSlotKey(resolvedAccountAddress, resolvedCallerAddress);
111112
},
112113

114+
async read({
115+
storageKey,
116+
accountAddress: accountAddressOverride,
117+
callerAddress: callerAddressOverride,
118+
}: ReadStorageParams = {}) {
119+
const resolvedAccountAddress = accountAddressOverride ?? accountAddress;
120+
const resolvedCallerAddress = callerAddressOverride ?? resolvedAccountAddress;
121+
122+
const slot = await getStorageSlot(resolvedAccountAddress, resolvedCallerAddress, storageKey);
123+
const namespace = getStorageNamespace(resolvedAccountAddress, resolvedCallerAddress);
124+
125+
return contractInstance.read({
126+
functionName: 'readStorage',
127+
args: [namespace, slot],
128+
}) as Promise<`0x${string}`>;
129+
},
130+
113131
async write({
114132
value,
115133
storageKey,

src/core/storage/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Address } from 'viem';
1+
import type { Address, Hex } from 'viem';
22
import type { ComposableCall, RuntimeConstraint, RuntimeValue } from '../encoding';
33
import type { Bytes32SupportedType } from '../encoding/utils';
44

@@ -23,6 +23,12 @@ export interface CheckStorageParams {
2323
callerAddress?: Address;
2424
}
2525

26+
export interface ReadStorageParams {
27+
storageKey?: bigint;
28+
accountAddress?: Address;
29+
callerAddress?: Address;
30+
}
31+
2632
export interface GetStorageKeyParams {
2733
accountAddress?: Address;
2834
callerAddress?: Address;
@@ -31,6 +37,7 @@ export interface GetStorageKeyParams {
3137
export interface StorageInstance {
3238
readonly accountAddress: Address;
3339
getStorageKey(params?: GetStorageKeyParams): Promise<bigint>;
40+
read(params?: ReadStorageParams): Promise<Hex>;
3441
write(params: WriteStorageParams): Promise<ComposableCall>;
3542
runtimeValue(params?: RuntimeValueStorageParams): Promise<RuntimeValue>;
3643
check(params: CheckStorageParams): Promise<ComposableCall>;

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

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
createMeeClient,
33
getMEEVersion,
44
MEEVersion,
5+
toBytes32,
56
toMultichainNexusAccount,
67
} from '@biconomy/abstractjs';
78
import { erc20Abi, getAddress, http } from 'viem';
@@ -167,4 +168,61 @@ describe('Integration — Biconomy abstractjs composable execution', () => {
167168
'UserOp [1] simulation failed. Revert reason: Execution reverted at contract 0x0000000020fe2f30453074ad916edeb653ec7e9d and reverted with error selector 0xa31844b0',
168169
);
169170
});
171+
172+
it('write value to namespace storage → use as runtime transfer amount → sweep remainder', async () => {
173+
// 1. Init Nexus SCA on Base Sepolia and resolve its address + MEE client
174+
const { scaAddress, meeClient } = await initNexus();
175+
176+
// 2. Fund SCA: EOA transfers mock USDC to the SCA and waits for 2 confirmations
177+
const fundTxHash = await _walletClient.writeContract({
178+
abi: erc20Abi,
179+
address: USDC,
180+
functionName: 'transfer',
181+
args: [scaAddress, FUND_AMOUNT],
182+
});
183+
184+
await publicClient.waitForTransactionReceipt({
185+
hash: fundTxHash,
186+
confirmations: 2,
187+
});
188+
189+
// 3. Build composable batch: write storage → check storage → partial transfer → sweep remainder
190+
const batch = ComposableBatch(publicClient, scaAddress);
191+
const usdc = batch.erc20Token(USDC);
192+
const storage = batch.storage();
193+
194+
// Pre-generate a storage key so all storage operations share the same slot
195+
const storageValue = FUND_AMOUNT / 2n;
196+
const storageKey = await storage.getStorageKey();
197+
198+
batch.add([
199+
// Step A: write FUND_AMOUNT/2 into the shared namespace storage slot
200+
await storage.write({ value: storageValue, storageKey }),
201+
// Step B: assert the stored value equals what was just written before proceeding
202+
await storage.check({ storageKey, constraints: [{ eq: storageValue }] }),
203+
// Step C: transfer the runtime-resolved storage value (FUND_AMOUNT/2) from SCA to EOA
204+
usdc.write({
205+
functionName: 'transfer',
206+
args: [_account.address, await storage.runtimeValue({ storageKey })],
207+
}),
208+
// Step D: sweep any remaining SCA balance (the other half) to the EOA
209+
usdc.write({ functionName: 'transfer', args: [_account.address, usdc.runtimeBalance()] }),
210+
]);
211+
212+
expect(batch.length).toBe(4);
213+
214+
// 4. Get a quote for the composable instruction, then sign and submit it via MEE
215+
const quote = await meeClient.getQuote({
216+
instructions: [{ calls: batch.calls, chainId: baseSepolia.id, isComposable: true }],
217+
simulation: { simulate: true },
218+
feeToken: { address: USDC, chainId: baseSepolia.id },
219+
});
220+
221+
// 5. Execute the signed quote and wait for the supertransaction to settle
222+
const { hash } = await meeClient.executeQuote({ quote });
223+
await meeClient.waitForSupertransactionReceipt({ hash });
224+
225+
// 6. Assert the on-chain storage slot holds the value that was written in step A
226+
expect(await storage.read({ storageKey })).to.eq(toBytes32(storageValue));
227+
});
170228
});

0 commit comments

Comments
 (0)