@x402r/refund 0.0.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/src/index.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Refund Helper Extension for x402
3
+ *
4
+ * Enables merchants to route payments to DepositRelay contracts via escrow,
5
+ * providing refund and dispute resolution capabilities.
6
+ *
7
+ * ## Overview
8
+ *
9
+ * The refund helper allows merchants to mark payment options as refundable,
10
+ * which routes payments through DepositRelay contracts into escrow accounts.
11
+ * This enables dispute resolution and refunds even if merchants are uncooperative.
12
+ *
13
+ * ## For Merchants (Server-Side)
14
+ *
15
+ * ### Step 1: Mark Payment Options as Refundable
16
+ *
17
+ * Use `refundable()` to mark payment options that should support refunds:
18
+ *
19
+ * ```typescript
20
+ * import { refundable } from '@x402r/extensions/refund';
21
+ *
22
+ * const option = refundable({
23
+ * scheme: 'exact',
24
+ * payTo: '0xmerchant123...', // Your merchant payout address
25
+ * price: '$0.01',
26
+ * network: 'eip155:84532',
27
+ * });
28
+ * ```
29
+ *
30
+ * ### Step 2: Process Routes with DepositRelay
31
+ *
32
+ * Use `withRefund()` to process route configurations and route refundable
33
+ * payments to DepositRelay:
34
+ *
35
+ * ```typescript
36
+ * import { refundable, withRefund } from '@x402r/extensions/refund';
37
+ *
38
+ * const FACTORY_ADDRESS = '0xFactory123...'; // Any CREATE3-compatible factory
39
+ * const CREATEX_ADDRESS = '0xCreateX123...'; // CreateX contract address
40
+ *
41
+ * const routes = {
42
+ * '/api': {
43
+ * accepts: refundable({
44
+ * scheme: 'exact',
45
+ * payTo: '0xmerchant123...',
46
+ * price: '$0.01',
47
+ * network: 'eip155:84532',
48
+ * }),
49
+ * },
50
+ * };
51
+ *
52
+ * // Process routes to route refundable payments to DepositRelay
53
+ * // Uses CREATE3 - no bytecode needed! Works with any CREATE3-compatible factory.
54
+ * // Version is optional - defaults to config file value
55
+ * // CreateX address is optional - uses standard address for network if not provided
56
+ * const processedRoutes = withRefund(routes, FACTORY_ADDRESS);
57
+ *
58
+ * // Use processedRoutes with paymentMiddleware
59
+ * app.use(paymentMiddleware(processedRoutes, server));
60
+ * ```
61
+ *
62
+ * ## For Facilitators
63
+ *
64
+ * Facilitators use `settleWithRefundHelper()` in hooks to handle refund settlements:
65
+ *
66
+ * ```typescript
67
+ * import { settleWithRefundHelper } from '@x402r/extensions/refund';
68
+ * import { x402Facilitator } from '@x402/core/facilitator';
69
+ *
70
+ * const ESCROW_FACTORY = '0xEscrowFactory123...';
71
+ *
72
+ * facilitator.onBeforeSettle(async (context) => {
73
+ * const result = await settleWithRefundHelper(
74
+ * context.paymentPayload,
75
+ * context.paymentRequirements,
76
+ * signer,
77
+ * ESCROW_FACTORY,
78
+ * );
79
+ *
80
+ * if (result) {
81
+ * // Refund was handled via DepositRelay
82
+ * return { abort: true, reason: 'handled_by_refund_helper' };
83
+ * }
84
+ *
85
+ * return null; // Proceed with normal settlement
86
+ * });
87
+ * ```
88
+ *
89
+ * ## How It Works
90
+ *
91
+ * 1. **Merchant Setup**: Merchant deploys escrow via EscrowFactory and marks options with `refundable()`
92
+ * 2. **Route Processing**: `withRefund()` sets `payTo` to DepositRelay address, stores original merchantPayout in `extra`
93
+ * 3. **Client Payment**: Client makes payment to DepositRelay address (transparent to client)
94
+ * 4. **Facilitator Settlement**: Facilitator detects refund payment, queries EscrowFactory for escrow, calls DepositRelay.executeDeposit()
95
+ * 5. **Escrow Hold**: Funds are held in escrow, enabling dispute resolution and refunds
96
+ *
97
+ * ## Key Features
98
+ *
99
+ * - **No Core Changes**: Works entirely through helpers and hooks
100
+ * - **Client Transparent**: Clients don't need to change anything
101
+ * - **Flexible**: Mix refundable and non-refundable options in same route
102
+ * - **Deep Cloning**: All helpers return new objects, don't mutate originals
103
+ */
104
+
105
+ // Export types
106
+ export {
107
+ REFUND_EXTENSION_KEY,
108
+ REFUND_MARKER_KEY,
109
+ isRefundableOption,
110
+ type RefundExtension,
111
+ type RefundExtensionInfo,
112
+ } from "./types";
113
+
114
+ // Export server-side helpers
115
+ export { declareRefundExtension, refundable, withRefund } from "./server";
116
+ export { computeRelayAddress } from "./server/computeRelayAddress";
117
+
118
+ // Export facilitator-side helpers
119
+ export { extractRefundInfo, settleWithRefundHelper } from "./facilitator";
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Helper to compute CREATE3 address for RelayProxy
3
+ *
4
+ * Uses the CREATE3 formula via CreateX (matching Solidity implementation):
5
+ *
6
+ * Where:
7
+ * - salt = keccak256(abi.encodePacked(factoryAddress, merchantPayout))
8
+ * - guardedSalt = keccak256(abi.encode(salt)) // CreateX guards the salt
9
+ * - createxDeployer = the CreateX contract address
10
+ *
11
+ * CREATE3 is much simpler than CREATE2 - no bytecode needed!
12
+ * The address depends only on the deployer (CreateX) and salt.
13
+ *
14
+ * IMPORTANT: The CreateX address must match the one used by the factory contract.
15
+ * The factory stores its CreateX address and can be queried via factory.getCreateX().
16
+ * This function computes addresses locally without any on-chain calls.
17
+ */
18
+
19
+ import { keccak256, encodePacked, encodeAbiParameters, getAddress } from "viem";
20
+ import { predictCreate3Address } from "@whoislewys/predict-deterministic-address";
21
+
22
+ /**
23
+ * Computes the CREATE3 address for a merchant's relay proxy
24
+ *
25
+ * This matches the Solidity implementation in DepositRelayFactory.getRelayAddress():
26
+ * 1. salt = keccak256(abi.encodePacked(merchantPayout))
27
+ * 2. guardedSalt = keccak256(abi.encode(salt)) // CreateX guards the salt
28
+ * 3. return CREATEX.computeCreate3Address(guardedSalt)
29
+ *
30
+ * Uses the @whoislewys/predict-deterministic-address library which correctly
31
+ * implements the CREATE3 formula used by CreateX (based on Solady's CREATE3).
32
+ * This ensures the computed address matches the factory's on-chain computation
33
+ * without requiring any on-chain calls.
34
+ *
35
+ * @param createxAddress - The CreateX contract address
36
+ * @param factoryAddress - The DepositRelayFactory contract address
37
+ * @param merchantPayout - The merchant's payout address
38
+ * @returns The deterministic proxy address
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // No bytecode needed! Computes locally without on-chain calls.
43
+ * const relayAddress = computeRelayAddress(
44
+ * "0xCreateX123...",
45
+ * "0xMerchant123...",
46
+ * 0n // version
47
+ * );
48
+ * ```
49
+ */
50
+ export function computeRelayAddress(
51
+ createxAddress: string,
52
+ factoryAddress: string,
53
+ merchantPayout: string,
54
+ ): string {
55
+ // Normalize addresses to checksummed format
56
+ const createx = getAddress(createxAddress);
57
+ const factory = getAddress(factoryAddress);
58
+ const merchant = getAddress(merchantPayout);
59
+
60
+ // Step 1: salt = keccak256(abi.encodePacked(factoryAddress, merchantPayout))
61
+ const salt = keccak256(encodePacked(["address", "address"], [factory, merchant]));
62
+
63
+ // Step 2: guardedSalt = keccak256(abi.encode(salt))
64
+ // Note: abi.encode (not encodePacked) - this adds length prefixes
65
+ const guardedSalt = keccak256(
66
+ encodeAbiParameters([{ type: "bytes32" }], [salt as `0x${string}`]),
67
+ );
68
+
69
+ // Step 3: Use predictCreate3Address to compute the CREATE3 address
70
+ // This matches CreateX's computeCreate3Address implementation exactly
71
+ // No on-chain calls needed - computes locally!
72
+ return predictCreate3Address(createx, guardedSalt as `0x${string}`);
73
+ }
package/src/server.ts ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Server-side helpers for the Refund Helper Extension
3
+ *
4
+ * These helpers allow merchants to mark payment options as refundable
5
+ * and process route configurations to route payments to X402DepositRelayProxy contracts.
6
+ */
7
+
8
+ import type { PaymentOption, RouteConfig, RoutesConfig } from "@x402/core/http";
9
+ import {
10
+ REFUND_MARKER_KEY,
11
+ isRefundableOption,
12
+ REFUND_EXTENSION_KEY,
13
+ type RefundExtension,
14
+ } from "./types";
15
+ import { computeRelayAddress } from "./server/computeRelayAddress";
16
+
17
+ /**
18
+ * Declares a refund extension with factory address and merchantPayouts map
19
+ *
20
+ * @param factoryAddress - The X402DepositRelayFactory contract address
21
+ * @param merchantPayouts - Map of proxy address to merchant payout address
22
+ * @returns Refund extension object with info and schema
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const extension = declareRefundExtension("0xFactory123...", {
27
+ * "0xProxy1...": "0xMerchant1...",
28
+ * "0xProxy2...": "0xMerchant2...",
29
+ * });
30
+ * ```
31
+ */
32
+ export function declareRefundExtension(
33
+ factoryAddress: string,
34
+ merchantPayouts: Record<string, string>,
35
+ ): Record<string, RefundExtension> {
36
+ return {
37
+ [REFUND_EXTENSION_KEY]: {
38
+ info: {
39
+ factoryAddress,
40
+ merchantPayouts,
41
+ },
42
+ schema: {
43
+ $schema: "https://json-schema.org/draft/2020-12/schema",
44
+ type: "object",
45
+ properties: {
46
+ factoryAddress: {
47
+ type: "string",
48
+ pattern: "^0x[a-fA-F0-9]{40}$",
49
+ description: "The X402DepositRelayFactory contract address",
50
+ },
51
+ merchantPayouts: {
52
+ type: "object",
53
+ additionalProperties: {
54
+ type: "string",
55
+ pattern: "^0x[a-fA-F0-9]{40}$",
56
+ },
57
+ description: "Map of proxy address to merchant payout address",
58
+ },
59
+ },
60
+ required: ["factoryAddress", "merchantPayouts"],
61
+ additionalProperties: false,
62
+ },
63
+ },
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Marks a payment option as refundable.
69
+ *
70
+ * This function marks the option as refundable so it can be processed by `withRefund()`.
71
+ * The merchantPayout is read directly from the option's `payTo` field when processing.
72
+ *
73
+ * @param option - The payment option to mark as refundable
74
+ * @returns A new PaymentOption marked as refundable (does not mutate original)
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const refundableOption = refundable({
79
+ * scheme: "exact",
80
+ * payTo: "0xmerchant123...",
81
+ * price: "$0.01",
82
+ * network: "eip155:84532",
83
+ * });
84
+ * // refundableOption.extra._x402_refund = true
85
+ * ```
86
+ */
87
+ export function refundable(option: PaymentOption): PaymentOption {
88
+ // Deep clone the option to avoid mutation
89
+ const clonedOption: PaymentOption = {
90
+ ...option,
91
+ extra: {
92
+ ...option.extra,
93
+ },
94
+ };
95
+
96
+ // Set marker to indicate this option is refundable
97
+ if (!clonedOption.extra) {
98
+ clonedOption.extra = {};
99
+ }
100
+
101
+ clonedOption.extra[REFUND_MARKER_KEY] = true;
102
+
103
+ return clonedOption;
104
+ }
105
+
106
+ /**
107
+ * Standard CreateX contract addresses per network.
108
+ * These are the official CreateX deployments from https://github.com/pcaversaccio/createx#createx-deployments
109
+ *
110
+ * Note: If a network is not listed here, CreateX may need to be deployed separately.
111
+ * The factory stores the CreateX address and can be queried via factory.getCreateX().
112
+ */
113
+ const STANDARD_CREATEX_ADDRESSES: Record<string, string> = {
114
+ // Ethereum Mainnet
115
+ "eip155:1": "0xba5Ed099633D3B313e4D5F7bdc1305d3c32ba066",
116
+ // Base Mainnet
117
+ "eip155:8453": "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed",
118
+ // Base Sepolia
119
+ "eip155:84532": "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed",
120
+ // Add more networks as CreateX deployments become available
121
+ };
122
+
123
+ /**
124
+ * Processes route configuration to handle refundable payment options.
125
+ *
126
+ * This function finds all payment options marked with `refundable()` and:
127
+ * 1. Computes the proxy address using CREATE3 (no bytecode needed!)
128
+ * 2. Sets `payTo` to the proxy address
129
+ * 3. Adds the refund extension with factory address
130
+ *
131
+ * @param routes - Route configuration (single RouteConfig or Record<string, RouteConfig>)
132
+ * @param factoryAddress - The X402DepositRelayFactory contract address (required)
133
+ * @param createxAddress - The CreateX contract address (optional, will use standard address for network if not provided)
134
+ * @returns A new RoutesConfig with refundable options routed to proxy (deep cloned, does not mutate original)
135
+ *
136
+ * @example
137
+ * ```typescript
138
+ * const routes = {
139
+ * "/api": {
140
+ * accepts: refundable({
141
+ * scheme: "exact",
142
+ * payTo: "0xmerchant123...",
143
+ * price: "$0.01",
144
+ * network: "eip155:84532",
145
+ * }),
146
+ * },
147
+ * };
148
+ *
149
+ * // Version is optional - defaults to config file value
150
+ * // CreateX address is optional - uses standard address for the network
151
+ * const processedRoutes = withRefund(routes, "0xFactory123...");
152
+ * // processedRoutes["/api"].accepts.payTo = computed proxy address
153
+ * // processedRoutes["/api"].extensions.refund = { info: { factoryAddress: "0xFactory123..." }, schema: {...} }
154
+ * ```
155
+ */
156
+ export function withRefund(
157
+ routes: RoutesConfig,
158
+ factoryAddress: string,
159
+ createxAddress?: string,
160
+ ): RoutesConfig {
161
+ // Deep clone to avoid mutation
162
+ if (typeof routes === "object" && routes !== null && !("accepts" in routes)) {
163
+ // Nested RoutesConfig: Record<string, RouteConfig>
164
+ const nestedRoutes = routes as Record<string, RouteConfig>;
165
+ const processedRoutes: Record<string, RouteConfig> = {};
166
+
167
+ for (const [pattern, config] of Object.entries(nestedRoutes)) {
168
+ processedRoutes[pattern] = processRouteConfig(config, factoryAddress, createxAddress);
169
+ }
170
+
171
+ return processedRoutes;
172
+ } else {
173
+ // Single RouteConfig
174
+ return processRouteConfig(routes as RouteConfig, factoryAddress, createxAddress);
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Gets the CreateX address for a given network.
180
+ * First checks if provided explicitly, then falls back to standard addresses.
181
+ *
182
+ * @param network - The network identifier (e.g., "eip155:84532")
183
+ * @param providedAddress - Optional CreateX address provided by user
184
+ * @returns The CreateX address to use
185
+ * @throws Error if no CreateX address can be determined
186
+ */
187
+ function getCreateXAddress(network: string, providedAddress?: string): string {
188
+ if (providedAddress) {
189
+ return providedAddress;
190
+ }
191
+
192
+ const standardAddress = STANDARD_CREATEX_ADDRESSES[network];
193
+ if (standardAddress) {
194
+ return standardAddress;
195
+ }
196
+
197
+ throw new Error(
198
+ `CreateX address not provided and no standard address found for network ${network}. ` +
199
+ `Please provide createxAddress parameter or check if CreateX is deployed on this network. ` +
200
+ `See https://github.com/pcaversaccio/createx#createx-deployments for standard deployments.`,
201
+ );
202
+ }
203
+
204
+ /**
205
+ * Processes a single RouteConfig to transform refundable payment options.
206
+ *
207
+ * @param config - The route configuration to process
208
+ * @param factoryAddress - The X402DepositRelayFactory contract address
209
+ * @param createxAddress - The CreateX contract address (optional)
210
+ * @returns A new RouteConfig with refundable options transformed
211
+ */
212
+ function processRouteConfig(
213
+ config: RouteConfig,
214
+ factoryAddress: string,
215
+ createxAddress?: string,
216
+ ): RouteConfig {
217
+ // Get the network from the first payment option to determine CreateX address
218
+ const firstOption = Array.isArray(config.accepts) ? config.accepts[0] : config.accepts;
219
+ const network = firstOption?.network;
220
+
221
+ if (!network) {
222
+ throw new Error("Payment option must have a network field to determine CreateX address");
223
+ }
224
+
225
+ // Get CreateX address (from parameter or standard mapping)
226
+ const resolvedCreatexAddress = getCreateXAddress(network, createxAddress);
227
+
228
+ // Check if any option is refundable
229
+ const hasRefundable = Array.isArray(config.accepts)
230
+ ? config.accepts.some(isRefundableOption)
231
+ : isRefundableOption(config.accepts);
232
+
233
+ // Build map of proxyAddress -> merchantPayout BEFORE processing options
234
+ // This allows us to store all merchantPayouts even if there are multiple refundable options
235
+ const merchantPayoutsMap: Record<string, string> = {};
236
+
237
+ if (hasRefundable) {
238
+ const refundableOptions = Array.isArray(config.accepts)
239
+ ? config.accepts.filter(isRefundableOption)
240
+ : isRefundableOption(config.accepts)
241
+ ? [config.accepts]
242
+ : [];
243
+
244
+ // Collect merchantPayouts from original options before processing
245
+ for (const option of refundableOptions) {
246
+ if (typeof option.payTo === "string") {
247
+ const merchantPayout = option.payTo; // Original merchantPayout (before we overwrite it)
248
+ const proxyAddress = computeRelayAddress(
249
+ resolvedCreatexAddress,
250
+ factoryAddress,
251
+ merchantPayout,
252
+ );
253
+ merchantPayoutsMap[proxyAddress.toLowerCase()] = merchantPayout;
254
+ }
255
+ }
256
+ }
257
+
258
+ // Deep clone the config and process options
259
+ const processedConfig: RouteConfig = {
260
+ ...config,
261
+ accepts: Array.isArray(config.accepts)
262
+ ? config.accepts.map(option =>
263
+ processPaymentOption(option, factoryAddress, resolvedCreatexAddress),
264
+ )
265
+ : processPaymentOption(config.accepts, factoryAddress, resolvedCreatexAddress),
266
+ extensions: {
267
+ ...config.extensions,
268
+ },
269
+ };
270
+
271
+ // Add refund extension if any option is refundable
272
+ if (hasRefundable && Object.keys(merchantPayoutsMap).length > 0) {
273
+ processedConfig.extensions = {
274
+ ...processedConfig.extensions,
275
+ ...declareRefundExtension(factoryAddress, merchantPayoutsMap),
276
+ };
277
+ } else if (hasRefundable) {
278
+ throw new Error(
279
+ "Refundable option must have a string payTo address. DynamicPayTo is not supported for refundable options.",
280
+ );
281
+ }
282
+
283
+ return processedConfig;
284
+ }
285
+
286
+ /**
287
+ * Processes a single PaymentOption to transform it if refundable.
288
+ *
289
+ * @param option - The payment option to process
290
+ * @param factoryAddress - The X402DepositRelayFactory contract address
291
+ * @param createxAddress - The CreateX contract address (required)
292
+ * @returns A new PaymentOption (transformed if refundable, unchanged otherwise)
293
+ */
294
+ function processPaymentOption(
295
+ option: PaymentOption,
296
+ factoryAddress: string,
297
+ createxAddress: string,
298
+ ): PaymentOption {
299
+ // Check if option is refundable
300
+ if (isRefundableOption(option)) {
301
+ // Read merchantPayout directly from payTo field (before we overwrite it)
302
+ const merchantPayout = option.payTo;
303
+
304
+ // If it's a function (DynamicPayTo), we can't compute the address (would need to call it)
305
+ // For now, require it to be a string
306
+ if (typeof merchantPayout !== "string") {
307
+ throw new Error(
308
+ "DynamicPayTo is not supported for refundable options. Use a static address.",
309
+ );
310
+ }
311
+
312
+ // Compute proxy address using CREATE3 (no bytecode needed!)
313
+ const proxyAddress = computeRelayAddress(createxAddress, factoryAddress, merchantPayout);
314
+
315
+ // Deep clone the option
316
+ const processedOption: PaymentOption = {
317
+ ...option,
318
+ payTo: proxyAddress, // Set payTo to proxy address
319
+ extra: {
320
+ ...option.extra,
321
+ },
322
+ };
323
+
324
+ // Remove the marker since we've processed it
325
+ if (processedOption.extra) {
326
+ delete processedOption.extra[REFUND_MARKER_KEY];
327
+ }
328
+
329
+ return processedOption;
330
+ }
331
+
332
+ // Not refundable, return as-is (still clone to avoid mutation)
333
+ return { ...option, extra: { ...option.extra } };
334
+ }
package/src/types.ts ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Type definitions for the Refund Helper Extension
3
+ */
4
+
5
+ import type { PaymentOption } from "@x402/core/http";
6
+
7
+ /**
8
+ * Extension identifier constant for the refund extension
9
+ */
10
+ export const REFUND_EXTENSION_KEY = "refund";
11
+
12
+ /**
13
+ * Constant for the refund marker key (internal marker)
14
+ * Used to identify refundable payment options
15
+ * The merchantPayout is read directly from the option's payTo field when processing
16
+ */
17
+ export const REFUND_MARKER_KEY = "_x402_refund";
18
+
19
+ /**
20
+ * Refund extension info structure
21
+ *
22
+ * merchantPayouts: Map of proxy address -> merchantPayout
23
+ * This allows multiple refundable options with different merchantPayouts
24
+ */
25
+ export interface RefundExtensionInfo {
26
+ factoryAddress: string;
27
+ merchantPayouts: Record<string, string>; // proxyAddress -> merchantPayout
28
+ }
29
+
30
+ /**
31
+ * Refund extension structure (matches extension pattern with info and schema)
32
+ */
33
+ export interface RefundExtension {
34
+ info: RefundExtensionInfo;
35
+ schema: {
36
+ $schema: "https://json-schema.org/draft/2020-12/schema";
37
+ type: "object";
38
+ properties: {
39
+ factoryAddress: {
40
+ type: "string";
41
+ pattern: "^0x[a-fA-F0-9]{40}$";
42
+ description: "The X402DepositRelayFactory contract address";
43
+ };
44
+ merchantPayouts: {
45
+ type: "object";
46
+ additionalProperties: {
47
+ type: "string";
48
+ pattern: "^0x[a-fA-F0-9]{40}$";
49
+ };
50
+ description: "Map of proxy address to merchant payout address";
51
+ };
52
+ };
53
+ required: ["factoryAddress", "merchantPayouts"];
54
+ additionalProperties: false;
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Type guard to check if a payment option is refundable
60
+ * A refundable option has a marker stored in extra
61
+ *
62
+ * @param option - The payment option to check
63
+ * @returns True if the option is refundable
64
+ */
65
+ export function isRefundableOption(option: PaymentOption): boolean {
66
+ // Check for marker in extra
67
+ return (
68
+ option.extra !== undefined &&
69
+ typeof option.extra === "object" &&
70
+ option.extra !== null &&
71
+ REFUND_MARKER_KEY in option.extra &&
72
+ option.extra[REFUND_MARKER_KEY] === true
73
+ );
74
+ }