@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.
Files changed (95) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/LICENSE +203 -0
  3. package/README.md +5 -0
  4. package/dist/actor-client.d.ts +13 -0
  5. package/dist/actor-client.d.ts.map +1 -0
  6. package/dist/actor-client.js +44 -0
  7. package/dist/actor-client.js.map +1 -0
  8. package/dist/bundler/client.d.ts +20 -0
  9. package/dist/bundler/client.d.ts.map +1 -0
  10. package/dist/bundler/client.js +228 -0
  11. package/dist/bundler/client.js.map +1 -0
  12. package/dist/bundler/providers/alchemy.d.ts +3 -0
  13. package/dist/bundler/providers/alchemy.d.ts.map +1 -0
  14. package/dist/bundler/providers/alchemy.js +15 -0
  15. package/dist/bundler/providers/alchemy.js.map +1 -0
  16. package/dist/bundler/providers/pimlico.d.ts +3 -0
  17. package/dist/bundler/providers/pimlico.d.ts.map +1 -0
  18. package/dist/bundler/providers/pimlico.js +12 -0
  19. package/dist/bundler/providers/pimlico.js.map +1 -0
  20. package/dist/client.d.ts +24 -0
  21. package/dist/client.d.ts.map +1 -0
  22. package/dist/client.js +352 -0
  23. package/dist/client.js.map +1 -0
  24. package/dist/conversion.d.ts +3 -0
  25. package/dist/conversion.d.ts.map +1 -0
  26. package/dist/conversion.js +27 -0
  27. package/dist/conversion.js.map +1 -0
  28. package/dist/erc4337/builder.d.ts +15 -0
  29. package/dist/erc4337/builder.d.ts.map +1 -0
  30. package/dist/erc4337/builder.js +144 -0
  31. package/dist/erc4337/builder.js.map +1 -0
  32. package/dist/erc4337/providers/kernel.d.ts +23 -0
  33. package/dist/erc4337/providers/kernel.d.ts.map +1 -0
  34. package/dist/erc4337/providers/kernel.js +120 -0
  35. package/dist/erc4337/providers/kernel.js.map +1 -0
  36. package/dist/erc4337/providers/safe.d.ts +23 -0
  37. package/dist/erc4337/providers/safe.d.ts.map +1 -0
  38. package/dist/erc4337/providers/safe.js +117 -0
  39. package/dist/erc4337/providers/safe.js.map +1 -0
  40. package/dist/erc4337/providers/types.d.ts +88 -0
  41. package/dist/erc4337/providers/types.d.ts.map +1 -0
  42. package/dist/erc4337/providers/types.js +35 -0
  43. package/dist/erc4337/providers/types.js.map +1 -0
  44. package/dist/erc4337/smart-wallet.d.ts +16 -0
  45. package/dist/erc4337/smart-wallet.d.ts.map +1 -0
  46. package/dist/erc4337/smart-wallet.js +86 -0
  47. package/dist/erc4337/smart-wallet.js.map +1 -0
  48. package/dist/index.d.ts +16 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/modules/payment-intents.d.ts +99 -0
  53. package/dist/modules/payment-intents.d.ts.map +1 -0
  54. package/dist/modules/payment-intents.js +118 -0
  55. package/dist/modules/payment-intents.js.map +1 -0
  56. package/dist/modules/policies.d.ts +87 -0
  57. package/dist/modules/policies.d.ts.map +1 -0
  58. package/dist/modules/policies.js +61 -0
  59. package/dist/modules/policies.js.map +1 -0
  60. package/dist/paymaster/client.d.ts +11 -0
  61. package/dist/paymaster/client.d.ts.map +1 -0
  62. package/dist/paymaster/client.js +119 -0
  63. package/dist/paymaster/client.js.map +1 -0
  64. package/dist/runtimee.d.ts +4 -0
  65. package/dist/runtimee.d.ts.map +1 -0
  66. package/dist/runtimee.js +4 -0
  67. package/dist/runtimee.js.map +1 -0
  68. package/dist/types.d.ts +389 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +29 -0
  73. package/src/actor-client.ts +57 -0
  74. package/src/bundler/client.ts +306 -0
  75. package/src/bundler/providers/alchemy.ts +21 -0
  76. package/src/bundler/providers/pimlico.ts +17 -0
  77. package/src/client.ts +472 -0
  78. package/src/conversion.ts +25 -0
  79. package/src/erc4337/builder.ts +181 -0
  80. package/src/erc4337/providers/kernel.ts +136 -0
  81. package/src/erc4337/providers/safe.ts +133 -0
  82. package/src/erc4337/providers/types.ts +73 -0
  83. package/src/erc4337/smart-wallet.ts +115 -0
  84. package/src/index.ts +60 -0
  85. package/src/modules/payment-intents.ts +156 -0
  86. package/src/modules/policies.ts +79 -0
  87. package/src/paymaster/client.ts +155 -0
  88. package/src/runtimee.ts +3 -0
  89. package/src/types.ts +469 -0
  90. package/test/architecture/architecture-invariants.test.ts +61 -0
  91. package/test/client.test.ts +180 -0
  92. package/test/conversion.test.ts +42 -0
  93. package/test/runtimee.test.ts +30 -0
  94. package/tsconfig.json +9 -0
  95. 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
+ }