@m2msentinel/sdk 1.1.1 → 1.1.2

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.
@@ -0,0 +1,265 @@
1
+ export const BASE_USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
2
+ export const BASE_CHAIN_ID = 8453;
3
+ export const EXPECTED_PAYOUT_RECIPIENT = '0x6d6c398390cfb88f1cd42715b84906a0bd6652aa';
4
+ export const DEFAULT_MAX_PRICE_USD = 0.05;
5
+
6
+ export interface X402SignerClientOptions {
7
+ wallet?: any;
8
+ walletSigner?: any;
9
+ baseUrl?: string;
10
+ timeoutMs?: number;
11
+ expectedRecipient?: string;
12
+ maxPriceUsd?: number;
13
+ }
14
+
15
+ export interface X402PaymentChallenge {
16
+ x402Version?: number;
17
+ scheme?: string;
18
+ tokenName?: string;
19
+ tokenVersion?: string;
20
+ assetContract?: string;
21
+ chainId?: number | string;
22
+ payTo?: string;
23
+ recipient?: string;
24
+ amountUnits?: string;
25
+ maxAmountRequired?: string;
26
+ amount?: string;
27
+ price?: string;
28
+ [key: string]: any;
29
+ }
30
+
31
+ export interface X402SignedAuthorization {
32
+ x402Version: number;
33
+ scheme: string;
34
+ network: string;
35
+ token: string;
36
+ assetContract: string;
37
+ authorization: {
38
+ from: string;
39
+ to: string;
40
+ value: string;
41
+ validAfter: number;
42
+ validBefore: number;
43
+ nonce: string;
44
+ v: number;
45
+ r: string;
46
+ s: string;
47
+ signature: string;
48
+ };
49
+ }
50
+
51
+ export function parsePriceToUnits(priceStr: string | number, decimals: number = 6): bigint {
52
+ if (typeof priceStr === 'number') return BigInt(Math.round(priceStr * 10 ** decimals));
53
+ const clean = String(priceStr).replace(/[^0-9.]/g, '');
54
+ const [whole, fraction = ''] = clean.split('.');
55
+ const paddedFraction = (fraction + '0'.repeat(decimals)).slice(0, decimals);
56
+ return BigInt(whole || '0') * BigInt(10 ** decimals) + BigInt(paddedFraction);
57
+ }
58
+
59
+ export function parsePaymentHeader(value: string | null): any {
60
+ if (!value) return null;
61
+ try {
62
+ return JSON.parse(value);
63
+ } catch {
64
+ try {
65
+ const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
66
+ const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
67
+ if (typeof atob === 'function') {
68
+ return JSON.parse(atob(padded));
69
+ }
70
+ return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ }
76
+
77
+ export class X402SignerClient {
78
+ public readonly wallet: any;
79
+ public readonly baseUrl: string;
80
+ public readonly timeoutMs: number;
81
+ public readonly expectedRecipient: string;
82
+ public readonly maxPriceUsd: number;
83
+ public readonly maxAmountUnits: bigint;
84
+
85
+ constructor(options: X402SignerClientOptions = {}) {
86
+ this.wallet = options.wallet || options.walletSigner || null;
87
+ this.baseUrl = (options.baseUrl || 'https://api.m2msentinel.com').replace(/\/+$/, '');
88
+ this.timeoutMs = Number(options.timeoutMs || 30000);
89
+ this.expectedRecipient = EXPECTED_PAYOUT_RECIPIENT;
90
+ this.maxPriceUsd = options.maxPriceUsd !== undefined ? Number(options.maxPriceUsd) : DEFAULT_MAX_PRICE_USD;
91
+ this.maxAmountUnits = parsePriceToUnits(this.maxPriceUsd, 6);
92
+ }
93
+
94
+ async signPaymentAuthorization(challenge: X402PaymentChallenge = {}): Promise<X402SignedAuthorization> {
95
+ if (!this.wallet) {
96
+ throw new Error('Signer wallet is required to sign x402 payment authorization');
97
+ }
98
+
99
+ // 1. IMMUTABLE LOCAL SECURITY CONSTANTS (Never challenge-controlled)
100
+ const chainId = BASE_CHAIN_ID; // Base Mainnet (8453)
101
+ const tokenContract = BASE_USDC_CONTRACT; // Base USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
102
+ const payTo = EXPECTED_PAYOUT_RECIPIENT; // M2M Sentinel Payout (0x6d6c398390cfb88f1cd42715b84906a0bd6652aa)
103
+
104
+ // 2. STRICT CHALLENGE INTEGRITY CHECKS (Refuse if challenge alters network, asset, or recipient)
105
+ if (challenge.chainId && Number(challenge.chainId) !== BASE_CHAIN_ID) {
106
+ throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${challenge.chainId}. Autonomous signer strictly requires Base Mainnet (8453).`);
107
+ }
108
+ if (challenge.assetContract && challenge.assetContract.toLowerCase() !== BASE_USDC_CONTRACT.toLowerCase()) {
109
+ throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${challenge.assetContract}. Autonomous signer strictly requires Base USDC (${BASE_USDC_CONTRACT}).`);
110
+ }
111
+ if (challenge.payTo && challenge.payTo.toLowerCase() !== EXPECTED_PAYOUT_RECIPIENT.toLowerCase()) {
112
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected recipient: ${challenge.payTo}. Autonomous signer strictly requires ${EXPECTED_PAYOUT_RECIPIENT}.`);
113
+ }
114
+ if (challenge.tokenName && challenge.tokenName !== 'USD Coin') {
115
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${challenge.tokenName}. Expected USD Coin.`);
116
+ }
117
+ if (challenge.tokenVersion && challenge.tokenVersion !== '2') {
118
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${challenge.tokenVersion}. Expected 2.`);
119
+ }
120
+
121
+ // 3. STRICT LOCAL PRICE CEILING CHECK
122
+ const requestedAmountUnits = challenge.maxAmountRequired || challenge.amountUnits || parsePriceToUnits(challenge.amount || challenge.price || '$0.005', 6).toString();
123
+ if (BigInt(requestedAmountUnits) > this.maxAmountUnits) {
124
+ throw new Error(`[x402 Security Policy] Requested amount (${requestedAmountUnits} units) exceeds local client authorized price ceiling (${this.maxAmountUnits.toString()} units / $${this.maxPriceUsd}).`);
125
+ }
126
+ const amountUnits = requestedAmountUnits;
127
+
128
+ const now = Math.floor(Date.now() / 1000);
129
+ const validAfter = now - 60;
130
+ const validBefore = now + 3600;
131
+
132
+ // Generate 32-byte hex nonce
133
+ let nonce = '0x';
134
+ if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
135
+ const bytes = new Uint8Array(32);
136
+ crypto.getRandomValues(bytes);
137
+ nonce += Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
138
+ } else {
139
+ const cryptoNode = require('crypto');
140
+ nonce += cryptoNode.randomBytes(32).toString('hex');
141
+ }
142
+
143
+ const domain = {
144
+ name: 'USD Coin',
145
+ version: '2',
146
+ chainId,
147
+ verifyingContract: tokenContract
148
+ };
149
+
150
+ const types = {
151
+ TransferWithAuthorization: [
152
+ { name: 'from', type: 'address' },
153
+ { name: 'to', type: 'address' },
154
+ { name: 'value', type: 'uint256' },
155
+ { name: 'validAfter', type: 'uint256' },
156
+ { name: 'validBefore', type: 'uint256' },
157
+ { name: 'nonce', type: 'bytes32' }
158
+ ]
159
+ };
160
+
161
+ const fromAddress = typeof this.wallet.getAddress === 'function'
162
+ ? await this.wallet.getAddress()
163
+ : (this.wallet.address || this.wallet.account?.address);
164
+
165
+ const message = {
166
+ from: fromAddress,
167
+ to: payTo,
168
+ value: amountUnits.toString(),
169
+ validAfter,
170
+ validBefore,
171
+ nonce
172
+ };
173
+
174
+ let signature: string | { r: string; s: string; v: number };
175
+ if (typeof this.wallet.signTypedData === 'function') {
176
+ signature = await this.wallet.signTypedData(domain, types, message);
177
+ } else if (typeof this.wallet._signTypedData === 'function') {
178
+ signature = await this.wallet._signTypedData(domain, types, message);
179
+ } else if (typeof this.wallet.signTypedDataV4 === 'function') {
180
+ signature = await this.wallet.signTypedDataV4({ domain, types, message, primaryType: 'TransferWithAuthorization' });
181
+ } else {
182
+ throw new Error('Wallet does not implement EIP-712 signTypedData');
183
+ }
184
+
185
+ let v: number, r: string, s: string;
186
+ if (typeof signature === 'string') {
187
+ const clean = signature.startsWith('0x') ? signature.slice(2) : signature;
188
+ r = '0x' + clean.slice(0, 64);
189
+ s = '0x' + clean.slice(64, 128);
190
+ v = parseInt(clean.slice(128, 130), 16);
191
+ if (v < 27) v += 27;
192
+ } else {
193
+ r = signature.r;
194
+ s = signature.s;
195
+ v = signature.v;
196
+ }
197
+
198
+ const flatSignature = typeof signature === 'string'
199
+ ? signature
200
+ : '0x' + r.slice(2) + s.slice(2) + v.toString(16).padStart(2, '0');
201
+
202
+ return {
203
+ x402Version: 2,
204
+ scheme: 'eip3009',
205
+ network: `eip155:${chainId}`,
206
+ token: tokenContract,
207
+ assetContract: tokenContract,
208
+ authorization: {
209
+ from: fromAddress,
210
+ to: payTo,
211
+ value: amountUnits.toString(),
212
+ validAfter,
213
+ validBefore,
214
+ nonce,
215
+ v,
216
+ r,
217
+ s,
218
+ signature: flatSignature
219
+ }
220
+ };
221
+ }
222
+
223
+ async fetchWithAutoPayment(path: string, options: any = {}): Promise<any> {
224
+ const url = path.startsWith('http') ? path : `${this.baseUrl}/${path.replace(/^\/+/, '')}`;
225
+ const headers = { 'Accept': 'application/json', 'User-Agent': '@m2msentinel/sdk-ts/1.1.2', ...(options.headers || {}) };
226
+
227
+ let res = await fetch(url, { method: options.method || 'GET', headers, body: options.body });
228
+ if (res.status !== 402) {
229
+ return res;
230
+ }
231
+
232
+ const challengeHeader = res.headers.get('PAYMENT-REQUIRED') || res.headers.get('x402-payment-required');
233
+ let challenge: any = parsePaymentHeader(challengeHeader);
234
+
235
+ if (!challenge) {
236
+ try {
237
+ const bodyJson = await res.clone().json();
238
+ challenge = bodyJson.accepts?.[0] || bodyJson.paymentRequired;
239
+ } catch {
240
+ // Body was not JSON
241
+ }
242
+ }
243
+
244
+ if (!challenge) {
245
+ throw new Error('HTTP 402 received but no valid x402 payment challenge was found in headers or response body.');
246
+ }
247
+
248
+ const paymentPayload = await this.signPaymentAuthorization(challenge);
249
+ const paymentB64 = typeof btoa === 'function'
250
+ ? btoa(JSON.stringify(paymentPayload))
251
+ : Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
252
+
253
+ const retryHeaders = {
254
+ ...headers,
255
+ 'PAYMENT-SIGNATURE': paymentB64,
256
+ 'x402-payment-authorization': paymentB64
257
+ };
258
+
259
+ return fetch(url, { method: options.method || 'GET', headers: retryHeaders, body: options.body });
260
+ }
261
+ }
262
+
263
+ export function x402SignerClient(options: X402SignerClientOptions = {}): X402SignerClient {
264
+ return new X402SignerClient(options);
265
+ }
package/x402_signer.js ADDED
@@ -0,0 +1,243 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Facilitator-Independent Headless x402 Signer Client.
5
+ *
6
+ * Implements automated HTTP 402 challenge negotiation and EIP-712 / EIP-3009
7
+ * transfer authorization signing directly on Base mainnet (chainId: 8453).
8
+ * Zero browser or UI wallet dependencies — pure headless operation for autonomous agents.
9
+ */
10
+
11
+ const https = require('https');
12
+ const http = require('http');
13
+ const crypto = require('crypto');
14
+
15
+ const BASE_USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
16
+ const BASE_CHAIN_ID = 8453;
17
+
18
+ function parsePaymentHeader(value) {
19
+ if (!value) return null;
20
+ if (typeof value === 'object') return value;
21
+ try { return JSON.parse(value); } catch (_) {}
22
+ try {
23
+ const normalized = String(value).replace(/-/g, '+').replace(/_/g, '/');
24
+ const padded = normalized + '='.repeat((4 - normalized.length % 4) % 4);
25
+ return JSON.parse(Buffer.from(padded, 'base64').toString('utf8'));
26
+ } catch (_) {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function parsePriceToUnits(priceStr, decimals = 6) {
32
+ if (typeof priceStr === 'number') return BigInt(Math.round(priceStr * 10 ** decimals));
33
+ const clean = String(priceStr).replace(/[^0-9.]/g, '');
34
+ const [whole, fraction = ''] = clean.split('.');
35
+ const paddedFraction = (fraction + '0'.repeat(decimals)).slice(0, decimals);
36
+ return BigInt(whole || '0') * BigInt(10 ** decimals) + BigInt(paddedFraction);
37
+ }
38
+
39
+ const EXPECTED_PAYOUT_RECIPIENT = '0x6d6c398390cfb88f1cd42715b84906a0bd6652aa';
40
+ const DEFAULT_MAX_PRICE_USD = 0.05; // 5 cents maximum per autonomous request
41
+ const EIP712_TOKEN_NAME = 'USD Coin';
42
+ const EIP712_TOKEN_VERSION = '2';
43
+
44
+ class X402SignerClient {
45
+ constructor(options = {}) {
46
+ this.wallet = options.wallet || options.walletSigner || null;
47
+ this.baseUrl = (options.baseUrl || 'https://api.m2msentinel.com').replace(/\/+$/, '');
48
+ this.timeoutMs = Number(options.timeoutMs || 30000);
49
+ this.expectedRecipient = EXPECTED_PAYOUT_RECIPIENT;
50
+ this.maxPriceUsd = options.maxPriceUsd !== undefined ? Number(options.maxPriceUsd) : DEFAULT_MAX_PRICE_USD;
51
+ this.maxAmountUnits = parsePriceToUnits(this.maxPriceUsd, 6);
52
+ }
53
+
54
+ async signPaymentAuthorization(challenge = {}) {
55
+ if (!this.wallet) {
56
+ throw new Error('Signer wallet is required to sign x402 payment authorization');
57
+ }
58
+
59
+ // 1. IMMUTABLE LOCAL SECURITY CONSTANTS (Never challenge-controlled)
60
+ const chainId = BASE_CHAIN_ID; // Base Mainnet (8453)
61
+ const tokenContract = BASE_USDC_CONTRACT; // Base USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)
62
+ const payTo = EXPECTED_PAYOUT_RECIPIENT; // M2M Sentinel Payout (0x6d6c398390cfb88f1cd42715b84906a0bd6652aa)
63
+
64
+ // 2. STRICT CHALLENGE INTEGRITY CHECKS (Refuse if challenge alters network, asset, or recipient)
65
+ if (challenge.chainId && Number(challenge.chainId) !== BASE_CHAIN_ID) {
66
+ throw new Error(`[x402 Security Policy] Refusing to sign on unverified network chainId: ${challenge.chainId}. Autonomous signer strictly requires Base Mainnet (8453).`);
67
+ }
68
+ if (challenge.assetContract && challenge.assetContract.toLowerCase() !== BASE_USDC_CONTRACT.toLowerCase()) {
69
+ throw new Error(`[x402 Security Policy] Refusing to sign for unapproved asset: ${challenge.assetContract}. Autonomous signer strictly requires Base USDC (${BASE_USDC_CONTRACT}).`);
70
+ }
71
+ if (challenge.payTo && challenge.payTo.toLowerCase() !== EXPECTED_PAYOUT_RECIPIENT.toLowerCase()) {
72
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected recipient: ${challenge.payTo}. Autonomous signer strictly requires ${EXPECTED_PAYOUT_RECIPIENT}.`);
73
+ }
74
+ if (challenge.tokenName && challenge.tokenName !== EIP712_TOKEN_NAME) {
75
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenName: ${challenge.tokenName}. Expected ${EIP712_TOKEN_NAME}.`);
76
+ }
77
+ if (challenge.tokenVersion && challenge.tokenVersion !== EIP712_TOKEN_VERSION) {
78
+ throw new Error(`[x402 Security Policy] Refusing to sign for unexpected tokenVersion: ${challenge.tokenVersion}. Expected ${EIP712_TOKEN_VERSION}.`);
79
+ }
80
+
81
+ // 3. STRICT LOCAL PRICE CEILING CHECK
82
+ const requestedAmountUnits = challenge.maxAmountRequired || challenge.amountUnits || parsePriceToUnits(challenge.amount || challenge.price || '$0.005', 6).toString();
83
+ if (BigInt(requestedAmountUnits) > this.maxAmountUnits) {
84
+ throw new Error(`[x402 Security Policy] Requested amount (${requestedAmountUnits} units) exceeds local client authorized price ceiling (${this.maxAmountUnits.toString()} units / $${this.maxPriceUsd}).`);
85
+ }
86
+ const amountUnits = requestedAmountUnits;
87
+
88
+ const now = Math.floor(Date.now() / 1000);
89
+ const validAfter = now - 60;
90
+ const validBefore = now + 3600;
91
+ const nonce = '0x' + crypto.randomBytes(32).toString('hex');
92
+
93
+ const domain = {
94
+ name: EIP712_TOKEN_NAME,
95
+ version: EIP712_TOKEN_VERSION,
96
+ chainId,
97
+ verifyingContract: tokenContract
98
+ };
99
+
100
+ const types = {
101
+ TransferWithAuthorization: [
102
+ { name: 'from', type: 'address' },
103
+ { name: 'to', type: 'address' },
104
+ { name: 'value', type: 'uint256' },
105
+ { name: 'validAfter', type: 'uint256' },
106
+ { name: 'validBefore', type: 'uint256' },
107
+ { name: 'nonce', type: 'bytes32' }
108
+ ]
109
+ };
110
+
111
+ const message = {
112
+ from: typeof this.wallet.getAddress === 'function' ? await this.wallet.getAddress() : (this.wallet.address || this.wallet.account?.address),
113
+ to: payTo,
114
+ value: amountUnits.toString(),
115
+ validAfter,
116
+ validBefore,
117
+ nonce
118
+ };
119
+
120
+ let signature;
121
+ if (typeof this.wallet.signTypedData === 'function') {
122
+ signature = await this.wallet.signTypedData(domain, types, message);
123
+ } else if (typeof this.wallet._signTypedData === 'function') {
124
+ signature = await this.wallet._signTypedData(domain, types, message);
125
+ } else if (typeof this.wallet.signTypedDataV4 === 'function') {
126
+ signature = await this.wallet.signTypedDataV4({ domain, types, message, primaryType: 'TransferWithAuthorization' });
127
+ } else {
128
+ throw new Error('Wallet does not implement EIP-712 signTypedData');
129
+ }
130
+
131
+ let v, r, s;
132
+ if (typeof signature === 'string') {
133
+ const clean = signature.startsWith('0x') ? signature.slice(2) : signature;
134
+ r = '0x' + clean.slice(0, 64);
135
+ s = '0x' + clean.slice(64, 128);
136
+ v = parseInt(clean.slice(128, 130), 16);
137
+ if (v < 27) v += 27;
138
+ } else {
139
+ r = signature.r;
140
+ s = signature.s;
141
+ v = signature.v;
142
+ }
143
+
144
+ return {
145
+ x402Version: 2,
146
+ scheme: 'eip3009',
147
+ network: 'eip155:8453',
148
+ token: tokenContract,
149
+ authorization: {
150
+ from: message.from,
151
+ to: message.to,
152
+ value: message.value,
153
+ validAfter: message.validAfter,
154
+ validBefore: message.validBefore,
155
+ nonce: message.nonce,
156
+ v,
157
+ r,
158
+ s
159
+ }
160
+ };
161
+ }
162
+
163
+ async fetchWithAutoPayment(path, options = {}) {
164
+ const url = path.startsWith('http') ? path : `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
165
+ const initialRes = await this._makeHttpRequest(url, options);
166
+
167
+ if (initialRes.status !== 402) {
168
+ return initialRes;
169
+ }
170
+
171
+ const challengeHeader = initialRes.headers['payment-required'] || initialRes.headers['x402-payment-required'] || initialRes.headers['www-authenticate'];
172
+ const challenge = parsePaymentHeader(challengeHeader) || (initialRes.json ? initialRes.json : null);
173
+
174
+ if (!challenge) {
175
+ throw new Error('Received HTTP 402 Payment Required but could not parse payment challenge header');
176
+ }
177
+
178
+ const paymentPayload = await this.signPaymentAuthorization(challenge);
179
+ const paymentHeaderValue = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
180
+
181
+ const paymentOptions = {
182
+ ...options,
183
+ headers: {
184
+ ...(options.headers || {}),
185
+ 'PAYMENT-SIGNATURE': paymentHeaderValue,
186
+ 'Accept': 'application/json'
187
+ }
188
+ };
189
+
190
+ return this._makeHttpRequest(url, paymentOptions);
191
+ }
192
+
193
+ _makeHttpRequest(urlStr, options = {}) {
194
+ return new Promise((resolve, reject) => {
195
+ const url = new URL(urlStr);
196
+ const isHttps = url.protocol === 'https:';
197
+ const client = isHttps ? https : http;
198
+
199
+ const reqOptions = {
200
+ hostname: url.hostname,
201
+ port: url.port || (isHttps ? 443 : 80),
202
+ path: url.pathname + url.search,
203
+ method: options.method || 'GET',
204
+ headers: {
205
+ 'User-Agent': 'M2M-Sentinel-X402Signer/1.1.2',
206
+ 'Accept': 'application/json',
207
+ ...(options.headers || {})
208
+ },
209
+ timeout: options.timeoutMs || this.timeoutMs
210
+ };
211
+
212
+ const req = client.request(reqOptions, (res) => {
213
+ let rawData = '';
214
+ res.on('data', (chunk) => { rawData += chunk; });
215
+ res.on('end', () => {
216
+ let json = null;
217
+ try { json = JSON.parse(rawData); } catch (_) {}
218
+ resolve({
219
+ status: res.statusCode,
220
+ headers: res.headers,
221
+ text: rawData,
222
+ json
223
+ });
224
+ });
225
+ });
226
+
227
+ req.on('error', (err) => reject(err));
228
+ req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
229
+
230
+ if (options.body) {
231
+ req.write(typeof options.body === 'string' ? options.body : JSON.stringify(options.body));
232
+ }
233
+ req.end();
234
+ });
235
+ }
236
+ }
237
+
238
+ module.exports = {
239
+ X402SignerClient,
240
+ x402SignerClient: (opts) => new X402SignerClient(opts),
241
+ parsePaymentHeader,
242
+ parsePriceToUnits
243
+ };