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