viem 3.0.0-next.12 → 3.0.0-next.13

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 (185) hide show
  1. package/dist/chains/definitions/creditCoin3Devnet.d.ts +6 -0
  2. package/dist/chains/definitions/creditCoin3Devnet.d.ts.map +1 -1
  3. package/dist/chains/definitions/creditCoin3Devnet.js +6 -0
  4. package/dist/chains/definitions/creditCoin3Devnet.js.map +1 -1
  5. package/dist/chains/definitions/creditCoin3Mainnet.d.ts +5 -1
  6. package/dist/chains/definitions/creditCoin3Mainnet.d.ts.map +1 -1
  7. package/dist/chains/definitions/creditCoin3Mainnet.js +4 -0
  8. package/dist/chains/definitions/creditCoin3Mainnet.js.map +1 -1
  9. package/dist/chains/definitions/creditCoin3Testnet.d.ts +5 -1
  10. package/dist/chains/definitions/creditCoin3Testnet.d.ts.map +1 -1
  11. package/dist/chains/definitions/creditCoin3Testnet.js +4 -0
  12. package/dist/chains/definitions/creditCoin3Testnet.js.map +1 -1
  13. package/dist/chains/definitions/crymad.d.ts +25 -0
  14. package/dist/chains/definitions/crymad.d.ts.map +1 -0
  15. package/dist/chains/definitions/crymad.js +23 -0
  16. package/dist/chains/definitions/crymad.js.map +1 -0
  17. package/dist/chains/definitions/crymadL2.d.ts +26 -0
  18. package/dist/chains/definitions/crymadL2.d.ts.map +1 -0
  19. package/dist/chains/definitions/crymadL2.js +24 -0
  20. package/dist/chains/definitions/crymadL2.js.map +1 -0
  21. package/dist/chains/definitions/kortana.d.ts +18 -0
  22. package/dist/chains/definitions/kortana.d.ts.map +1 -0
  23. package/dist/chains/definitions/kortana.js +16 -0
  24. package/dist/chains/definitions/kortana.js.map +1 -0
  25. package/dist/chains/index.d.ts +3 -0
  26. package/dist/chains/index.d.ts.map +1 -1
  27. package/dist/chains/index.js +3 -0
  28. package/dist/chains/index.js.map +1 -1
  29. package/dist/core/actions/wallet/sendCalls.d.ts.map +1 -1
  30. package/dist/core/actions/wallet/sendCalls.js +1 -0
  31. package/dist/core/actions/wallet/sendCalls.js.map +1 -1
  32. package/dist/erc4337/actions/userOperation/waitForReceipt.js +1 -1
  33. package/dist/erc4337/actions/userOperation/waitForReceipt.js.map +1 -1
  34. package/dist/node/transports/ipc.d.ts.map +1 -1
  35. package/dist/node/transports/ipc.js +20 -2
  36. package/dist/node/transports/ipc.js.map +1 -1
  37. package/dist/tempo/Abis.d.ts +778 -174
  38. package/dist/tempo/Abis.d.ts.map +1 -1
  39. package/dist/tempo/Abis.js +178 -37
  40. package/dist/tempo/Abis.js.map +1 -1
  41. package/dist/tempo/Capabilities.d.ts +7 -13
  42. package/dist/tempo/Capabilities.d.ts.map +1 -1
  43. package/dist/tempo/Client.d.ts +0 -5
  44. package/dist/tempo/Client.d.ts.map +1 -1
  45. package/dist/tempo/Client.js +3 -10
  46. package/dist/tempo/Client.js.map +1 -1
  47. package/dist/tempo/ExecutionError.d.ts +81 -0
  48. package/dist/tempo/ExecutionError.d.ts.map +1 -0
  49. package/dist/tempo/ExecutionError.js +309 -0
  50. package/dist/tempo/ExecutionError.js.map +1 -0
  51. package/dist/tempo/Relay.d.ts +303 -0
  52. package/dist/tempo/Relay.d.ts.map +1 -0
  53. package/dist/tempo/Relay.js +170 -0
  54. package/dist/tempo/Relay.js.map +1 -0
  55. package/dist/tempo/Selectors.d.ts +16 -5
  56. package/dist/tempo/Selectors.d.ts.map +1 -1
  57. package/dist/tempo/Selectors.js +16 -5
  58. package/dist/tempo/Selectors.js.map +1 -1
  59. package/dist/tempo/Transport.d.ts +11 -11
  60. package/dist/tempo/Transport.d.ts.map +1 -1
  61. package/dist/tempo/Transport.js +22 -51
  62. package/dist/tempo/Transport.js.map +1 -1
  63. package/dist/tempo/actions/token/burn.d.ts +38 -9
  64. package/dist/tempo/actions/token/burn.d.ts.map +1 -1
  65. package/dist/tempo/actions/token/burn.js +12 -5
  66. package/dist/tempo/actions/token/burn.js.map +1 -1
  67. package/dist/tempo/actions/token/burnSync.d.ts +2 -0
  68. package/dist/tempo/actions/token/burnSync.d.ts.map +1 -1
  69. package/dist/tempo/chainConfig.d.ts +4 -5
  70. package/dist/tempo/chainConfig.d.ts.map +1 -1
  71. package/dist/tempo/chainConfig.js +6 -1
  72. package/dist/tempo/chainConfig.js.map +1 -1
  73. package/dist/tempo/index.d.ts +5 -3
  74. package/dist/tempo/index.d.ts.map +1 -1
  75. package/dist/tempo/index.js +5 -3
  76. package/dist/tempo/index.js.map +1 -1
  77. package/dist/tempo/internal/relay/cache.d.ts +9 -0
  78. package/dist/tempo/internal/relay/cache.d.ts.map +1 -0
  79. package/dist/tempo/internal/relay/cache.js +72 -0
  80. package/dist/tempo/internal/relay/cache.js.map +1 -0
  81. package/dist/tempo/internal/relay/deadline.d.ts +4 -0
  82. package/dist/tempo/internal/relay/deadline.d.ts.map +1 -0
  83. package/dist/tempo/internal/relay/deadline.js +34 -0
  84. package/dist/tempo/internal/relay/deadline.js.map +1 -0
  85. package/dist/tempo/internal/relay/error.d.ts +47 -0
  86. package/dist/tempo/internal/relay/error.d.ts.map +1 -0
  87. package/dist/tempo/internal/relay/error.js +101 -0
  88. package/dist/tempo/internal/relay/error.js.map +1 -0
  89. package/dist/tempo/internal/relay/feePayer.d.ts +70 -0
  90. package/dist/tempo/internal/relay/feePayer.d.ts.map +1 -0
  91. package/dist/tempo/internal/relay/feePayer.js +442 -0
  92. package/dist/tempo/internal/relay/feePayer.js.map +1 -0
  93. package/dist/tempo/internal/relay/feeToken.d.ts +38 -0
  94. package/dist/tempo/internal/relay/feeToken.d.ts.map +1 -0
  95. package/dist/tempo/internal/relay/feeToken.js +143 -0
  96. package/dist/tempo/internal/relay/feeToken.js.map +1 -0
  97. package/dist/tempo/internal/relay/multisig.d.ts +4 -0
  98. package/dist/tempo/internal/relay/multisig.d.ts.map +1 -0
  99. package/dist/tempo/{Multisig.js → internal/relay/multisig.js} +134 -126
  100. package/dist/tempo/internal/relay/multisig.js.map +1 -0
  101. package/dist/tempo/internal/relay/plugin.d.ts +5 -0
  102. package/dist/tempo/internal/relay/plugin.d.ts.map +1 -0
  103. package/dist/tempo/internal/relay/plugin.js +10 -0
  104. package/dist/tempo/internal/relay/plugin.js.map +1 -0
  105. package/dist/tempo/internal/relay/preflight.d.ts +17 -0
  106. package/dist/tempo/internal/relay/preflight.d.ts.map +1 -0
  107. package/dist/tempo/internal/relay/preflight.js +53 -0
  108. package/dist/tempo/internal/relay/preflight.js.map +1 -0
  109. package/dist/tempo/internal/relay/request.d.ts +16 -0
  110. package/dist/tempo/internal/relay/request.d.ts.map +1 -0
  111. package/dist/tempo/internal/relay/request.js +327 -0
  112. package/dist/tempo/internal/relay/request.js.map +1 -0
  113. package/dist/tempo/internal/relay/simulate.d.ts +62 -0
  114. package/dist/tempo/internal/relay/simulate.d.ts.map +1 -0
  115. package/dist/tempo/internal/relay/simulate.js +284 -0
  116. package/dist/tempo/internal/relay/simulate.js.map +1 -0
  117. package/dist/tempo/internal/relay/utils.d.ts +346 -0
  118. package/dist/tempo/internal/relay/utils.d.ts.map +1 -0
  119. package/dist/tempo/internal/relay/utils.js +185 -0
  120. package/dist/tempo/internal/relay/utils.js.map +1 -0
  121. package/dist/tempo/internal/relay/virtualAddress.d.ts +9 -0
  122. package/dist/tempo/internal/relay/virtualAddress.d.ts.map +1 -0
  123. package/dist/tempo/internal/relay/virtualAddress.js +73 -0
  124. package/dist/tempo/internal/relay/virtualAddress.js.map +1 -0
  125. package/dist/tempo/internal/relay.d.ts +4 -0
  126. package/dist/tempo/internal/relay.d.ts.map +1 -0
  127. package/dist/tempo/internal/relay.js +132 -0
  128. package/dist/tempo/internal/relay.js.map +1 -0
  129. package/dist/tokens/definitions/ousd.d.ts +26 -0
  130. package/dist/tokens/definitions/ousd.d.ts.map +1 -0
  131. package/dist/tokens/definitions/ousd.js +26 -0
  132. package/dist/tokens/definitions/ousd.js.map +1 -0
  133. package/dist/tokens/index.d.ts +2 -0
  134. package/dist/tokens/index.d.ts.map +1 -1
  135. package/dist/tokens/index.js +2 -0
  136. package/dist/tokens/index.js.map +1 -1
  137. package/dist/tokens/sets.d.ts +33 -0
  138. package/dist/tokens/sets.d.ts.map +1 -1
  139. package/dist/tokens/sets.js +4 -1
  140. package/dist/tokens/sets.js.map +1 -1
  141. package/dist/version.d.ts +1 -1
  142. package/dist/version.js +1 -1
  143. package/package.json +1 -1
  144. package/src/chains/definitions/creditCoin3Devnet.ts +6 -0
  145. package/src/chains/definitions/creditCoin3Mainnet.ts +4 -0
  146. package/src/chains/definitions/creditCoin3Testnet.ts +4 -0
  147. package/src/chains/definitions/crymad.ts +23 -0
  148. package/src/chains/definitions/crymadL2.ts +24 -0
  149. package/src/chains/definitions/kortana.ts +16 -0
  150. package/src/chains/index.ts +3 -0
  151. package/src/core/actions/wallet/sendCalls.ts +1 -0
  152. package/src/erc4337/actions/userOperation/waitForReceipt.ts +1 -1
  153. package/src/node/transports/ipc.ts +17 -2
  154. package/src/tempo/Abis.ts +179 -37
  155. package/src/tempo/Account.ts +1 -1
  156. package/src/tempo/Capabilities.ts +7 -15
  157. package/src/tempo/Client.ts +3 -23
  158. package/src/tempo/ExecutionError.ts +428 -0
  159. package/src/tempo/Relay.ts +430 -0
  160. package/src/tempo/Selectors.ts +25 -11
  161. package/src/tempo/Transport.ts +71 -41
  162. package/src/tempo/actions/token/burn.ts +21 -9
  163. package/src/tempo/actions/token/burnSync.ts +2 -0
  164. package/src/tempo/chainConfig.ts +9 -6
  165. package/src/tempo/index.ts +7 -4
  166. package/src/tempo/internal/relay/cache.ts +82 -0
  167. package/src/tempo/internal/relay/deadline.ts +45 -0
  168. package/src/tempo/internal/relay/error.ts +129 -0
  169. package/src/tempo/internal/relay/feePayer.ts +693 -0
  170. package/src/tempo/internal/relay/feeToken.ts +190 -0
  171. package/src/tempo/{Multisig.ts → internal/relay/multisig.ts} +165 -193
  172. package/src/tempo/internal/relay/plugin.ts +13 -0
  173. package/src/tempo/internal/relay/preflight.ts +83 -0
  174. package/src/tempo/internal/relay/request.ts +401 -0
  175. package/src/tempo/internal/relay/simulate.ts +394 -0
  176. package/src/tempo/internal/relay/utils.ts +213 -0
  177. package/src/tempo/internal/relay/virtualAddress.ts +95 -0
  178. package/src/tempo/internal/relay.ts +149 -0
  179. package/src/tokens/definitions/ousd.ts +26 -0
  180. package/src/tokens/index.ts +2 -0
  181. package/src/tokens/sets.ts +4 -1
  182. package/src/version.ts +1 -1
  183. package/dist/tempo/Multisig.d.ts +0 -39
  184. package/dist/tempo/Multisig.d.ts.map +0 -1
  185. package/dist/tempo/Multisig.js.map +0 -1
@@ -0,0 +1,693 @@
1
+ import type { Address } from 'abitype'
2
+ import { Hash, type Hex, RpcResponse, Signature } from 'ox'
3
+ import { type Transaction as core_Transaction, TxEnvelopeTempo } from 'ox/tempo'
4
+ import type { Local as LocalAccount } from '../../../core/Account.js'
5
+ import { type Client, create as createClient } from '../../../core/Client.js'
6
+ import { http } from '../../../core/transports/http.js'
7
+ import type * as Relay from '../../Relay.js'
8
+ import * as Transaction from 'ox/tempo/TxEnvelopeTempo'
9
+ import type * as TransactionRequest from 'ox/tempo/TransactionRequest'
10
+ import * as Request from './request.js'
11
+ import * as Utils from './utils.js'
12
+
13
+ export function create(options: Relay.feePayer.Options): Relay.Plugin {
14
+ // Bind signing permission to the middleware invocation and its validated payload.
15
+ const authorized = new WeakMap<Relay.Plugin.Context, Hex.Hex>()
16
+ const allowedFeePayers = new Set(
17
+ options.allowedFeePayers?.map((url) =>
18
+ ExternalFeePayerUrl.normalize(url, {
19
+ allowUnsafe: options.internal_allowUnsafeUrls,
20
+ }),
21
+ ),
22
+ )
23
+ return {
24
+ async handleRequest(context, next) {
25
+ const { request } = context
26
+ if (
27
+ ![
28
+ 'eth_fillTransaction',
29
+ 'eth_signRawTransaction',
30
+ 'eth_sendRawTransaction',
31
+ 'eth_sendRawTransactionSync',
32
+ ].includes(request.method)
33
+ )
34
+ return next()
35
+ const { client, getClient, options: requestOptions } = context
36
+ const chainId = requestOptions.chainId
37
+
38
+ const getTokens = (id: number) => context.resolveTokens(id)
39
+
40
+ const record = (details: SponsorshipDetails | undefined) => {
41
+ if (details && requestOptions.response)
42
+ requestOptions.response.sponsorship_details = details
43
+ }
44
+
45
+ if (request.method !== 'eth_fillTransaction') {
46
+ if (!options.account) {
47
+ if (request.method === 'eth_signRawTransaction')
48
+ throw new RpcResponse.MethodNotFoundError({
49
+ message:
50
+ 'eth_signRawTransaction requires a fee payer to be configured on the relay. Add `Relay.feePayer({ account })` to enable transaction sponsorship.',
51
+ })
52
+
53
+ return next()
54
+ }
55
+
56
+ const serialized = request.params?.[0]
57
+ if (
58
+ typeof serialized !== 'string' ||
59
+ !requestsRawSponsorship(serialized as Hex.Hex)
60
+ )
61
+ return next()
62
+
63
+ const result = await handleRawTransaction({
64
+ ...options,
65
+ account: options.account,
66
+ getClient,
67
+ getFeeToken: async (id) => (await getTokens(id))[0],
68
+ method: request.method as
69
+ | 'eth_signRawTransaction'
70
+ | 'eth_sendRawTransaction'
71
+ | 'eth_sendRawTransactionSync',
72
+ request,
73
+ })
74
+
75
+ record(result.sponsorshipDetails)
76
+ return result.result
77
+ }
78
+
79
+ const parameters = request.params![0] as Record<string, unknown>
80
+ const { feePayer: _, ...normalized } =
81
+ Utils.normalizeFillTransactionRequest(parameters)
82
+
83
+ const external =
84
+ typeof parameters.feePayer === 'string'
85
+ ? ExternalFeePayerUrl.normalize(parameters.feePayer, {
86
+ allowUnsafe: options.internal_allowUnsafeUrls ?? false,
87
+ })
88
+ : undefined
89
+ if (external && !allowedFeePayers.has(external))
90
+ throw new RpcResponse.InvalidParamsError({
91
+ message: 'External fee payer URL is not allowed.',
92
+ })
93
+
94
+ const wantsSponsorship =
95
+ (!!options.account || !!external) && parameters.feePayer !== false
96
+ if (options.account && !external) delete normalized.feePayerSignature
97
+ const base = { ...normalized, chainId }
98
+ if (!wantsSponsorship) return Request.fill(client, base)
99
+
100
+ const token =
101
+ options.feeToken ??
102
+ (parameters.feeToken as Address | undefined) ??
103
+ (!options.validate || external
104
+ ? (await getTokens(chainId!))[0]
105
+ : undefined)
106
+ const transaction = {
107
+ ...base,
108
+ feePayer: true,
109
+ ...(token ? { feeToken: token } : {}),
110
+ }
111
+
112
+ const prepared = isPreparedTransaction(transaction)
113
+ const fillClient = external
114
+ ? createClient({
115
+ chain: client.chain,
116
+ transport: http(external, {
117
+ fetchOptions: { redirect: 'error' },
118
+ retryCount: 0,
119
+ }),
120
+ })
121
+ : client
122
+ const result: Request.Result =
123
+ prepared && !external
124
+ ? { tx: transaction }
125
+ : await Request.fill(
126
+ fillClient,
127
+ transaction,
128
+ external ? { ...requestOptions, retryCount: 0 } : requestOptions,
129
+ )
130
+ const filled = Utils.normalizeTempoTransaction(result.tx)
131
+
132
+ // Reserve intrinsic gas for larger signatures before validating and signing the candidate.
133
+ if (
134
+ (!prepared || parameters.feePayer !== true) &&
135
+ filled.gas &&
136
+ !filled.feePayerSignature
137
+ )
138
+ filled.gas += 20_000n
139
+ if (token && (!external || filled.feeToken == null))
140
+ Object.assign(filled, { feeToken: token })
141
+
142
+ const sponsored =
143
+ external ||
144
+ !options.validate ||
145
+ (await shouldSponsor({
146
+ sender: parameters.from as Address | undefined,
147
+ transaction: filled,
148
+ validate: options.validate,
149
+ }))
150
+ if (!sponsored) return Request.fill(client, { ...base, feePayer: false })
151
+
152
+ const defer =
153
+ typeof normalized.multisigSimulation === 'object' &&
154
+ normalized.multisigSimulation !== null
155
+
156
+ const sponsor = sponsored
157
+ ? external
158
+ ? (result.capabilities?.sponsor ?? result.sponsor)
159
+ : options.account
160
+ ? getSponsor({ ...options, account: options.account })
161
+ : undefined
162
+ : undefined
163
+
164
+ if (options.account && !external && !defer) {
165
+ delete filled.signature
166
+ if (!filled.from) filled.from = parameters.from as Address
167
+ authorized.set(
168
+ context,
169
+ TxEnvelopeTempo.getFeePayerSignPayload(
170
+ TxEnvelopeTempo.from(filled as never),
171
+ { sender: filled.from },
172
+ ),
173
+ )
174
+ }
175
+ return {
176
+ ...result,
177
+ ...(sponsor ? { sponsor } : {}),
178
+ tx: Utils.formatTempoTransaction(
179
+ filled as core_Transaction.Transaction,
180
+ ),
181
+ capabilities: {
182
+ ...result.capabilities,
183
+ sponsored: external
184
+ ? (result.capabilities?.sponsored ?? !!sponsor)
185
+ : !!sponsor,
186
+ ...(sponsor ? { sponsor } : {}),
187
+ },
188
+ }
189
+ },
190
+ async signTransaction(result, context) {
191
+ const approved = authorized.get(context)
192
+ authorized.delete(context)
193
+ const parameters = context.request.params![0] as Record<string, unknown>
194
+ if (
195
+ !approved ||
196
+ !options.account ||
197
+ typeof parameters.feePayer === 'string' ||
198
+ parameters.feePayer === false ||
199
+ !result.capabilities?.sponsored ||
200
+ (typeof parameters.multisigSimulation === 'object' &&
201
+ parameters.multisigSimulation !== null)
202
+ )
203
+ return undefined
204
+ const transaction = Utils.normalizeTempoTransaction(result.tx)
205
+ const payload = TxEnvelopeTempo.getFeePayerSignPayload(
206
+ TxEnvelopeTempo.from(transaction as never),
207
+ { sender: (transaction.from ?? parameters.from) as Address },
208
+ )
209
+ if (payload !== approved)
210
+ throw new RpcResponse.InvalidParamsError({
211
+ message: 'Sponsored transaction changed after validation.',
212
+ })
213
+ const signed = await sign({
214
+ account: options.account,
215
+ onSponsored: options.onSponsored,
216
+ sender: parameters.from as Address | undefined,
217
+ transaction,
218
+ signal: context.options.signal,
219
+ })
220
+ if (signed.sponsorshipDetails && context.options.response)
221
+ context.options.response.sponsorship_details = signed.sponsorshipDetails
222
+ const tx = Utils.formatTempoTransaction(
223
+ signed.transaction as core_Transaction.Transaction,
224
+ )
225
+ return tx.feePayerSignature as Relay.Plugin.Signature
226
+ },
227
+ }
228
+ }
229
+
230
+ /** Checks a prepared transaction with its chain ID. Rejected fills fall back to sender payment; rejected raw submissions return a refusal. */
231
+ export type Validate = (
232
+ request: TransactionRequest.TransactionRequest & {
233
+ chainId?: number | Hex.Hex | undefined
234
+ },
235
+ ) => Validation | Promise<Validation>
236
+
237
+ /** A sponsorship verdict: `true` sponsors; `false` or a named reason refuses. */
238
+ export type Validation =
239
+ | boolean
240
+ | 'billing_past_due'
241
+ | 'billing_required'
242
+ | 'fee_token_unsupported'
243
+ | 'spend_limit_exceeded'
244
+ | 'tx_fee_limit_exceeded'
245
+
246
+ /** Details recorded for a sponsored transaction. */
247
+ export type SponsorshipDetails = {
248
+ /** Whether Tempo subsidizes the sponsorship instead of charging the API-key organization. */
249
+ subsidized: boolean
250
+ }
251
+
252
+ /** Refusal messages keyed by named validation reason. */
253
+ const refusalMessages = {
254
+ billing_past_due: 'Billing past due.',
255
+ billing_required: 'Billing required.',
256
+ fee_token_unsupported: 'Fee token unsupported.',
257
+ spend_limit_exceeded: 'Spend limit exceeded.',
258
+ tx_fee_limit_exceeded: 'Transaction fee limit exceeded.',
259
+ } as const satisfies Record<Exclude<Validation, boolean>, string>
260
+
261
+ /** Returns sponsor metadata for `eth_fillTransaction` responses. */
262
+ // biome-ignore lint/correctness/noUnusedVariables: declaration merge
263
+ function getSponsor(options: getSponsor.Options): getSponsor.ReturnType {
264
+ const { account, name, url } = options
265
+ return {
266
+ address: account.address,
267
+ ...(name ? { name } : {}),
268
+ ...(url ? { url } : {}),
269
+ }
270
+ }
271
+
272
+ declare namespace getSponsor {
273
+ type Options = {
274
+ /** Account used for sponsorship. */
275
+ account: LocalAccount
276
+ /** Optional display name. */
277
+ name?: string | undefined
278
+ /** Optional display URL. */
279
+ url?: string | undefined
280
+ }
281
+
282
+ type ReturnType = {
283
+ /** Sponsor address. */
284
+ address: Address
285
+ /** Sponsor display name. */
286
+ name?: string | undefined
287
+ /** Sponsor display URL. */
288
+ url?: string | undefined
289
+ }
290
+ }
291
+
292
+ /** Returns whether the fee payer approves a filled transaction. */
293
+ // biome-ignore lint/correctness/noUnusedVariables: declaration merge
294
+ async function shouldSponsor(options: shouldSponsor.Options) {
295
+ const { sender, transaction, validate } = options
296
+ if (!validate) return true
297
+ // Named refusal reasons collapse to false: fills fall back to an
298
+ // unsponsored fill instead of surfacing the reason.
299
+ const verdict = await validate({
300
+ ...transaction,
301
+ from: sender,
302
+ } as TransactionRequest.TransactionRequest)
303
+ return verdict === true
304
+ }
305
+
306
+ declare namespace shouldSponsor {
307
+ type Options = {
308
+ /** Sender address from the original request. */
309
+ sender?: Address | undefined
310
+ /** Filled transaction to validate. */
311
+ transaction: Record<string, unknown>
312
+ /** Optional sponsorship approval callback. */
313
+ validate?: Validate | undefined
314
+ }
315
+ }
316
+
317
+ /** Returns whether a raw Tempo transaction is explicitly requesting sponsorship. */
318
+ function requestsRawSponsorship(serialized: `0x${string}`) {
319
+ if (!Utils.isSerializedTempoTransaction(serialized)) return false
320
+ const transaction = Transaction.deserialize(serialized)
321
+ return (
322
+ 'feePayerSignature' in transaction && transaction.feePayerSignature === null
323
+ )
324
+ }
325
+
326
+ /** Returns `true` when a fill request already has the fields needed for sponsorship signing. */
327
+ function isPreparedTransaction(value: Record<string, unknown>) {
328
+ return (
329
+ typeof value.from === 'string' &&
330
+ typeof Utils.resolveChainId(value.chainId) === 'number' &&
331
+ typeof value.gas !== 'undefined' &&
332
+ typeof value.nonce !== 'undefined' &&
333
+ (typeof value.maxFeePerGas !== 'undefined' ||
334
+ typeof value.gasPrice !== 'undefined')
335
+ )
336
+ }
337
+
338
+ /** Signs a filled transaction as the fee payer. */
339
+ export async function sign(options: sign.Options) {
340
+ const { account, transaction, sender } = options
341
+ const from = (transaction.from as Address | undefined) ?? sender
342
+ const { signature: _, ...withoutSenderSig } = transaction
343
+ const prepared = { ...withoutSenderSig, from }
344
+
345
+ if (!prepared.from)
346
+ throw new RpcResponse.InvalidParamsError({
347
+ message: 'Transaction sender must be provided before fee payer signing.',
348
+ })
349
+ if (!account.sign)
350
+ throw new Error('Fee payer account cannot sign transactions.')
351
+
352
+ const chainId = Utils.resolveChainId(transaction.chainId)
353
+ if (chainId === undefined)
354
+ throw new RpcResponse.InvalidParamsError({
355
+ message: 'Transaction chainId must be provided before fee payer signing.',
356
+ })
357
+
358
+ const envelope = TxEnvelopeTempo.from(prepared as never)
359
+ const signPayload = TxEnvelopeTempo.getFeePayerSignPayload(envelope, {
360
+ sender: prepared.from,
361
+ })
362
+ options.signal?.throwIfAborted()
363
+ const feePayerSignature = Signature.from(
364
+ await account.sign({ hash: signPayload }),
365
+ )
366
+ options.signal?.throwIfAborted()
367
+
368
+ // Record the commitment before releasing the signature, since the signed fill can be broadcast through another endpoint.
369
+ const feeToken = transaction.feeToken as Address | null | undefined
370
+ const sponsorshipDetails = await options.onSponsored?.({
371
+ chainId,
372
+ ...(feeToken ? { feeToken } : {}),
373
+ method: 'eth_fillTransaction',
374
+ sender: prepared.from,
375
+ signPayload,
376
+ transaction: TxEnvelopeTempo.serialize(envelope, { feePayerSignature }),
377
+ })
378
+
379
+ return {
380
+ ...(sponsorshipDetails ? { sponsorshipDetails } : {}),
381
+ transaction: { ...prepared, feePayerSignature },
382
+ }
383
+ }
384
+
385
+ export declare namespace sign {
386
+ type Options = {
387
+ /** Abort signal for the enclosing fill. */
388
+ signal?: AbortSignal | undefined
389
+ /** Account used as the fee payer. */
390
+ account: LocalAccount
391
+ /** Called once the fee payer has signed the fill. Awaited: a throw aborts the fill. */
392
+ onSponsored?:
393
+ | ((
394
+ event: SponsoredEvent,
395
+ ) => SponsorshipDetails | Promise<SponsorshipDetails | void> | void)
396
+ | undefined
397
+ /** Filled transaction to sign. */
398
+ transaction: Record<string, unknown>
399
+ /** Sender address from the original request. */
400
+ sender?: Address | undefined
401
+ }
402
+ }
403
+
404
+ /** Handles `eth_signRawTransaction` and broadcast methods for sponsored Tempo transactions. */
405
+ // biome-ignore lint/correctness/noUnusedVariables: declaration merge
406
+ async function handleRawTransaction(options: handleRawTransaction.Options) {
407
+ const {
408
+ account,
409
+ feeToken: sponsorFeeToken,
410
+ getClient,
411
+ method,
412
+ request,
413
+ validate,
414
+ } = options
415
+ const serialized = request.params?.[0] as `0x76${string}` | undefined
416
+
417
+ if (!Utils.isSerializedTempoTransaction(serialized))
418
+ throw new RpcResponse.InvalidParamsError({
419
+ message: 'Only Tempo (0x76/0x78) transactions are supported.',
420
+ })
421
+
422
+ const transaction = Transaction.deserialize(serialized)
423
+ // Prefer sender recovered from raw envelope; multisig finalize path supplies fallback sender.
424
+ const sender = transaction.from ?? options.sender
425
+
426
+ // Sponsorship only applies after sender has signed original transaction.
427
+ if (!transaction.signature || !sender)
428
+ throw new RpcResponse.InvalidParamsError({
429
+ message:
430
+ 'Transaction must be signed by the sender before fee payer signing.',
431
+ })
432
+ if (!account.sign)
433
+ throw new Error('Fee payer account cannot sign transactions.')
434
+
435
+ const client = getClient(transaction.chainId)
436
+ const feeToken_chain = (
437
+ client.chain as { feeToken?: Address | undefined } | undefined
438
+ )?.feeToken
439
+ const feeToken =
440
+ (transaction.feeToken as Address | null | undefined) ??
441
+ sponsorFeeToken ??
442
+ (await options.getFeeToken?.(transaction.chainId)) ??
443
+ feeToken_chain
444
+ const transaction_sponsored = feeToken
445
+ ? { ...transaction, feeToken }
446
+ : transaction
447
+
448
+ if (validate) {
449
+ const verdict = await validate(
450
+ transaction_sponsored as TransactionRequest.TransactionRequest,
451
+ )
452
+ if (verdict !== true)
453
+ throw new RpcResponse.InvalidParamsError(
454
+ verdict === false
455
+ ? { message: 'Sponsorship rejected.' }
456
+ : { data: { code: verdict }, message: refusalMessages[verdict] },
457
+ )
458
+ }
459
+
460
+ const envelope = TxEnvelopeTempo.from(transaction_sponsored as never)
461
+ const signPayload = TxEnvelopeTempo.getFeePayerSignPayload(envelope, {
462
+ sender,
463
+ })
464
+ const feePayerSignature = Signature.from(
465
+ await account.sign({ hash: signPayload }),
466
+ )
467
+ const serializedTransaction = TxEnvelopeTempo.serialize(envelope, {
468
+ feePayerSignature,
469
+ signature: transaction.signature,
470
+ })
471
+
472
+ // Awaited before any broadcast so a recording failure refuses sponsorship;
473
+ // no sponsored transaction ever reaches the chain unrecorded.
474
+ const sponsorshipDetails = await options.onSponsored?.({
475
+ chainId: transaction.chainId,
476
+ ...(feeToken ? { feeToken } : {}),
477
+ method,
478
+ sender,
479
+ signPayload,
480
+ transaction: serializedTransaction,
481
+ transactionHash: Hash.keccak256(serializedTransaction),
482
+ })
483
+
484
+ // Raw-sign requests stop after fee-payer signature is added; send methods broadcast it.
485
+ const result =
486
+ method === 'eth_signRawTransaction'
487
+ ? serializedTransaction
488
+ : await client.request({
489
+ method: method as never,
490
+ params: [
491
+ serializedTransaction,
492
+ ...(request.params?.slice(1) ?? []),
493
+ ] as never,
494
+ })
495
+ return { result, ...(sponsorshipDetails ? { sponsorshipDetails } : {}) }
496
+ }
497
+
498
+ declare namespace handleRawTransaction {
499
+ type Options = {
500
+ /** Account used as the fee payer. */
501
+ account: LocalAccount
502
+ /** Optional token the fee payer prefers for sponsored raw transactions. */
503
+ feeToken?: Address | undefined
504
+ /** Optional fee-token resolver used when the raw envelope omits `feeToken`. */
505
+ getFeeToken?:
506
+ | ((chainId: number) => Promise<Address | undefined>)
507
+ | undefined
508
+ /** Client resolver keyed by transaction `chainId`. */
509
+ getClient: (chainId?: number) => Client
510
+ /** Raw transaction method to handle. */
511
+ method:
512
+ | 'eth_signRawTransaction'
513
+ | 'eth_sendRawTransaction'
514
+ | 'eth_sendRawTransactionSync'
515
+ /** Called once the fee payer has signed, before any broadcast. Awaited: a throw aborts the request. */
516
+ onSponsored?:
517
+ | ((
518
+ event: SponsoredEvent,
519
+ ) => SponsorshipDetails | Promise<SponsorshipDetails | void> | void)
520
+ | undefined
521
+ /** Incoming JSON-RPC request. */
522
+ request: { params?: readonly unknown[] | undefined }
523
+ /** Sender address to use if it cannot be recovered from the raw envelope. */
524
+ sender?: Address | undefined
525
+ /** Optional sponsorship approval callback. */
526
+ validate?: Validate | undefined
527
+ }
528
+ }
529
+
530
+ /** Facts of one sponsorship commitment, emitted at fee-payer signing time. */
531
+ export type SponsoredEvent = {
532
+ /** Chain the sponsored transaction targets. */
533
+ chainId: number
534
+ /** Fee token the sponsorship resolved, when known. */
535
+ feeToken?: Address | undefined
536
+ /** Method that triggered sponsorship: a fill (intent) or a raw submission. */
537
+ method:
538
+ | 'eth_fillTransaction'
539
+ | 'eth_signRawTransaction'
540
+ | 'eth_sendRawTransaction'
541
+ | 'eth_sendRawTransactionSync'
542
+ /** Transaction sender. */
543
+ sender: Address
544
+ /** Fee-payer sign payload: a stable identity for the sponsored envelope. */
545
+ signPayload: Hex.Hex
546
+ /** Serialized sponsored transaction (sender-unsigned for fill intents). */
547
+ transaction: Hex.Hex
548
+ /** Transaction hash (keccak256 of the signed envelope); absent for fill intents, whose senders have not signed yet. */
549
+ transactionHash?: Hex.Hex | undefined
550
+ }
551
+
552
+ export namespace ExternalFeePayerUrl {
553
+ type Options = {
554
+ /** Whether to allow non-HTTPS or local/private external fee payer URLs. */
555
+ allowUnsafe?: boolean | undefined
556
+ }
557
+
558
+ /** Normalizes external fee-payer relay URLs before forwarding fill payloads. */
559
+ export function normalize(value: string, options: Options = {}): string {
560
+ let url: URL
561
+ try {
562
+ url = new URL(value)
563
+ } catch {
564
+ throw new RpcResponse.InvalidParamsError({
565
+ message: 'Invalid fee payer URL.',
566
+ })
567
+ }
568
+
569
+ const allowUnsafe = options.allowUnsafe === true
570
+ if (url.protocol !== 'http:' && url.protocol !== 'https:')
571
+ throw new RpcResponse.InvalidParamsError({
572
+ message: 'Invalid fee payer URL protocol.',
573
+ })
574
+ if (!allowUnsafe && url.protocol !== 'https:')
575
+ throw new RpcResponse.InvalidParamsError({
576
+ message: 'Invalid fee payer URL protocol.',
577
+ })
578
+ if (!allowUnsafe && isUnsafeHostname(url.hostname))
579
+ throw new RpcResponse.InvalidParamsError({
580
+ message: 'Invalid fee payer URL host.',
581
+ })
582
+
583
+ url.username = ''
584
+ url.password = ''
585
+ url.hash = ''
586
+ return url.href
587
+ }
588
+
589
+ function isUnsafeHostname(hostname: string): boolean {
590
+ const host = normalizeHostname(hostname)
591
+ if (host === 'localhost' || host.endsWith('.localhost')) return true
592
+ const ipv4 = parseIpv4(host)
593
+ if (ipv4) return isUnsafeIpv4(ipv4)
594
+ if (!host.includes(':')) return false
595
+ const ipv6 = parseIpv6(host)
596
+ if (!ipv6) return true
597
+ return isUnsafeIpv6(ipv6)
598
+ }
599
+
600
+ function normalizeHostname(hostname: string): string {
601
+ return hostname
602
+ .toLowerCase()
603
+ .replace(/^\[|\]$/g, '')
604
+ .split('%')[0]!
605
+ }
606
+
607
+ function parseIpv4(
608
+ value: string,
609
+ ): [number, number, number, number] | undefined {
610
+ const parts = value.split('.')
611
+ if (parts.length !== 4 || parts.some((part) => !/^\d+$/.test(part)))
612
+ return undefined
613
+ const numbers = parts.map((part) => Number(part))
614
+ if (
615
+ numbers.some((part) => !Number.isInteger(part) || part < 0 || part > 255)
616
+ )
617
+ return undefined
618
+ return numbers as [number, number, number, number]
619
+ }
620
+
621
+ function isUnsafeIpv4(parts: [number, number, number, number]): boolean {
622
+ const [a, b] = parts
623
+ if (a === 0 || a === 10 || a === 127) return true
624
+ if (a === 100 && b >= 64 && b <= 127) return true
625
+ if (a === 169 && b === 254) return true
626
+ if (a === 172 && b >= 16 && b <= 31) return true
627
+ if (a === 192 && b === 168) return true
628
+ return a >= 224
629
+ }
630
+
631
+ function parseIpv6(
632
+ value: string,
633
+ ):
634
+ | [number, number, number, number, number, number, number, number]
635
+ | undefined {
636
+ const halves = value.split('::')
637
+ if (halves.length > 2) return undefined
638
+
639
+ const left = halves[0] ? halves[0].split(':') : []
640
+ const right = halves[1] ? halves[1].split(':') : []
641
+ const missing = halves.length === 2 ? 8 - left.length - right.length : 0
642
+ if (missing < 0) return undefined
643
+
644
+ const parts = [...left, ...Array(missing).fill('0'), ...right]
645
+ if (parts.length !== 8) return undefined
646
+
647
+ const numbers = parts.map((part) =>
648
+ /^[\da-f]{1,4}$/i.test(part) ? Number.parseInt(part, 16) : NaN,
649
+ )
650
+ if (
651
+ numbers.some(
652
+ (part) => !Number.isInteger(part) || part < 0 || part > 0xffff,
653
+ )
654
+ )
655
+ return undefined
656
+ return numbers as [
657
+ number,
658
+ number,
659
+ number,
660
+ number,
661
+ number,
662
+ number,
663
+ number,
664
+ number,
665
+ ]
666
+ }
667
+
668
+ function isUnsafeIpv6(
669
+ parts: [number, number, number, number, number, number, number, number],
670
+ ) {
671
+ const [a] = parts
672
+ if (parts.every((part) => part === 0)) return true
673
+ if (parts.slice(0, 7).every((part) => part === 0) && parts[7] === 1)
674
+ return true
675
+ if ((a & 0xfe00) === 0xfc00) return true
676
+ if ((a & 0xffc0) === 0xfe80) return true
677
+ if ((a & 0xffc0) === 0xfec0) return true
678
+ if ((a & 0xff00) === 0xff00) return true
679
+
680
+ const ipv4 = ipv4FromIpv6(parts)
681
+ return ipv4 ? isUnsafeIpv4(ipv4) : false
682
+ }
683
+
684
+ function ipv4FromIpv6(
685
+ parts: [number, number, number, number, number, number, number, number],
686
+ ): [number, number, number, number] | undefined {
687
+ const mapped =
688
+ parts.slice(0, 5).every((part) => part === 0) && parts[5] === 0xffff
689
+ const compatible = parts.slice(0, 6).every((part) => part === 0)
690
+ if (!mapped && !compatible) return undefined
691
+ return [parts[6]! >> 8, parts[6]! & 0xff, parts[7]! >> 8, parts[7]! & 0xff]
692
+ }
693
+ }