Skip to content

Commit c562589

Browse files
authored
Merge pull request #1 from gelatodigital/feat/bundler/implementation
feat: bundler
2 parents cf80518 + 4bdff16 commit c562589

27 files changed

Lines changed: 1098 additions & 5 deletions

.changeset/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"changelog": "@changesets/cli/changelog",
66
"commit": false,
77
"fixed": [],
8-
"ignore": [],
8+
"ignore": ["bundler-sponsored-payment", "bundler-erc20-payment"],
99
"linked": [],
1010
"updateInternalDependencies": "patch"
1111
}

biome.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"options": {
8181
"patterns": [
8282
{
83-
"group": ["**/dist/**"],
83+
"group": ["**/dist/**", "**/_dist/**"],
8484
"message": "Direct imports from 'dist' folders are disallowed. Import from the package root instead."
8585
}
8686
]
@@ -153,7 +153,7 @@
153153
}
154154
},
155155
{
156-
"includes": ["scripts/**/*"],
156+
"includes": ["scripts/**/*", "examples/**/*"],
157157
"linter": {
158158
"rules": {
159159
"style": {
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
GELATO_API_KEY="Get your api key at https://app.gelato.cloud/"
2+
PRIVATE_KEY="0x<PRIVATE_KEY>" # Optional
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "bundler-sponsored-payment",
3+
"private": true,
4+
"version": "0.0.1",
5+
"scripts": {
6+
"dev": "tsx src/index.ts"
7+
},
8+
"dependencies": {
9+
"@gelatonetwork/ferry-sdk": "workspace:*",
10+
"permissionless": "catalog:",
11+
"viem": "catalog:"
12+
},
13+
"devDependencies": {
14+
"@types/node": "latest",
15+
"dotenv": "^17.2.3",
16+
"tsx": "^4.21.0",
17+
"typescript": "catalog:"
18+
}
19+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { baseSepolia } from 'viem/chains';
2+
import 'dotenv/config';
3+
import { createGelatoBundlerClient, sponsored } from '@gelatonetwork/ferry-sdk';
4+
import { toKernelSmartAccount } from 'permissionless/accounts';
5+
import { createWalletClient, type Hex, http, publicActions } from 'viem';
6+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
7+
8+
const GELATO_API_KEY = process.env['GELATO_API_KEY'];
9+
const PRIVATE_KEY = process.env['PRIVATE_KEY'];
10+
11+
if (!GELATO_API_KEY) {
12+
throw new Error('GELATO_API_KEY is not set');
13+
}
14+
15+
const chain = baseSepolia;
16+
17+
const main = async () => {
18+
const owner = privateKeyToAccount((PRIVATE_KEY ?? generatePrivateKey()) as Hex);
19+
20+
const client = createWalletClient({
21+
account: owner,
22+
chain,
23+
transport: http()
24+
}).extend(publicActions);
25+
26+
const account = await toKernelSmartAccount({
27+
client,
28+
owners: [owner],
29+
version: '0.3.3'
30+
});
31+
32+
const bundler = await createGelatoBundlerClient({
33+
account,
34+
apiKey: GELATO_API_KEY,
35+
client,
36+
payment: sponsored(),
37+
pollingInterval: 100
38+
});
39+
40+
const hash = await bundler.sendUserOperation({
41+
calls: [
42+
{
43+
data: '0xd09de08a',
44+
to: '0xE27C1359cf02B49acC6474311Bd79d1f10b1f8De'
45+
}
46+
]
47+
});
48+
49+
console.log('userOpHash:', hash);
50+
51+
const { receipt } = await bundler.waitForUserOperationReceipt({ hash });
52+
53+
console.log('transaction hash:', receipt.transactionHash);
54+
process.exit(0);
55+
};
56+
57+
main().catch((error) => {
58+
console.error(error);
59+
process.exit(1);
60+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": "../../../tsconfig.json"
3+
}

pnpm-lock.yaml

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ packages:
33
- src
44

55
catalog:
6+
permissionless: 0.3.2
67
typescript: 5.9.3
78
viem: 2.39.3
89

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import type { BaseError, Chain, Client, Transport } from 'viem';
2+
import {
3+
type EstimateUserOperationGasParameters,
4+
type EstimateUserOperationGasReturnType,
5+
formatUserOperationGas,
6+
formatUserOperationRequest,
7+
getUserOperationError,
8+
type PrepareUserOperationParameters,
9+
type SmartAccount,
10+
type UserOperation
11+
} from 'viem/account-abstraction';
12+
import { parseAccount } from 'viem/accounts';
13+
import { AccountNotFoundError, type Payment } from '../../types/index.js';
14+
import { serializeStateOverride } from '../../utils/index.js';
15+
import type { Capabilities } from './getCapabilities.js';
16+
import { prepareUserOperation } from './prepareUserOperation.js';
17+
18+
export const estimateUserOperationGas = async <account extends SmartAccount | undefined>(
19+
client: Client<Transport, Chain | undefined, account>,
20+
parameters: EstimateUserOperationGasParameters,
21+
capabilities: Capabilities,
22+
payment?: Payment
23+
): Promise<EstimateUserOperationGasReturnType> => {
24+
const { account: account_ = client.account, entryPointAddress, stateOverride } = parameters;
25+
26+
if (!account_ && !parameters.sender) throw new AccountNotFoundError();
27+
const account = account_ ? parseAccount(account_) : undefined;
28+
29+
const rpcStateOverride = serializeStateOverride(stateOverride);
30+
31+
const request = account
32+
? await prepareUserOperation(
33+
client,
34+
{
35+
...parameters,
36+
parameters: ['authorization', 'factory', 'nonce', 'paymaster', 'signature']
37+
} as unknown as PrepareUserOperationParameters,
38+
capabilities,
39+
payment
40+
)
41+
: parameters;
42+
43+
try {
44+
const params = [
45+
formatUserOperationRequest(request as UserOperation),
46+
// biome-ignore lint/style/noNonNullAssertion: copied from viem
47+
(entryPointAddress ?? account?.entryPoint?.address)!
48+
] as const;
49+
50+
const result = await client.request({
51+
method: 'eth_estimateUserOperationGas',
52+
params: rpcStateOverride ? [...params, rpcStateOverride] : [...params]
53+
});
54+
return formatUserOperationGas(result) as EstimateUserOperationGasReturnType<account>;
55+
} catch (error) {
56+
// biome-ignore lint/suspicious/noExplicitAny: copied from viem
57+
const calls = (parameters as any).calls;
58+
throw getUserOperationError(error as BaseError, {
59+
...(request as UserOperation),
60+
...(calls ? { calls } : {})
61+
});
62+
}
63+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { Address, Chain, Client, Transport } from 'viem';
2+
3+
export type Capabilities = {
4+
feeCollector: Address;
5+
};
6+
7+
export const getCapabilities = async (client: Client<Transport, Chain>): Promise<Capabilities> => {
8+
const capabilities = await client.request({
9+
method: 'relayer_getCapabilities',
10+
params: [client.chain.id.toString()]
11+
} as never);
12+
13+
return capabilities[client.chain.id.toString()];
14+
};

0 commit comments

Comments
 (0)