@brninpay/sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/LICENSE +203 -0
- package/README.md +5 -0
- package/dist/actor-client.d.ts +13 -0
- package/dist/actor-client.d.ts.map +1 -0
- package/dist/actor-client.js +44 -0
- package/dist/actor-client.js.map +1 -0
- package/dist/bundler/client.d.ts +20 -0
- package/dist/bundler/client.d.ts.map +1 -0
- package/dist/bundler/client.js +228 -0
- package/dist/bundler/client.js.map +1 -0
- package/dist/bundler/providers/alchemy.d.ts +3 -0
- package/dist/bundler/providers/alchemy.d.ts.map +1 -0
- package/dist/bundler/providers/alchemy.js +15 -0
- package/dist/bundler/providers/alchemy.js.map +1 -0
- package/dist/bundler/providers/pimlico.d.ts +3 -0
- package/dist/bundler/providers/pimlico.d.ts.map +1 -0
- package/dist/bundler/providers/pimlico.js +12 -0
- package/dist/bundler/providers/pimlico.js.map +1 -0
- package/dist/client.d.ts +24 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +352 -0
- package/dist/client.js.map +1 -0
- package/dist/conversion.d.ts +3 -0
- package/dist/conversion.d.ts.map +1 -0
- package/dist/conversion.js +27 -0
- package/dist/conversion.js.map +1 -0
- package/dist/erc4337/builder.d.ts +15 -0
- package/dist/erc4337/builder.d.ts.map +1 -0
- package/dist/erc4337/builder.js +144 -0
- package/dist/erc4337/builder.js.map +1 -0
- package/dist/erc4337/providers/kernel.d.ts +23 -0
- package/dist/erc4337/providers/kernel.d.ts.map +1 -0
- package/dist/erc4337/providers/kernel.js +120 -0
- package/dist/erc4337/providers/kernel.js.map +1 -0
- package/dist/erc4337/providers/safe.d.ts +23 -0
- package/dist/erc4337/providers/safe.d.ts.map +1 -0
- package/dist/erc4337/providers/safe.js +117 -0
- package/dist/erc4337/providers/safe.js.map +1 -0
- package/dist/erc4337/providers/types.d.ts +88 -0
- package/dist/erc4337/providers/types.d.ts.map +1 -0
- package/dist/erc4337/providers/types.js +35 -0
- package/dist/erc4337/providers/types.js.map +1 -0
- package/dist/erc4337/smart-wallet.d.ts +16 -0
- package/dist/erc4337/smart-wallet.d.ts.map +1 -0
- package/dist/erc4337/smart-wallet.js +86 -0
- package/dist/erc4337/smart-wallet.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/payment-intents.d.ts +99 -0
- package/dist/modules/payment-intents.d.ts.map +1 -0
- package/dist/modules/payment-intents.js +118 -0
- package/dist/modules/payment-intents.js.map +1 -0
- package/dist/modules/policies.d.ts +87 -0
- package/dist/modules/policies.d.ts.map +1 -0
- package/dist/modules/policies.js +61 -0
- package/dist/modules/policies.js.map +1 -0
- package/dist/paymaster/client.d.ts +11 -0
- package/dist/paymaster/client.d.ts.map +1 -0
- package/dist/paymaster/client.js +119 -0
- package/dist/paymaster/client.js.map +1 -0
- package/dist/runtimee.d.ts +4 -0
- package/dist/runtimee.d.ts.map +1 -0
- package/dist/runtimee.js +4 -0
- package/dist/runtimee.js.map +1 -0
- package/dist/types.d.ts +389 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +29 -0
- package/src/actor-client.ts +57 -0
- package/src/bundler/client.ts +306 -0
- package/src/bundler/providers/alchemy.ts +21 -0
- package/src/bundler/providers/pimlico.ts +17 -0
- package/src/client.ts +472 -0
- package/src/conversion.ts +25 -0
- package/src/erc4337/builder.ts +181 -0
- package/src/erc4337/providers/kernel.ts +136 -0
- package/src/erc4337/providers/safe.ts +133 -0
- package/src/erc4337/providers/types.ts +73 -0
- package/src/erc4337/smart-wallet.ts +115 -0
- package/src/index.ts +60 -0
- package/src/modules/payment-intents.ts +156 -0
- package/src/modules/policies.ts +79 -0
- package/src/paymaster/client.ts +155 -0
- package/src/runtimee.ts +3 -0
- package/src/types.ts +469 -0
- package/test/architecture/architecture-invariants.test.ts +61 -0
- package/test/client.test.ts +180 -0
- package/test/conversion.test.ts +42 -0
- package/test/runtimee.test.ts +30 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { ExecutionError, GasEstimationError, NetworkError, ValidationError } from '@brninpay/core'
|
|
2
|
+
import type {
|
|
3
|
+
BundlerClientConfig,
|
|
4
|
+
BundlerProviderConfig,
|
|
5
|
+
GasEstimate,
|
|
6
|
+
JsonRpcErrorShape,
|
|
7
|
+
JsonRpcFailure,
|
|
8
|
+
JsonRpcSuccess,
|
|
9
|
+
SdkLogger,
|
|
10
|
+
UserOperation,
|
|
11
|
+
UserOperationByHash,
|
|
12
|
+
UserOperationReceipt,
|
|
13
|
+
} from '../types.js'
|
|
14
|
+
|
|
15
|
+
const defaultLogger: SdkLogger = {
|
|
16
|
+
debug: () => undefined,
|
|
17
|
+
info: () => undefined,
|
|
18
|
+
warn: () => undefined,
|
|
19
|
+
error: () => undefined,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sleep(ms: number): Promise<void> {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function bigintToHex(value: bigint): `0x${string}` {
|
|
27
|
+
return `0x${value.toString(16)}`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function numberToHex(value: number): `0x${string}` {
|
|
31
|
+
return bigintToHex(BigInt(value))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function hexToBigInt(value: string | bigint | undefined): bigint {
|
|
35
|
+
if (value === undefined) {
|
|
36
|
+
return 0n
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof value === 'bigint') {
|
|
40
|
+
return value
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return BigInt(value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function serializeUserOperation(userOperation: UserOperation): Record<string, string> {
|
|
47
|
+
if (userOperation.version === 'v0.6') {
|
|
48
|
+
return {
|
|
49
|
+
sender: userOperation.sender,
|
|
50
|
+
nonce: bigintToHex(userOperation.nonce),
|
|
51
|
+
initCode: userOperation.initCode,
|
|
52
|
+
callData: userOperation.callData,
|
|
53
|
+
callGasLimit: bigintToHex(userOperation.callGasLimit),
|
|
54
|
+
verificationGasLimit: bigintToHex(userOperation.verificationGasLimit),
|
|
55
|
+
preVerificationGas: bigintToHex(userOperation.preVerificationGas),
|
|
56
|
+
maxFeePerGas: bigintToHex(userOperation.maxFeePerGas),
|
|
57
|
+
maxPriorityFeePerGas: bigintToHex(userOperation.maxPriorityFeePerGas),
|
|
58
|
+
paymasterAndData: userOperation.paymasterAndData,
|
|
59
|
+
signature: userOperation.signature,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
sender: userOperation.sender,
|
|
65
|
+
nonce: bigintToHex(userOperation.nonce),
|
|
66
|
+
factory: userOperation.factory,
|
|
67
|
+
factoryData: userOperation.factoryData,
|
|
68
|
+
callData: userOperation.callData,
|
|
69
|
+
callGasLimit: bigintToHex(userOperation.callGasLimit),
|
|
70
|
+
verificationGasLimit: bigintToHex(userOperation.verificationGasLimit),
|
|
71
|
+
preVerificationGas: bigintToHex(userOperation.preVerificationGas),
|
|
72
|
+
maxFeePerGas: bigintToHex(userOperation.maxFeePerGas),
|
|
73
|
+
maxPriorityFeePerGas: bigintToHex(userOperation.maxPriorityFeePerGas),
|
|
74
|
+
paymaster: userOperation.paymaster,
|
|
75
|
+
paymasterVerificationGasLimit: bigintToHex(userOperation.paymasterVerificationGasLimit),
|
|
76
|
+
paymasterPostOpGasLimit: bigintToHex(userOperation.paymasterPostOpGasLimit),
|
|
77
|
+
paymasterData: userOperation.paymasterData,
|
|
78
|
+
signature: userOperation.signature,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mapJsonRpcError(error: JsonRpcErrorShape, method: string): Error {
|
|
83
|
+
if (method === 'eth_estimateUserOperationGas') {
|
|
84
|
+
return new GasEstimationError(error.message, error.data)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return new ExecutionError('bundler_rpc_error', error.message, {
|
|
88
|
+
method,
|
|
89
|
+
code: error.code,
|
|
90
|
+
data: error.data,
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export class BundlerClient {
|
|
95
|
+
private readonly timeoutMs: number
|
|
96
|
+
private readonly pollingIntervalMs: number
|
|
97
|
+
private readonly maxRetries: number
|
|
98
|
+
private readonly logger: SdkLogger
|
|
99
|
+
private readonly fetchImplementation: typeof fetch
|
|
100
|
+
|
|
101
|
+
constructor(private readonly config: BundlerClientConfig) {
|
|
102
|
+
this.timeoutMs = config.timeoutMs ?? 15_000
|
|
103
|
+
this.pollingIntervalMs = config.pollingIntervalMs ?? 2_000
|
|
104
|
+
this.maxRetries = config.maxRetries ?? 1
|
|
105
|
+
this.logger = config.logger ?? defaultLogger
|
|
106
|
+
this.fetchImplementation = config.fetchImplementation ?? fetch.bind(globalThis)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async estimateUserOperationGas(userOperation: UserOperation): Promise<GasEstimate> {
|
|
110
|
+
const result = await this.requestWithFallback<Record<string, string>>(
|
|
111
|
+
'eth_estimateUserOperationGas',
|
|
112
|
+
[serializeUserOperation(userOperation), this.config.entryPointAddress]
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
callGasLimit: hexToBigInt(result.callGasLimit),
|
|
117
|
+
verificationGasLimit: hexToBigInt(result.verificationGasLimit),
|
|
118
|
+
preVerificationGas: hexToBigInt(result.preVerificationGas),
|
|
119
|
+
maxFeePerGas: userOperation.maxFeePerGas,
|
|
120
|
+
maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas,
|
|
121
|
+
paymasterVerificationGasLimit: result.paymasterVerificationGasLimit
|
|
122
|
+
? hexToBigInt(result.paymasterVerificationGasLimit)
|
|
123
|
+
: undefined,
|
|
124
|
+
paymasterPostOpGasLimit: result.paymasterPostOpGasLimit
|
|
125
|
+
? hexToBigInt(result.paymasterPostOpGasLimit)
|
|
126
|
+
: undefined,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async sendUserOperation(userOperation: UserOperation): Promise<`0x${string}`> {
|
|
131
|
+
return this.requestWithFallback<`0x${string}`>('eth_sendUserOperation', [
|
|
132
|
+
serializeUserOperation(userOperation),
|
|
133
|
+
this.config.entryPointAddress,
|
|
134
|
+
])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async getUserOperationReceipt(
|
|
138
|
+
hash: `0x${string}`,
|
|
139
|
+
options: { pollingIntervalMs?: number; timeoutMs?: number } = {}
|
|
140
|
+
): Promise<UserOperationReceipt> {
|
|
141
|
+
const startedAt = Date.now()
|
|
142
|
+
const pollingIntervalMs = options.pollingIntervalMs ?? this.pollingIntervalMs
|
|
143
|
+
const timeoutMs = options.timeoutMs ?? 60_000
|
|
144
|
+
|
|
145
|
+
while (true) {
|
|
146
|
+
const receipt = await this.requestWithFallback<{
|
|
147
|
+
userOpHash: `0x${string}`
|
|
148
|
+
sender: `0x${string}`
|
|
149
|
+
nonce: string
|
|
150
|
+
actualGasCost: string
|
|
151
|
+
actualGasUsed: string
|
|
152
|
+
success: boolean
|
|
153
|
+
entryPoint: `0x${string}`
|
|
154
|
+
receipt: {
|
|
155
|
+
transactionHash: `0x${string}`
|
|
156
|
+
blockHash: `0x${string}`
|
|
157
|
+
blockNumber: string
|
|
158
|
+
}
|
|
159
|
+
logs: readonly unknown[]
|
|
160
|
+
} | null>('eth_getUserOperationReceipt', [hash], {
|
|
161
|
+
allowNotFound: true,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if (receipt) {
|
|
165
|
+
return {
|
|
166
|
+
userOpHash: receipt.userOpHash,
|
|
167
|
+
sender: receipt.sender,
|
|
168
|
+
nonce: BigInt(receipt.nonce),
|
|
169
|
+
actualGasCost: BigInt(receipt.actualGasCost),
|
|
170
|
+
actualGasUsed: BigInt(receipt.actualGasUsed),
|
|
171
|
+
success: receipt.success,
|
|
172
|
+
entryPoint: receipt.entryPoint,
|
|
173
|
+
receipt: {
|
|
174
|
+
transactionHash: receipt.receipt.transactionHash,
|
|
175
|
+
blockHash: receipt.receipt.blockHash,
|
|
176
|
+
blockNumber: BigInt(receipt.receipt.blockNumber),
|
|
177
|
+
},
|
|
178
|
+
logs: receipt.logs,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (Date.now() - startedAt >= timeoutMs) {
|
|
183
|
+
throw new ValidationError('Timed out while waiting for user operation receipt', {
|
|
184
|
+
hash,
|
|
185
|
+
timeoutMs,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await sleep(pollingIntervalMs)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getUserOperationByHash(hash: `0x${string}`): Promise<UserOperationByHash> {
|
|
194
|
+
const result = await this.requestWithFallback<{
|
|
195
|
+
blockHash: `0x${string}`
|
|
196
|
+
blockNumber: string
|
|
197
|
+
entryPoint: `0x${string}`
|
|
198
|
+
transactionHash: `0x${string}`
|
|
199
|
+
userOperation: UserOperation
|
|
200
|
+
}>('eth_getUserOperationByHash', [hash])
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
blockHash: result.blockHash,
|
|
204
|
+
blockNumber: BigInt(result.blockNumber),
|
|
205
|
+
entryPoint: result.entryPoint,
|
|
206
|
+
transactionHash: result.transactionHash,
|
|
207
|
+
userOperation: result.userOperation,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async requestWithFallback<T>(
|
|
212
|
+
method: string,
|
|
213
|
+
params: unknown[],
|
|
214
|
+
options: { allowNotFound?: boolean } = {}
|
|
215
|
+
): Promise<T> {
|
|
216
|
+
const providers = [this.config.primary, this.config.secondary].filter(
|
|
217
|
+
(provider): provider is BundlerProviderConfig => Boolean(provider)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
let lastError: Error | undefined
|
|
221
|
+
for (const provider of providers) {
|
|
222
|
+
try {
|
|
223
|
+
return await this.requestWithProvider<T>(provider, method, params, options)
|
|
224
|
+
} catch (error) {
|
|
225
|
+
lastError = error instanceof Error ? error : new Error(String(error))
|
|
226
|
+
this.logger.warn('Bundler provider failed', {
|
|
227
|
+
provider: provider.name,
|
|
228
|
+
method,
|
|
229
|
+
error: lastError.message,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
throw lastError ?? new ExecutionError('bundler_failure', `Bundler request ${method} failed`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private async requestWithProvider<T>(
|
|
238
|
+
provider: BundlerProviderConfig,
|
|
239
|
+
method: string,
|
|
240
|
+
params: unknown[],
|
|
241
|
+
options: { allowNotFound?: boolean }
|
|
242
|
+
): Promise<T> {
|
|
243
|
+
const body = JSON.stringify({
|
|
244
|
+
jsonrpc: '2.0',
|
|
245
|
+
id: Date.now(),
|
|
246
|
+
method,
|
|
247
|
+
params,
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
let attempt = 0
|
|
251
|
+
while (true) {
|
|
252
|
+
attempt += 1
|
|
253
|
+
const controller = new AbortController()
|
|
254
|
+
const timeoutId = setTimeout(() => controller.abort('Bundler request timed out'), this.timeoutMs)
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const response = await this.fetchImplementation(provider.url, {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
headers: {
|
|
260
|
+
'Content-Type': 'application/json',
|
|
261
|
+
...(provider.headers ?? {}),
|
|
262
|
+
},
|
|
263
|
+
body,
|
|
264
|
+
signal: controller.signal,
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (!response.ok) {
|
|
268
|
+
throw new NetworkError(`Bundler HTTP ${response.status}: ${response.statusText}`, {
|
|
269
|
+
provider: provider.name,
|
|
270
|
+
method,
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const payload = (await response.json()) as JsonRpcSuccess<T> | JsonRpcFailure
|
|
275
|
+
if ('error' in payload) {
|
|
276
|
+
if (options.allowNotFound && payload.error.code === -32001) {
|
|
277
|
+
return null as T
|
|
278
|
+
}
|
|
279
|
+
throw mapJsonRpcError(payload.error, method)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return payload.result
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (attempt > this.maxRetries + 1) {
|
|
285
|
+
throw error
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
289
|
+
throw new NetworkError('Bundler request timed out', {
|
|
290
|
+
provider: provider.name,
|
|
291
|
+
method,
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (error instanceof NetworkError) {
|
|
296
|
+
await sleep(250 * attempt)
|
|
297
|
+
continue
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
throw error
|
|
301
|
+
} finally {
|
|
302
|
+
clearTimeout(timeoutId)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ValidationError } from '@brninpay/core'
|
|
2
|
+
import type { AlchemyBundlerProviderParams, BundlerProviderConfig } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
export function createAlchemyBundlerProvider(
|
|
5
|
+
params: AlchemyBundlerProviderParams
|
|
6
|
+
): BundlerProviderConfig {
|
|
7
|
+
if (!params.apiKey.trim()) {
|
|
8
|
+
throw new ValidationError('Alchemy bundler provider requires a non-empty apiKey')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!params.url && !params.network?.trim()) {
|
|
12
|
+
throw new ValidationError('Alchemy bundler provider requires either a url or network')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
name: 'alchemy',
|
|
17
|
+
url:
|
|
18
|
+
params.url ??
|
|
19
|
+
`https://${params.network}.g.alchemy.com/v2/${encodeURIComponent(params.apiKey)}`,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ValidationError } from '@brninpay/core'
|
|
2
|
+
import type { BundlerProviderConfig, PimlicoBundlerProviderParams } from '../../types.js'
|
|
3
|
+
|
|
4
|
+
export function createPimlicoBundlerProvider(
|
|
5
|
+
params: PimlicoBundlerProviderParams
|
|
6
|
+
): BundlerProviderConfig {
|
|
7
|
+
if (!params.apiKey.trim()) {
|
|
8
|
+
throw new ValidationError('Pimlico bundler provider requires a non-empty apiKey')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
name: 'pimlico',
|
|
13
|
+
url:
|
|
14
|
+
params.url ??
|
|
15
|
+
`https://api.pimlico.io/v2/${params.chainId}/rpc?apikey=${encodeURIComponent(params.apiKey)}`,
|
|
16
|
+
}
|
|
17
|
+
}
|