@chipi-stack/backend 13.13.0 → 14.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -1
- package/dist/index.d.mts +228 -2
- package/dist/index.d.ts +228 -2
- package/dist/index.js +750 -141
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +750 -145
- package/dist/index.mjs.map +1 -1
- package/dist/skuPurchases.js +94 -48
- package/dist/skuPurchases.js.map +1 -1
- package/dist/skuPurchases.mjs +95 -49
- package/dist/skuPurchases.mjs.map +1 -1
- package/dist/skus.js.map +1 -1
- package/dist/skus.mjs.map +1 -1
- package/dist/transactions.d.mts +9 -1
- package/dist/transactions.d.ts +9 -1
- package/dist/transactions.js +81 -24
- package/dist/transactions.js.map +1 -1
- package/dist/transactions.mjs +81 -24
- package/dist/transactions.mjs.map +1 -1
- package/dist/wallets.d.mts +10 -1
- package/dist/wallets.d.ts +10 -1
- package/dist/wallets.js +30 -8
- package/dist/wallets.js.map +1 -1
- package/dist/wallets.mjs +30 -8
- package/dist/wallets.mjs.map +1 -1
- package/package.json +4 -3
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { isValidApiKey, ChipiAuthError, STARKNET_NETWORKS, API_VERSIONING, validateErrorResponse, ChipiApiError, handleApiError, WALLET_RPC_ENDPOINTS, WALLET_CLASS_HASHES, API_ENDPOINTS, ChipiTransactionError, formatAmount, SKU_CONTRACTS, CARRIER_IDS, SERVICE_TYPES, ChipiSessionError, SESSION_ERRORS, SESSION_DEFAULTS, SESSION_ENTRYPOINTS } from '@chipi-stack/shared';
|
|
1
|
+
import { isValidApiKey, ChipiAuthError, STARKNET_NETWORKS, API_VERSIONING, validateErrorResponse, ChipiApiError, handleApiError, WALLET_RPC_ENDPOINTS, WALLET_CLASS_HASHES, API_ENDPOINTS, ChipiTransactionError, getWalletTypeFromClassHash, formatAmount, SKU_CONTRACTS, CARRIER_IDS, SERVICE_TYPES, ChipiSessionError, SESSION_ERRORS, SESSION_DEFAULTS, SESSION_ENTRYPOINTS, CONTRACT_ADDRESSES, TOKEN_DECIMALS } from '@chipi-stack/shared';
|
|
2
2
|
import CryptoES from 'crypto-es';
|
|
3
|
-
import { RpcProvider, ec, hash, Account, num, CairoCustomEnum, CairoOption, CairoOptionVariant, CallData, cairo, typedData } from 'starknet';
|
|
4
|
-
import { STARKNET_CONTRACTS } from '@chipi-stack/types';
|
|
3
|
+
import { RpcProvider, ec, hash, Account, num, CairoCustomEnum, CairoOption, CairoOptionVariant, CallData, cairo, typedData, uint256 } from 'starknet';
|
|
4
|
+
import { STARKNET_CONTRACTS, Chain } from '@chipi-stack/types';
|
|
5
5
|
export { Chain, ChainToken } from '@chipi-stack/types';
|
|
6
|
+
import { buildX402TypedData } from '@chipi-stack/core';
|
|
6
7
|
|
|
7
8
|
// src/chipi-sdk.ts
|
|
8
9
|
var ChipiClient = class {
|
|
@@ -14,7 +15,7 @@ var ChipiClient = class {
|
|
|
14
15
|
this.customAlphaUrl = config.alphaUrl;
|
|
15
16
|
this.baseUrl = this.getBaseUrl();
|
|
16
17
|
this.nodeUrl = config.nodeUrl || STARKNET_NETWORKS.MAINNET;
|
|
17
|
-
this.sdkVersion = "
|
|
18
|
+
this.sdkVersion = "14.0.0";
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Get the API public key (for internal SDK use)
|
|
@@ -25,10 +26,10 @@ var ChipiClient = class {
|
|
|
25
26
|
getBaseUrl() {
|
|
26
27
|
const version = `v${API_VERSIONING.VERSION}`;
|
|
27
28
|
if (this.customAlphaUrl) {
|
|
28
|
-
const cleanUrl = this.customAlphaUrl.replace(/\/v\d
|
|
29
|
+
const cleanUrl = this.customAlphaUrl.replace(/\/v\d+\/?$/, "").replace(/\/$/, "");
|
|
29
30
|
return `${cleanUrl}/${version}`;
|
|
30
31
|
}
|
|
31
|
-
return `https://
|
|
32
|
+
return `https://api.chipipay.com/${version}`;
|
|
32
33
|
}
|
|
33
34
|
/**
|
|
34
35
|
* Add API version query parameters to track the API version being used
|
|
@@ -95,7 +96,6 @@ var ChipiClient = class {
|
|
|
95
96
|
});
|
|
96
97
|
const data = await response.json();
|
|
97
98
|
if (!response.ok) {
|
|
98
|
-
console.error("there was an error", data);
|
|
99
99
|
const errorData = validateErrorResponse(data);
|
|
100
100
|
throw new ChipiApiError(
|
|
101
101
|
errorData.message,
|
|
@@ -189,16 +189,26 @@ var ChipiClient = class {
|
|
|
189
189
|
}
|
|
190
190
|
};
|
|
191
191
|
var encryptPrivateKey = (privateKey, password) => {
|
|
192
|
+
if (!privateKey || !password) {
|
|
193
|
+
throw new Error("Private key and password are required");
|
|
194
|
+
}
|
|
192
195
|
return CryptoES.AES.encrypt(privateKey, password).toString();
|
|
193
196
|
};
|
|
194
197
|
var decryptPrivateKey = (encryptedPrivateKey, password) => {
|
|
198
|
+
if (!encryptedPrivateKey || !password) {
|
|
199
|
+
throw new Error("Encrypted private key and password are required");
|
|
200
|
+
}
|
|
195
201
|
try {
|
|
196
202
|
const bytes = CryptoES.AES.decrypt(encryptedPrivateKey, password);
|
|
197
203
|
const decrypted = bytes.toString(CryptoES.enc.Utf8);
|
|
198
|
-
if (!decrypted)
|
|
204
|
+
if (!decrypted) {
|
|
205
|
+
throw new Error("Decryption resulted in empty string");
|
|
206
|
+
}
|
|
199
207
|
return decrypted;
|
|
200
208
|
} catch (error) {
|
|
201
|
-
|
|
209
|
+
if (error instanceof Error) {
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
202
212
|
throw new Error(`Decryption failed: ${error}`);
|
|
203
213
|
}
|
|
204
214
|
};
|
|
@@ -227,6 +237,7 @@ var ChipiWallets = class {
|
|
|
227
237
|
userId,
|
|
228
238
|
walletType = "CHIPI",
|
|
229
239
|
// Default to CHIPI wallet
|
|
240
|
+
classHash: customClassHash,
|
|
230
241
|
usePasskey,
|
|
231
242
|
chain
|
|
232
243
|
} = params;
|
|
@@ -242,11 +253,8 @@ var ChipiWallets = class {
|
|
|
242
253
|
const provider = new RpcProvider({ nodeUrl: rpcUrl });
|
|
243
254
|
const privateKeyAX = this.getPrivateKeyAX();
|
|
244
255
|
const starkKeyPubAX = ec.starkCurve.getStarkKey(privateKeyAX);
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
walletType,
|
|
248
|
-
starkKeyPubAX
|
|
249
|
-
);
|
|
256
|
+
const constructorCallData = this.buildConstructorCallData(walletType, starkKeyPubAX);
|
|
257
|
+
const accountClassHash = customClassHash ?? (walletType === "READY" ? WALLET_CLASS_HASHES.READY : WALLET_CLASS_HASHES.CHIPI);
|
|
250
258
|
const publicKey = hash.calculateContractAddressFromHash(
|
|
251
259
|
starkKeyPubAX,
|
|
252
260
|
accountClassHash,
|
|
@@ -261,8 +269,8 @@ var ChipiWallets = class {
|
|
|
261
269
|
chain,
|
|
262
270
|
publicKey,
|
|
263
271
|
walletType,
|
|
264
|
-
starkKeyPubAX
|
|
265
|
-
|
|
272
|
+
starkKeyPubAX,
|
|
273
|
+
...customClassHash && { classHash: customClassHash }
|
|
266
274
|
}
|
|
267
275
|
});
|
|
268
276
|
const { typedData: typedData2, accountClassHash: accountClassHashResponse } = typedDataResponse;
|
|
@@ -358,7 +366,7 @@ var ChipiWallets = class {
|
|
|
358
366
|
if (err instanceof ChipiApiError && err.status === 404) {
|
|
359
367
|
return null;
|
|
360
368
|
}
|
|
361
|
-
throw
|
|
369
|
+
throw err;
|
|
362
370
|
}
|
|
363
371
|
}
|
|
364
372
|
async updateWalletEncryption(params, bearerToken) {
|
|
@@ -386,7 +394,121 @@ var ChipiWallets = class {
|
|
|
386
394
|
});
|
|
387
395
|
return getBalanceResponse;
|
|
388
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* Prepare a wallet upgrade (e.g., READY → CHIPI or CHIPI v29 → v33).
|
|
399
|
+
* Returns typed data that must be signed by the wallet owner.
|
|
400
|
+
*/
|
|
401
|
+
async prepareWalletUpgrade(params, bearerToken) {
|
|
402
|
+
return this.client.post({
|
|
403
|
+
endpoint: `${API_ENDPOINTS.CHIPI_WALLETS}/prepare-upgrade`,
|
|
404
|
+
bearerToken,
|
|
405
|
+
body: params
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Execute a wallet upgrade after the owner has signed the typed data.
|
|
410
|
+
*/
|
|
411
|
+
async executeWalletUpgrade(params, bearerToken) {
|
|
412
|
+
return this.client.post({
|
|
413
|
+
endpoint: `${API_ENDPOINTS.CHIPI_WALLETS}/upgrade`,
|
|
414
|
+
bearerToken,
|
|
415
|
+
body: params
|
|
416
|
+
});
|
|
417
|
+
}
|
|
389
418
|
};
|
|
419
|
+
function unwrapPrepareResponse(response) {
|
|
420
|
+
if (!response || typeof response !== "object") {
|
|
421
|
+
throw new Error(
|
|
422
|
+
"Invalid response from /transactions/prepare-typed-data: response is not an object"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
if ("typedData" in response) {
|
|
426
|
+
const resp = response;
|
|
427
|
+
if (!resp.typedData || typeof resp.typedData !== "object" || !("types" in resp.typedData) || !("message" in resp.typedData)) {
|
|
428
|
+
throw new Error(
|
|
429
|
+
"Invalid response from /transactions/prepare-typed-data: typedData structure is invalid"
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
return { typedData: resp.typedData, walletType: resp.walletType };
|
|
433
|
+
}
|
|
434
|
+
if ("types" in response && "message" in response) {
|
|
435
|
+
return { typedData: response };
|
|
436
|
+
}
|
|
437
|
+
const apiMessage = typeof response.message === "string" ? response.message : void 0;
|
|
438
|
+
throw new Error(
|
|
439
|
+
apiMessage ?? "Invalid response from /transactions/prepare-typed-data: typedData is missing"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
function normalizeSignature(signature) {
|
|
443
|
+
if (Array.isArray(signature)) {
|
|
444
|
+
if (signature.length < 2) {
|
|
445
|
+
throw new Error(`Array signature must have at least 2 elements, got ${signature.length}`);
|
|
446
|
+
}
|
|
447
|
+
const r = signature[0];
|
|
448
|
+
const s = signature[1];
|
|
449
|
+
if (r == null || s == null) {
|
|
450
|
+
throw new Error(`Signature elements must not be null/undefined. Got r=${r}, s=${s}`);
|
|
451
|
+
}
|
|
452
|
+
return { r: String(r), s: String(s) };
|
|
453
|
+
}
|
|
454
|
+
if (signature && typeof signature === "object" && "r" in signature && "s" in signature) {
|
|
455
|
+
const sig = signature;
|
|
456
|
+
if (sig.r == null || sig.s == null) {
|
|
457
|
+
throw new Error(`Object signature has null/undefined fields. Got r=${sig.r}, s=${sig.s}`);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
r: String(sig.r),
|
|
461
|
+
s: String(sig.s)
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
throw new Error("Unrecognized signature format from account.signMessage()");
|
|
465
|
+
}
|
|
466
|
+
var ChipiPaymasterAdapter = class {
|
|
467
|
+
constructor(client, bearerToken) {
|
|
468
|
+
this.client = client;
|
|
469
|
+
this.bearerToken = bearerToken;
|
|
470
|
+
}
|
|
471
|
+
async execute({ account, calls }) {
|
|
472
|
+
const classHash = await account.getClassHashAt(account.address);
|
|
473
|
+
const walletType = getWalletTypeFromClassHash(classHash);
|
|
474
|
+
const accountClassHash = walletType ? WALLET_CLASS_HASHES[walletType] : classHash;
|
|
475
|
+
const prepareResponse = await this.client.post({
|
|
476
|
+
endpoint: "/transactions/prepare-typed-data",
|
|
477
|
+
bearerToken: this.bearerToken,
|
|
478
|
+
body: {
|
|
479
|
+
publicKey: account.address,
|
|
480
|
+
calls,
|
|
481
|
+
accountClassHash
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
const { typedData: typedData2, walletType: resolvedWalletType } = unwrapPrepareResponse(prepareResponse);
|
|
485
|
+
const signature = await account.signMessage(typedData2);
|
|
486
|
+
const normalizedSig = normalizeSignature(signature);
|
|
487
|
+
const result = await this.client.post({
|
|
488
|
+
endpoint: "/transactions/execute-sponsored-transaction",
|
|
489
|
+
bearerToken: this.bearerToken,
|
|
490
|
+
body: {
|
|
491
|
+
publicKey: account.address,
|
|
492
|
+
typedData: typedData2,
|
|
493
|
+
userSignature: normalizedSig,
|
|
494
|
+
walletType: resolvedWalletType ?? walletType,
|
|
495
|
+
saveToDatabase: true
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
if (!result || typeof result !== "object") {
|
|
499
|
+
throw new Error(
|
|
500
|
+
"Invalid response from /transactions/execute-sponsored-transaction: response is not an object"
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const apiMessage = typeof result.message === "string" ? result.message : void 0;
|
|
504
|
+
if (!result.transactionHash) {
|
|
505
|
+
throw new Error(apiMessage ?? "Response does not contain transaction hash");
|
|
506
|
+
}
|
|
507
|
+
return result.transactionHash;
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
// src/execute-paymaster-transaction.ts
|
|
390
512
|
var executePaymasterTransaction = async ({
|
|
391
513
|
params,
|
|
392
514
|
bearerToken,
|
|
@@ -399,14 +521,15 @@ var executePaymasterTransaction = async ({
|
|
|
399
521
|
usePasskey ? "encryptKey is required when using passkey. The passkey authentication should have provided the encryptKey." : "encryptKey is required for transaction execution"
|
|
400
522
|
);
|
|
401
523
|
}
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
524
|
+
const walletType = wallet.walletType ?? "READY";
|
|
525
|
+
const isKnownType = Object.prototype.hasOwnProperty.call(WALLET_RPC_ENDPOINTS, walletType) && Object.prototype.hasOwnProperty.call(WALLET_CLASS_HASHES, walletType);
|
|
526
|
+
const rpcUrl = isKnownType ? WALLET_RPC_ENDPOINTS[walletType] : WALLET_RPC_ENDPOINTS["CHIPI"];
|
|
527
|
+
const accountClassHash = isKnownType ? WALLET_CLASS_HASHES[walletType] : WALLET_CLASS_HASHES["CHIPI"];
|
|
528
|
+
let privateKeyDecrypted;
|
|
529
|
+
try {
|
|
530
|
+
privateKeyDecrypted = decryptPrivateKey(wallet.encryptedPrivateKey, encryptKey);
|
|
531
|
+
} catch {
|
|
532
|
+
throw new Error("Failed to decrypt private key. Verify the encryptKey is correct.");
|
|
410
533
|
}
|
|
411
534
|
const provider = new RpcProvider({ nodeUrl: rpcUrl });
|
|
412
535
|
const account = new Account({
|
|
@@ -429,42 +552,47 @@ var executePaymasterTransaction = async ({
|
|
|
429
552
|
);
|
|
430
553
|
}
|
|
431
554
|
let typedData2;
|
|
432
|
-
let
|
|
555
|
+
let resolvedWalletType;
|
|
433
556
|
if ("typedData" in prepareResponse) {
|
|
434
557
|
const resp = prepareResponse;
|
|
435
558
|
typedData2 = resp.typedData;
|
|
436
|
-
walletType
|
|
559
|
+
if (resp.walletType && Object.prototype.hasOwnProperty.call(WALLET_CLASS_HASHES, resp.walletType)) {
|
|
560
|
+
resolvedWalletType = resp.walletType;
|
|
561
|
+
}
|
|
437
562
|
} else if (prepareResponse && "types" in prepareResponse && "message" in prepareResponse) {
|
|
438
563
|
typedData2 = prepareResponse;
|
|
439
564
|
} else {
|
|
440
|
-
const
|
|
565
|
+
const apiMessage2 = prepareResponse && typeof prepareResponse.message === "string" ? prepareResponse.message : void 0;
|
|
441
566
|
throw new Error(
|
|
442
|
-
|
|
567
|
+
apiMessage2 ?? "Invalid response from /transactions/prepare-typed-data: typedData is missing. This may occur for older STARKNET wallets that don't support paymaster transactions. Please ensure the wallet is a CHIPI wallet type."
|
|
443
568
|
);
|
|
444
569
|
}
|
|
445
|
-
if (!typedData2 || typeof typedData2 !== "object" || !("message" in typedData2)) {
|
|
570
|
+
if (!typedData2 || typeof typedData2 !== "object" || !("types" in typedData2) || !("message" in typedData2)) {
|
|
446
571
|
throw new Error(
|
|
447
|
-
"Invalid typedData received from backend: missing 'message' field. This may occur for older STARKNET wallets that don't support paymaster transactions."
|
|
572
|
+
"Invalid typedData received from backend: missing 'types' or 'message' field. This may occur for older STARKNET wallets that don't support paymaster transactions."
|
|
448
573
|
);
|
|
449
574
|
}
|
|
450
575
|
const userSignature = await account.signMessage(typedData2);
|
|
576
|
+
const normalizedSig = normalizeSignature(userSignature);
|
|
451
577
|
const result = await client.post({
|
|
452
578
|
endpoint: "/transactions/execute-sponsored-transaction",
|
|
453
579
|
bearerToken,
|
|
454
580
|
body: {
|
|
455
581
|
publicKey: wallet.publicKey,
|
|
456
582
|
typedData: typedData2,
|
|
457
|
-
userSignature:
|
|
458
|
-
r: userSignature.r.toString(),
|
|
459
|
-
s: userSignature.s.toString(),
|
|
460
|
-
recovery: userSignature.recovery
|
|
461
|
-
},
|
|
583
|
+
userSignature: normalizedSig,
|
|
462
584
|
saveToDatabase,
|
|
463
|
-
walletType
|
|
585
|
+
walletType: resolvedWalletType ?? walletType
|
|
464
586
|
}
|
|
465
587
|
});
|
|
466
|
-
if (!result
|
|
467
|
-
throw new Error(
|
|
588
|
+
if (!result || typeof result !== "object") {
|
|
589
|
+
throw new Error(
|
|
590
|
+
"Invalid response from /transactions/execute-sponsored-transaction: response is not an object"
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const apiMessage = typeof result.message === "string" ? result.message : void 0;
|
|
594
|
+
if (typeof result.transactionHash !== "string" || result.transactionHash.trim().length === 0) {
|
|
595
|
+
throw new Error(apiMessage ?? "Response does not contain transaction hash");
|
|
468
596
|
}
|
|
469
597
|
return result.transactionHash;
|
|
470
598
|
} catch (error) {
|
|
@@ -479,16 +607,13 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
479
607
|
}) => {
|
|
480
608
|
const { encryptKey, wallet, session, calls, saveToDatabase } = params;
|
|
481
609
|
if (wallet.walletType !== "CHIPI") {
|
|
482
|
-
console.error(
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
hint: "Session keys only work with CHIPI wallets (SNIP-9 compatible)"
|
|
490
|
-
}
|
|
491
|
-
);
|
|
610
|
+
console.error("[ChipiSDK:Session:Execute] Invalid wallet type for session execution", {
|
|
611
|
+
provided: wallet.walletType,
|
|
612
|
+
required: "CHIPI",
|
|
613
|
+
expectedClassHash: WALLET_CLASS_HASHES.CHIPI,
|
|
614
|
+
walletAddress: wallet.publicKey?.slice(0, 15) + "...",
|
|
615
|
+
hint: "Session keys only work with CHIPI wallets (SNIP-9 compatible)"
|
|
616
|
+
});
|
|
492
617
|
throw new ChipiSessionError(
|
|
493
618
|
`Session execution requires CHIPI wallet type. Got: "${wallet.walletType || "undefined"}"`,
|
|
494
619
|
SESSION_ERRORS.INVALID_WALLET_TYPE_FOR_SESSION
|
|
@@ -507,18 +632,14 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
507
632
|
);
|
|
508
633
|
}
|
|
509
634
|
try {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
encryptKey
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
sessionPubKey: session.publicKey?.slice(0, 15) + "...",
|
|
519
|
-
hint: "Ensure the encryptKey matches the one used when creating the session"
|
|
520
|
-
}
|
|
521
|
-
);
|
|
635
|
+
let sessionPrivateKey;
|
|
636
|
+
try {
|
|
637
|
+
sessionPrivateKey = decryptPrivateKey(session.encryptedPrivateKey, encryptKey);
|
|
638
|
+
} catch {
|
|
639
|
+
console.error("[ChipiSDK:Session:Execute] Failed to decrypt session private key", {
|
|
640
|
+
sessionPubKey: session.publicKey?.slice(0, 15) + "...",
|
|
641
|
+
hint: "Ensure the encryptKey matches the one used when creating the session"
|
|
642
|
+
});
|
|
522
643
|
throw new ChipiSessionError(
|
|
523
644
|
"Failed to decrypt session private key. Verify the encryptKey is correct.",
|
|
524
645
|
SESSION_ERRORS.SESSION_DECRYPTION_FAILED
|
|
@@ -537,7 +658,7 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
537
658
|
);
|
|
538
659
|
}
|
|
539
660
|
const accountClassHash = WALLET_CLASS_HASHES.CHIPI;
|
|
540
|
-
const
|
|
661
|
+
const prepareResponse = await client.post({
|
|
541
662
|
endpoint: "/transactions/prepare-typed-data",
|
|
542
663
|
bearerToken,
|
|
543
664
|
body: {
|
|
@@ -547,7 +668,31 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
547
668
|
walletType: "CHIPI"
|
|
548
669
|
}
|
|
549
670
|
});
|
|
550
|
-
|
|
671
|
+
if (!prepareResponse || typeof prepareResponse !== "object") {
|
|
672
|
+
throw new ChipiSessionError(
|
|
673
|
+
"Invalid response from /transactions/prepare-typed-data: response is not an object",
|
|
674
|
+
SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
let sessionTypedData;
|
|
678
|
+
if ("typedData" in prepareResponse && prepareResponse.typedData) {
|
|
679
|
+
const candidate = prepareResponse.typedData;
|
|
680
|
+
if (!candidate || typeof candidate !== "object" || !("types" in candidate) || !("message" in candidate)) {
|
|
681
|
+
throw new ChipiSessionError(
|
|
682
|
+
"Invalid response from /transactions/prepare-typed-data: typedData structure is invalid",
|
|
683
|
+
SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
sessionTypedData = candidate;
|
|
687
|
+
} else if ("types" in prepareResponse && "message" in prepareResponse) {
|
|
688
|
+
sessionTypedData = prepareResponse;
|
|
689
|
+
} else {
|
|
690
|
+
throw new ChipiSessionError(
|
|
691
|
+
"Invalid response from /transactions/prepare-typed-data: typedData is missing",
|
|
692
|
+
SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
const msgHash = typedData.getMessageHash(sessionTypedData, wallet.publicKey);
|
|
551
696
|
const { r, s } = ec.starkCurve.sign(msgHash, sessionPrivateKey);
|
|
552
697
|
const sessionSignature = [
|
|
553
698
|
session.publicKey,
|
|
@@ -560,7 +705,7 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
560
705
|
bearerToken,
|
|
561
706
|
body: {
|
|
562
707
|
publicKey: wallet.publicKey,
|
|
563
|
-
|
|
708
|
+
typedData: sessionTypedData,
|
|
564
709
|
// Session signature format - array of 4 hex strings
|
|
565
710
|
userSignature: sessionSignature,
|
|
566
711
|
saveToDatabase,
|
|
@@ -569,14 +714,21 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
569
714
|
// Flag to indicate session signature format
|
|
570
715
|
}
|
|
571
716
|
});
|
|
572
|
-
if (!result
|
|
573
|
-
|
|
574
|
-
"
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
717
|
+
if (!result || typeof result !== "object") {
|
|
718
|
+
throw new ChipiSessionError(
|
|
719
|
+
"Invalid response from /transactions/execute-sponsored-transaction: response is not an object",
|
|
720
|
+
SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
if (typeof result.transactionHash !== "string" || result.transactionHash.trim().length === 0) {
|
|
724
|
+
const apiMessage = typeof result.message === "string" ? result.message : void 0;
|
|
725
|
+
console.error("[ChipiSDK:Session:Execute] No transaction hash in response", {
|
|
726
|
+
result
|
|
727
|
+
});
|
|
728
|
+
throw new ChipiSessionError(
|
|
729
|
+
apiMessage ?? "Response does not contain transaction hash",
|
|
730
|
+
SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
578
731
|
);
|
|
579
|
-
throw new Error("Response does not contain transaction hash");
|
|
580
732
|
}
|
|
581
733
|
return result.transactionHash;
|
|
582
734
|
} catch (error) {
|
|
@@ -714,6 +866,24 @@ var ChipiTransactions = class {
|
|
|
714
866
|
});
|
|
715
867
|
return response;
|
|
716
868
|
}
|
|
869
|
+
/**
|
|
870
|
+
* Get a single transaction by hash or internal ID
|
|
871
|
+
*/
|
|
872
|
+
async getTransaction(hashOrId, bearerToken) {
|
|
873
|
+
return this.client.get({
|
|
874
|
+
endpoint: `${API_ENDPOINTS.TRANSACTIONS}/${hashOrId}`,
|
|
875
|
+
bearerToken
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Get on-chain transaction status
|
|
880
|
+
*/
|
|
881
|
+
async getTransactionStatus(hash3, bearerToken) {
|
|
882
|
+
return this.client.get({
|
|
883
|
+
endpoint: `${API_ENDPOINTS.TRANSACTIONS}/${hash3}/status`,
|
|
884
|
+
bearerToken
|
|
885
|
+
});
|
|
886
|
+
}
|
|
717
887
|
};
|
|
718
888
|
var ChipiExchanges = class {
|
|
719
889
|
constructor(client) {
|
|
@@ -742,32 +912,21 @@ var ChipiSkuPurchases = class {
|
|
|
742
912
|
* @returns Promise<SkuPurchaseResponse> - The purchase response with transaction details
|
|
743
913
|
*/
|
|
744
914
|
async purchaseSku(params, bearerToken) {
|
|
745
|
-
const {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
token
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
currency,
|
|
758
|
-
bearerToken
|
|
759
|
-
);
|
|
760
|
-
const tokenAddress = STARKNET_CONTRACTS[token].contractAddress;
|
|
761
|
-
if (usdAmount <= 0) {
|
|
762
|
-
throw new Error(
|
|
763
|
-
`Invalid USD amount: ${usdAmount}. Amount must be positive.`
|
|
764
|
-
);
|
|
915
|
+
const { skuId, skuReference, currencyAmount, currency, chain, token, encryptKey, wallet } = params;
|
|
916
|
+
if (chain !== Chain.STARKNET) {
|
|
917
|
+
throw new Error(`Unsupported chain for SKU purchases: ${chain}`);
|
|
918
|
+
}
|
|
919
|
+
const tokenConfig = STARKNET_CONTRACTS[token];
|
|
920
|
+
if (!tokenConfig?.contractAddress) {
|
|
921
|
+
throw new Error(`Unsupported token for Starknet SKU purchases: ${token}`);
|
|
922
|
+
}
|
|
923
|
+
const usdAmount = await this.exchanges.getUsdAmount(currencyAmount, currency, bearerToken);
|
|
924
|
+
const tokenAddress = tokenConfig.contractAddress;
|
|
925
|
+
if (!Number.isFinite(usdAmount) || usdAmount <= 0) {
|
|
926
|
+
throw new Error(`Invalid USD amount: ${usdAmount}. Amount must be positive.`);
|
|
765
927
|
}
|
|
766
928
|
const minUsdAmount = Math.max(usdAmount, 1e-6);
|
|
767
|
-
const parsedAmount = formatAmount(
|
|
768
|
-
minUsdAmount.toString(),
|
|
769
|
-
STARKNET_CONTRACTS[token].decimals
|
|
770
|
-
);
|
|
929
|
+
const parsedAmount = formatAmount(minUsdAmount.toString(), tokenConfig.decimals);
|
|
771
930
|
const calls = [
|
|
772
931
|
{
|
|
773
932
|
contractAddress: tokenAddress,
|
|
@@ -929,6 +1088,12 @@ var ChipiSessions = class {
|
|
|
929
1088
|
*/
|
|
930
1089
|
createSessionKey(params) {
|
|
931
1090
|
const { encryptKey, durationSeconds = SESSION_DEFAULTS.DURATION_SECONDS } = params;
|
|
1091
|
+
if (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || !Number.isInteger(durationSeconds) || durationSeconds <= 0) {
|
|
1092
|
+
throw new ChipiSessionError(
|
|
1093
|
+
`Invalid session duration: ${durationSeconds}. Must be a positive integer.`,
|
|
1094
|
+
SESSION_ERRORS.SESSION_CREATION_FAILED
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
932
1097
|
try {
|
|
933
1098
|
const rawPrivateKey = ec.starkCurve.utils.randomPrivateKey();
|
|
934
1099
|
const privateKeyHex = this.toHex(rawPrivateKey);
|
|
@@ -942,13 +1107,10 @@ var ChipiSessions = class {
|
|
|
942
1107
|
};
|
|
943
1108
|
} catch (error) {
|
|
944
1109
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
945
|
-
console.error(
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
durationRequested: durationSeconds
|
|
950
|
-
}
|
|
951
|
-
);
|
|
1110
|
+
console.error("[ChipiSDK:Session:Create] Failed to generate session key", {
|
|
1111
|
+
error: err.message,
|
|
1112
|
+
durationRequested: durationSeconds
|
|
1113
|
+
});
|
|
952
1114
|
throw new ChipiSessionError(
|
|
953
1115
|
`Failed to create session key: ${err.message}`,
|
|
954
1116
|
SESSION_ERRORS.SESSION_CREATION_FAILED
|
|
@@ -983,22 +1145,15 @@ var ChipiSessions = class {
|
|
|
983
1145
|
*/
|
|
984
1146
|
async addSessionKeyToContract(params, bearerToken) {
|
|
985
1147
|
const { encryptKey, wallet, sessionConfig } = params;
|
|
986
|
-
this.validateChipiWallet(
|
|
987
|
-
wallet.walletType,
|
|
988
|
-
"AddToContract",
|
|
989
|
-
wallet.publicKey
|
|
990
|
-
);
|
|
1148
|
+
this.validateChipiWallet(wallet.walletType, "AddToContract", wallet.publicKey);
|
|
991
1149
|
try {
|
|
992
|
-
console.log(
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
allowedEntrypoints: sessionConfig.allowedEntrypoints.length
|
|
1000
|
-
}
|
|
1001
|
-
);
|
|
1150
|
+
console.log("[ChipiSDK:Session:AddToContract] Registering session on-chain", {
|
|
1151
|
+
walletAddress: wallet.publicKey.slice(0, 15) + "...",
|
|
1152
|
+
sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "...",
|
|
1153
|
+
validUntil: new Date(sessionConfig.validUntil * 1e3).toISOString(),
|
|
1154
|
+
maxCalls: sessionConfig.maxCalls,
|
|
1155
|
+
allowedEntrypoints: sessionConfig.allowedEntrypoints.length
|
|
1156
|
+
});
|
|
1002
1157
|
const calldata = [
|
|
1003
1158
|
sessionConfig.sessionPublicKey,
|
|
1004
1159
|
num.toHex(sessionConfig.validUntil),
|
|
@@ -1024,13 +1179,10 @@ var ChipiSessions = class {
|
|
|
1024
1179
|
bearerToken,
|
|
1025
1180
|
client: this.client
|
|
1026
1181
|
});
|
|
1027
|
-
console.log(
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "..."
|
|
1032
|
-
}
|
|
1033
|
-
);
|
|
1182
|
+
console.log("[ChipiSDK:Session:AddToContract] Session registered successfully", {
|
|
1183
|
+
txHash,
|
|
1184
|
+
sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "..."
|
|
1185
|
+
});
|
|
1034
1186
|
return txHash;
|
|
1035
1187
|
} catch (error) {
|
|
1036
1188
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1178,17 +1330,33 @@ var ChipiSessions = class {
|
|
|
1178
1330
|
return sessionData;
|
|
1179
1331
|
} catch (error) {
|
|
1180
1332
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1181
|
-
|
|
1333
|
+
const sessionNotFoundPatterns = [
|
|
1334
|
+
/entry point.*not found/i,
|
|
1335
|
+
/requested entrypoint was not found/i,
|
|
1336
|
+
/invalid message selector/i,
|
|
1337
|
+
/contract not found/i,
|
|
1338
|
+
/is not deployed/i,
|
|
1339
|
+
/revert.*get_session_data/i
|
|
1340
|
+
];
|
|
1341
|
+
const isSessionNotFound = sessionNotFoundPatterns.some((p) => p.test(err.message));
|
|
1342
|
+
if (isSessionNotFound) {
|
|
1343
|
+
console.log("[ChipiSDK:Session:GetData] Session not found on contract", {
|
|
1344
|
+
walletAddress: walletAddress?.slice(0, 15) + "...",
|
|
1345
|
+
sessionPubKey: sessionPublicKey?.slice(0, 15) + "..."
|
|
1346
|
+
});
|
|
1347
|
+
return {
|
|
1348
|
+
isActive: false,
|
|
1349
|
+
validUntil: 0,
|
|
1350
|
+
remainingCalls: 0,
|
|
1351
|
+
allowedEntrypoints: []
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
console.error("[ChipiSDK:Session:GetData] RPC/network error", {
|
|
1182
1355
|
error: err.message,
|
|
1183
1356
|
walletAddress: walletAddress?.slice(0, 15) + "...",
|
|
1184
1357
|
sessionPubKey: sessionPublicKey?.slice(0, 15) + "..."
|
|
1185
1358
|
});
|
|
1186
|
-
|
|
1187
|
-
isActive: false,
|
|
1188
|
-
validUntil: 0,
|
|
1189
|
-
remainingCalls: 0,
|
|
1190
|
-
allowedEntrypoints: []
|
|
1191
|
-
};
|
|
1359
|
+
throw err;
|
|
1192
1360
|
}
|
|
1193
1361
|
}
|
|
1194
1362
|
/**
|
|
@@ -1252,6 +1420,240 @@ var ChipiSessions = class {
|
|
|
1252
1420
|
});
|
|
1253
1421
|
}
|
|
1254
1422
|
};
|
|
1423
|
+
var InMemoryNonceStore = class {
|
|
1424
|
+
constructor() {
|
|
1425
|
+
this.used = /* @__PURE__ */ new Set();
|
|
1426
|
+
}
|
|
1427
|
+
has(nonce) {
|
|
1428
|
+
return this.used.has(nonce);
|
|
1429
|
+
}
|
|
1430
|
+
consume(nonce) {
|
|
1431
|
+
if (this.used.has(nonce)) return false;
|
|
1432
|
+
this.used.add(nonce);
|
|
1433
|
+
return true;
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
var X402Facilitator = class _X402Facilitator {
|
|
1437
|
+
constructor(client, bearerToken, config) {
|
|
1438
|
+
this.client = client;
|
|
1439
|
+
this.bearerToken = bearerToken;
|
|
1440
|
+
this.nonceStore = config?.nonceStore ?? new InMemoryNonceStore();
|
|
1441
|
+
this.provider = config?.provider;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Factory method for simpler instantiation.
|
|
1445
|
+
*
|
|
1446
|
+
* @example
|
|
1447
|
+
* ```typescript
|
|
1448
|
+
* const facilitator = X402Facilitator.create({
|
|
1449
|
+
* apiKey: process.env.CHIPI_API_KEY!,
|
|
1450
|
+
* bearerToken: process.env.CHIPI_BEARER_TOKEN!,
|
|
1451
|
+
* });
|
|
1452
|
+
* ```
|
|
1453
|
+
*/
|
|
1454
|
+
static create(options) {
|
|
1455
|
+
const client = new ChipiClient({ apiPublicKey: options.apiKey });
|
|
1456
|
+
return new _X402Facilitator(client, options.bearerToken, {
|
|
1457
|
+
nonceStore: options.nonceStore,
|
|
1458
|
+
provider: options.provider
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
/**
|
|
1462
|
+
* Verify a payment signature is valid (doesn't execute the transfer).
|
|
1463
|
+
*
|
|
1464
|
+
* Checks:
|
|
1465
|
+
* 1. Nonce has not been used (replay protection)
|
|
1466
|
+
* 2. Payment has not expired (validUntil > now)
|
|
1467
|
+
* 3. Asset is USDC mainnet
|
|
1468
|
+
* 4. Amount is positive
|
|
1469
|
+
* 5. Addresses are non-empty
|
|
1470
|
+
*/
|
|
1471
|
+
async verify(payment) {
|
|
1472
|
+
const { payload } = payment;
|
|
1473
|
+
if (payment.x402Version !== 1) {
|
|
1474
|
+
return { isValid: false, invalidReason: `Unsupported x402 version: ${payment.x402Version}` };
|
|
1475
|
+
}
|
|
1476
|
+
if (payment.network !== "starknet-mainnet") {
|
|
1477
|
+
return { isValid: false, invalidReason: `Unsupported network: ${payment.network}` };
|
|
1478
|
+
}
|
|
1479
|
+
if (await this.nonceStore.has(payload.nonce)) {
|
|
1480
|
+
return { isValid: false, invalidReason: "Nonce already used (replay)" };
|
|
1481
|
+
}
|
|
1482
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1483
|
+
if (payload.validUntil <= now) {
|
|
1484
|
+
return { isValid: false, invalidReason: "Payment has expired (validUntil in the past)" };
|
|
1485
|
+
}
|
|
1486
|
+
const normalizedAsset = payload.asset.toLowerCase().replace(/^0x0+/, "0x");
|
|
1487
|
+
const normalizedUsdc = CONTRACT_ADDRESSES.USDC_MAINNET.toLowerCase().replace(/^0x0+/, "0x");
|
|
1488
|
+
if (normalizedAsset !== normalizedUsdc) {
|
|
1489
|
+
return {
|
|
1490
|
+
isValid: false,
|
|
1491
|
+
invalidReason: `Unsupported asset: ${payload.asset}. Only USDC is accepted.`
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
let amount;
|
|
1495
|
+
try {
|
|
1496
|
+
amount = BigInt(payload.amount);
|
|
1497
|
+
} catch {
|
|
1498
|
+
return { isValid: false, invalidReason: "Invalid payment amount format" };
|
|
1499
|
+
}
|
|
1500
|
+
if (amount <= 0n) {
|
|
1501
|
+
return { isValid: false, invalidReason: "Payment amount must be positive" };
|
|
1502
|
+
}
|
|
1503
|
+
if (!payload.fromAddress || !payload.toAddress) {
|
|
1504
|
+
return { isValid: false, invalidReason: "Missing fromAddress or toAddress" };
|
|
1505
|
+
}
|
|
1506
|
+
const sigResult = await this.verifySignature(payment);
|
|
1507
|
+
if (!sigResult.valid) {
|
|
1508
|
+
return { isValid: false, invalidReason: sigResult.reason };
|
|
1509
|
+
}
|
|
1510
|
+
return { isValid: true };
|
|
1511
|
+
}
|
|
1512
|
+
/**
|
|
1513
|
+
* Verify the SNIP-12 signature on a payment payload.
|
|
1514
|
+
*
|
|
1515
|
+
* If an RPC provider is configured, reconstructs the typed data, computes the
|
|
1516
|
+
* message hash, and calls `is_valid_signature()` on the sender's account contract.
|
|
1517
|
+
* Otherwise falls back to a presence-only check (backwards compatible).
|
|
1518
|
+
*/
|
|
1519
|
+
async verifySignature(payment) {
|
|
1520
|
+
if (!this.provider) {
|
|
1521
|
+
if (!payment.payload?.signature?.r || !payment.payload?.signature?.s) {
|
|
1522
|
+
return { valid: false, reason: "Missing signature" };
|
|
1523
|
+
}
|
|
1524
|
+
return { valid: true };
|
|
1525
|
+
}
|
|
1526
|
+
const payload = payment.payload;
|
|
1527
|
+
if (!payload?.signature?.r || !payload?.signature?.s) {
|
|
1528
|
+
return { valid: false, reason: "Missing signature" };
|
|
1529
|
+
}
|
|
1530
|
+
const paymentTypedData = buildX402TypedData({
|
|
1531
|
+
from_address: payload.fromAddress,
|
|
1532
|
+
to_address: payload.toAddress,
|
|
1533
|
+
amount: payload.amount,
|
|
1534
|
+
asset: payload.asset,
|
|
1535
|
+
valid_until: String(payload.validUntil),
|
|
1536
|
+
nonce: payload.nonce
|
|
1537
|
+
});
|
|
1538
|
+
const msgHash = typedData.getMessageHash(paymentTypedData, payload.fromAddress);
|
|
1539
|
+
try {
|
|
1540
|
+
const result = await this.provider.callContract({
|
|
1541
|
+
contractAddress: payload.fromAddress,
|
|
1542
|
+
entrypoint: "is_valid_signature",
|
|
1543
|
+
calldata: [msgHash, "2", payload.signature.r, payload.signature.s]
|
|
1544
|
+
});
|
|
1545
|
+
const VALID = "0x56414c4944";
|
|
1546
|
+
if (result[0] === VALID || result[0] === BigInt(VALID).toString()) {
|
|
1547
|
+
return { valid: true };
|
|
1548
|
+
}
|
|
1549
|
+
return { valid: false, reason: "Invalid SNIP-12 signature" };
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1552
|
+
return { valid: false, reason: `Signature verification failed: ${message}` };
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
/**
|
|
1556
|
+
* Settle a verified payment by executing the USDC transfer via paymaster.
|
|
1557
|
+
*
|
|
1558
|
+
* IMPORTANT: Mark nonce as consumed BEFORE executing to prevent concurrent replays.
|
|
1559
|
+
* If the transfer fails after nonce consumption, the nonce is still burned
|
|
1560
|
+
* (caller should implement idempotency at a higher level if needed).
|
|
1561
|
+
*/
|
|
1562
|
+
async settle(payment) {
|
|
1563
|
+
const { payload } = payment;
|
|
1564
|
+
const verification = await this.verify(payment);
|
|
1565
|
+
if (!verification.isValid) {
|
|
1566
|
+
return { success: false, errorReason: verification.invalidReason };
|
|
1567
|
+
}
|
|
1568
|
+
const consumed = await this.nonceStore.consume(payload.nonce);
|
|
1569
|
+
if (!consumed) {
|
|
1570
|
+
return { success: false, errorReason: "Nonce already consumed (concurrent replay)" };
|
|
1571
|
+
}
|
|
1572
|
+
try {
|
|
1573
|
+
const amountU256 = uint256.bnToUint256(BigInt(payload.amount));
|
|
1574
|
+
const transferCall = {
|
|
1575
|
+
contractAddress: payload.asset,
|
|
1576
|
+
entrypoint: "transfer",
|
|
1577
|
+
calldata: [payload.toAddress, amountU256.low.toString(), amountU256.high.toString()]
|
|
1578
|
+
};
|
|
1579
|
+
const prepareResponse = await this.client.post({
|
|
1580
|
+
endpoint: "/transactions/prepare-typed-data",
|
|
1581
|
+
bearerToken: this.bearerToken,
|
|
1582
|
+
body: {
|
|
1583
|
+
publicKey: payload.fromAddress,
|
|
1584
|
+
calls: [transferCall]
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
const result = await this.client.post({
|
|
1588
|
+
endpoint: "/transactions/execute-sponsored-transaction",
|
|
1589
|
+
bearerToken: this.bearerToken,
|
|
1590
|
+
body: {
|
|
1591
|
+
publicKey: payload.fromAddress,
|
|
1592
|
+
typedData: prepareResponse.typedData,
|
|
1593
|
+
userSignature: payload.signature,
|
|
1594
|
+
walletType: prepareResponse.walletType,
|
|
1595
|
+
saveToDatabase: true
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
if (typeof result?.transactionHash !== "string" || !result.transactionHash.trim()) {
|
|
1599
|
+
return {
|
|
1600
|
+
success: false,
|
|
1601
|
+
errorReason: result?.message ?? "No transaction hash returned from paymaster"
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
success: true,
|
|
1606
|
+
transactionHash: result.transactionHash,
|
|
1607
|
+
networkId: "starknet-mainnet"
|
|
1608
|
+
};
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1611
|
+
return {
|
|
1612
|
+
success: false,
|
|
1613
|
+
errorReason: `Settlement failed: ${message}`
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
var signTypedData = async ({
|
|
1619
|
+
params
|
|
1620
|
+
}) => {
|
|
1621
|
+
try {
|
|
1622
|
+
const { encryptKey, wallet, typedData: typedData2 } = params;
|
|
1623
|
+
if (!encryptKey) {
|
|
1624
|
+
throw new Error("encryptKey is required for signing typed data");
|
|
1625
|
+
}
|
|
1626
|
+
if (!wallet || !wallet.encryptedPrivateKey || !wallet.publicKey) {
|
|
1627
|
+
throw new Error("wallet with publicKey and encryptedPrivateKey is required");
|
|
1628
|
+
}
|
|
1629
|
+
if (!typedData2) {
|
|
1630
|
+
throw new Error("typedData is required for signing");
|
|
1631
|
+
}
|
|
1632
|
+
const walletType = wallet.walletType ?? "READY";
|
|
1633
|
+
if (!Object.prototype.hasOwnProperty.call(WALLET_RPC_ENDPOINTS, walletType) || !Object.prototype.hasOwnProperty.call(WALLET_CLASS_HASHES, walletType)) {
|
|
1634
|
+
throw new Error(`Unsupported wallet type: "${String(walletType)}"`);
|
|
1635
|
+
}
|
|
1636
|
+
const rpcUrl = WALLET_RPC_ENDPOINTS[walletType];
|
|
1637
|
+
let privateKeyDecrypted;
|
|
1638
|
+
try {
|
|
1639
|
+
privateKeyDecrypted = decryptPrivateKey(wallet.encryptedPrivateKey, encryptKey);
|
|
1640
|
+
} catch {
|
|
1641
|
+
throw new Error("Failed to decrypt private key. Verify the encryptKey is correct.");
|
|
1642
|
+
}
|
|
1643
|
+
const provider = new RpcProvider({ nodeUrl: rpcUrl });
|
|
1644
|
+
const account = new Account({
|
|
1645
|
+
provider,
|
|
1646
|
+
address: wallet.publicKey,
|
|
1647
|
+
signer: privateKeyDecrypted
|
|
1648
|
+
});
|
|
1649
|
+
const signature = await account.signMessage(typedData2);
|
|
1650
|
+
const normalizedSig = normalizeSignature(signature);
|
|
1651
|
+
return normalizedSig;
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
console.error("Error signing typed data", error);
|
|
1654
|
+
throw error;
|
|
1655
|
+
}
|
|
1656
|
+
};
|
|
1255
1657
|
|
|
1256
1658
|
// src/chipi-sdk.ts
|
|
1257
1659
|
var ChipiSDK = class {
|
|
@@ -1274,6 +1676,9 @@ var ChipiSDK = class {
|
|
|
1274
1676
|
this.recordSendTransaction = this.recordSendTransaction.bind(this);
|
|
1275
1677
|
this.purchaseSku = this.purchaseSku.bind(this);
|
|
1276
1678
|
this.getUsdAmount = this.getUsdAmount.bind(this);
|
|
1679
|
+
this.signTypedData = this.signTypedData.bind(this);
|
|
1680
|
+
this.createPaymasterAdapter = this.createPaymasterAdapter.bind(this);
|
|
1681
|
+
this.createX402Facilitator = this.createX402Facilitator.bind(this);
|
|
1277
1682
|
}
|
|
1278
1683
|
/**
|
|
1279
1684
|
* Resolve bearer token - uses provided token or falls back to apiSecretKey
|
|
@@ -1287,6 +1692,43 @@ var ChipiSDK = class {
|
|
|
1287
1692
|
}
|
|
1288
1693
|
return token;
|
|
1289
1694
|
}
|
|
1695
|
+
/**
|
|
1696
|
+
* Create a PaymasterAdapter for use with TxBuilder.sendSponsored().
|
|
1697
|
+
*
|
|
1698
|
+
* @example
|
|
1699
|
+
* ```typescript
|
|
1700
|
+
* const sdk = new ChipiServerSDK({ apiPublicKey: "pk_...", apiSecretKey: "sk_..." });
|
|
1701
|
+
* const paymaster = sdk.createPaymasterAdapter();
|
|
1702
|
+
*
|
|
1703
|
+
* const txHash = await new TxBuilder(account, { paymaster })
|
|
1704
|
+
* .transfer(USDC, [{ to, amount }])
|
|
1705
|
+
* .sendSponsored(); // gasless!
|
|
1706
|
+
* ```
|
|
1707
|
+
*/
|
|
1708
|
+
createPaymasterAdapter(bearerToken) {
|
|
1709
|
+
const token = this.resolveBearerToken(bearerToken);
|
|
1710
|
+
return new ChipiPaymasterAdapter(this.client, token);
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Create an X402Facilitator for verifying and settling x402 payments.
|
|
1714
|
+
*
|
|
1715
|
+
* @example
|
|
1716
|
+
* ```typescript
|
|
1717
|
+
* const facilitator = sdk.createX402Facilitator();
|
|
1718
|
+
*
|
|
1719
|
+
* // With custom nonce store (Redis)
|
|
1720
|
+
* const facilitator = sdk.createX402Facilitator(bearerToken, {
|
|
1721
|
+
* nonceStore: {
|
|
1722
|
+
* has: async (nonce) => (await redis.sismember("x402:nonces", nonce)) === 1,
|
|
1723
|
+
* consume: async (nonce) => (await redis.sadd("x402:nonces", nonce)) === 1,
|
|
1724
|
+
* },
|
|
1725
|
+
* });
|
|
1726
|
+
* ```
|
|
1727
|
+
*/
|
|
1728
|
+
createX402Facilitator(bearerToken, config) {
|
|
1729
|
+
const token = this.resolveBearerToken(bearerToken);
|
|
1730
|
+
return new X402Facilitator(this.client, token, config);
|
|
1731
|
+
}
|
|
1290
1732
|
/**
|
|
1291
1733
|
* Execute a gasless transaction
|
|
1292
1734
|
*/
|
|
@@ -1342,6 +1784,29 @@ var ChipiSDK = class {
|
|
|
1342
1784
|
this.resolveBearerToken(bearerToken)
|
|
1343
1785
|
);
|
|
1344
1786
|
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Sign SNIP-12 typed data without executing a transaction.
|
|
1789
|
+
*
|
|
1790
|
+
* Purely local operation: decrypt private key → sign → return { r, s }.
|
|
1791
|
+
* No API calls are made. Useful for x402 payment signatures and other
|
|
1792
|
+
* off-chain signing flows.
|
|
1793
|
+
*
|
|
1794
|
+
* @example
|
|
1795
|
+
* ```typescript
|
|
1796
|
+
* const { r, s } = await sdk.signTypedData({
|
|
1797
|
+
* params: {
|
|
1798
|
+
* encryptKey: pin,
|
|
1799
|
+
* wallet,
|
|
1800
|
+
* typedData: snip12TypedData,
|
|
1801
|
+
* },
|
|
1802
|
+
* });
|
|
1803
|
+
* ```
|
|
1804
|
+
*/
|
|
1805
|
+
async signTypedData({
|
|
1806
|
+
params
|
|
1807
|
+
}) {
|
|
1808
|
+
return signTypedData({ params });
|
|
1809
|
+
}
|
|
1345
1810
|
/**
|
|
1346
1811
|
* Transfer tokens
|
|
1347
1812
|
*/
|
|
@@ -1401,7 +1866,6 @@ var ChipiSDK = class {
|
|
|
1401
1866
|
params,
|
|
1402
1867
|
bearerToken
|
|
1403
1868
|
}) {
|
|
1404
|
-
console.log("createWallet params:", params);
|
|
1405
1869
|
return this.wallets.createWallet({
|
|
1406
1870
|
...params,
|
|
1407
1871
|
bearerToken: this.resolveBearerToken(bearerToken)
|
|
@@ -1417,10 +1881,7 @@ var ChipiSDK = class {
|
|
|
1417
1881
|
});
|
|
1418
1882
|
}
|
|
1419
1883
|
async getTransactionList(query, bearerToken) {
|
|
1420
|
-
return this.transactions.getTransactionList(
|
|
1421
|
-
query,
|
|
1422
|
-
this.resolveBearerToken(bearerToken)
|
|
1423
|
-
);
|
|
1884
|
+
return this.transactions.getTransactionList(query, this.resolveBearerToken(bearerToken));
|
|
1424
1885
|
}
|
|
1425
1886
|
async getWallet(params, bearerToken) {
|
|
1426
1887
|
return this.wallets.getWallet(params, this.resolveBearerToken(bearerToken));
|
|
@@ -1438,19 +1899,13 @@ var ChipiSDK = class {
|
|
|
1438
1899
|
params,
|
|
1439
1900
|
bearerToken
|
|
1440
1901
|
}) {
|
|
1441
|
-
return this.skuPurchases.purchaseSku(
|
|
1442
|
-
params,
|
|
1443
|
-
this.resolveBearerToken(bearerToken)
|
|
1444
|
-
);
|
|
1902
|
+
return this.skuPurchases.purchaseSku(params, this.resolveBearerToken(bearerToken));
|
|
1445
1903
|
}
|
|
1446
1904
|
/**
|
|
1447
1905
|
* Get a SKU purchase by id
|
|
1448
1906
|
*/
|
|
1449
1907
|
async getSkuPurchase(id, bearerToken) {
|
|
1450
|
-
return this.skuPurchases.getSkuPurchase(
|
|
1451
|
-
id,
|
|
1452
|
-
this.resolveBearerToken(bearerToken)
|
|
1453
|
-
);
|
|
1908
|
+
return this.skuPurchases.getSkuPurchase(id, this.resolveBearerToken(bearerToken));
|
|
1454
1909
|
}
|
|
1455
1910
|
/// SKUS
|
|
1456
1911
|
async getSkuList(params, bearerToken) {
|
|
@@ -1459,6 +1914,33 @@ var ChipiSDK = class {
|
|
|
1459
1914
|
async getSku(id, bearerToken) {
|
|
1460
1915
|
return this.skus.getSku(id, this.resolveBearerToken(bearerToken));
|
|
1461
1916
|
}
|
|
1917
|
+
/**
|
|
1918
|
+
* Get a single transaction by hash or internal ID
|
|
1919
|
+
*/
|
|
1920
|
+
async getTransaction(hashOrId, bearerToken) {
|
|
1921
|
+
return this.transactions.getTransaction(hashOrId, this.resolveBearerToken(bearerToken));
|
|
1922
|
+
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Get on-chain transaction status (supports polling for confirmation)
|
|
1925
|
+
*/
|
|
1926
|
+
async getTransactionStatus(hash3, bearerToken) {
|
|
1927
|
+
return this.transactions.getTransactionStatus(hash3, this.resolveBearerToken(bearerToken));
|
|
1928
|
+
}
|
|
1929
|
+
/**
|
|
1930
|
+
* Prepare a wallet upgrade (e.g., READY → CHIPI or CHIPI v29 → v33).
|
|
1931
|
+
* Returns typed data that must be signed by the wallet owner.
|
|
1932
|
+
* Server-only: requires API secret key.
|
|
1933
|
+
*/
|
|
1934
|
+
async prepareWalletUpgrade(params, bearerToken) {
|
|
1935
|
+
return this.wallets.prepareWalletUpgrade(params, this.resolveBearerToken(bearerToken));
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Execute a wallet upgrade after signing.
|
|
1939
|
+
* Server-only: requires API secret key.
|
|
1940
|
+
*/
|
|
1941
|
+
async executeWalletUpgrade(params, bearerToken) {
|
|
1942
|
+
return this.wallets.executeWalletUpgrade(params, this.resolveBearerToken(bearerToken));
|
|
1943
|
+
}
|
|
1462
1944
|
async getUsdAmount(currencyAmount, currency, bearerToken) {
|
|
1463
1945
|
return this.exchanges.getUsdAmount(
|
|
1464
1946
|
currencyAmount,
|
|
@@ -1490,7 +1972,130 @@ var ChipiBrowserSDK = class extends ChipiSDK {
|
|
|
1490
1972
|
// All methods are inherited from ChipiSDK
|
|
1491
1973
|
// bearerToken parameter is required for each authenticated request
|
|
1492
1974
|
};
|
|
1975
|
+
function amountToBaseUnits(amount, decimals) {
|
|
1976
|
+
const [whole, frac = ""] = amount.split(".");
|
|
1977
|
+
const fracPadded = (frac + "0".repeat(decimals)).slice(0, decimals);
|
|
1978
|
+
return String(BigInt(whole + fracPadded));
|
|
1979
|
+
}
|
|
1980
|
+
function buildPaymentRequirement(config, resource) {
|
|
1981
|
+
const amountBaseUnits = amountToBaseUnits(config.amount, TOKEN_DECIMALS.USDC);
|
|
1982
|
+
return {
|
|
1983
|
+
scheme: "exact",
|
|
1984
|
+
network: "starknet-mainnet",
|
|
1985
|
+
maxAmountRequired: amountBaseUnits,
|
|
1986
|
+
resource,
|
|
1987
|
+
description: config.description,
|
|
1988
|
+
payTo: config.payTo,
|
|
1989
|
+
maxTimeoutSeconds: 300,
|
|
1990
|
+
asset: CONTRACT_ADDRESSES.USDC_MAINNET
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function isFlatConfig(config) {
|
|
1994
|
+
return "amount" in config && "recipient" in config;
|
|
1995
|
+
}
|
|
1996
|
+
function resolveConfig(config) {
|
|
1997
|
+
if (isFlatConfig(config)) {
|
|
1998
|
+
return {
|
|
1999
|
+
facilitator: config.facilitator,
|
|
2000
|
+
paymentConfig: {
|
|
2001
|
+
amount: config.amount,
|
|
2002
|
+
payTo: config.recipient,
|
|
2003
|
+
description: config.description
|
|
2004
|
+
},
|
|
2005
|
+
verifyOnly: config.verifyOnly
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
return config;
|
|
2009
|
+
}
|
|
2010
|
+
function x402Middleware(config) {
|
|
2011
|
+
const resolved = resolveConfig(config);
|
|
2012
|
+
return async (req, res, next) => {
|
|
2013
|
+
if (req.method === "OPTIONS") {
|
|
2014
|
+
return next();
|
|
2015
|
+
}
|
|
2016
|
+
const paymentHeader = req.headers?.["x-payment"] ?? req.headers?.["X-PAYMENT"];
|
|
2017
|
+
if (!paymentHeader) {
|
|
2018
|
+
const resource = req.protocol && req.get ? `${req.protocol}://${req.get("host")}${req.originalUrl}` : req.url ?? "";
|
|
2019
|
+
const requirement2 = buildPaymentRequirement(resolved.paymentConfig, resource);
|
|
2020
|
+
return res.status(402).set("Payment-Required", JSON.stringify(requirement2)).json({
|
|
2021
|
+
error: "Payment Required",
|
|
2022
|
+
paymentRequirement: requirement2
|
|
2023
|
+
});
|
|
2024
|
+
}
|
|
2025
|
+
let payment;
|
|
2026
|
+
try {
|
|
2027
|
+
payment = typeof paymentHeader === "string" ? JSON.parse(paymentHeader) : paymentHeader;
|
|
2028
|
+
} catch {
|
|
2029
|
+
return res.status(400).json({ error: "Malformed X-PAYMENT header: invalid JSON" });
|
|
2030
|
+
}
|
|
2031
|
+
const payload = payment?.payload;
|
|
2032
|
+
const toAddress = typeof payload?.toAddress === "string" ? payload.toAddress : null;
|
|
2033
|
+
const amount = typeof payload?.amount === "string" ? payload.amount : null;
|
|
2034
|
+
const asset = typeof payload?.asset === "string" ? payload.asset : null;
|
|
2035
|
+
if (!toAddress || !amount || !asset) {
|
|
2036
|
+
return res.status(400).json({ error: "Malformed X-PAYMENT payload: missing required fields" });
|
|
2037
|
+
}
|
|
2038
|
+
const requirement = buildPaymentRequirement(resolved.paymentConfig, "");
|
|
2039
|
+
const normalizeAddr = (a) => a.toLowerCase().replace(/^0x0+/, "0x");
|
|
2040
|
+
if (normalizeAddr(toAddress) !== normalizeAddr(resolved.paymentConfig.payTo)) {
|
|
2041
|
+
return res.status(402).json({
|
|
2042
|
+
error: "Payment recipient mismatch",
|
|
2043
|
+
reason: `Expected ${resolved.paymentConfig.payTo}, got ${toAddress}`
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
let paidAmount;
|
|
2047
|
+
try {
|
|
2048
|
+
paidAmount = BigInt(amount);
|
|
2049
|
+
} catch {
|
|
2050
|
+
return res.status(400).json({ error: "Malformed X-PAYMENT payload", reason: "amount must be an integer string" });
|
|
2051
|
+
}
|
|
2052
|
+
if (paidAmount < BigInt(requirement.maxAmountRequired)) {
|
|
2053
|
+
return res.status(402).json({
|
|
2054
|
+
error: "Payment amount insufficient",
|
|
2055
|
+
reason: `Required ${requirement.maxAmountRequired} base units, got ${amount}`
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
if (normalizeAddr(asset) !== normalizeAddr(requirement.asset)) {
|
|
2059
|
+
return res.status(402).json({
|
|
2060
|
+
error: "Payment asset mismatch",
|
|
2061
|
+
reason: `Expected USDC, got ${asset}`
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
try {
|
|
2065
|
+
const verified = await resolved.facilitator.verify(payment);
|
|
2066
|
+
if (!verified.isValid) {
|
|
2067
|
+
return res.status(402).json({
|
|
2068
|
+
error: "Payment verification failed",
|
|
2069
|
+
reason: verified.invalidReason
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
if (!resolved.verifyOnly) {
|
|
2073
|
+
const settled = await resolved.facilitator.settle(payment);
|
|
2074
|
+
if (!settled.success) {
|
|
2075
|
+
return res.status(402).json({
|
|
2076
|
+
error: "Payment settlement failed",
|
|
2077
|
+
reason: settled.errorReason
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
req.x402 = {
|
|
2081
|
+
txHash: settled.transactionHash,
|
|
2082
|
+
payment,
|
|
2083
|
+
networkId: settled.networkId
|
|
2084
|
+
};
|
|
2085
|
+
} else {
|
|
2086
|
+
req.x402 = { payment };
|
|
2087
|
+
}
|
|
2088
|
+
} catch (err) {
|
|
2089
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2090
|
+
return res.status(500).json({
|
|
2091
|
+
error: "Payment processing error",
|
|
2092
|
+
reason: message
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
next();
|
|
2096
|
+
};
|
|
2097
|
+
}
|
|
1493
2098
|
|
|
1494
|
-
export { ChipiBrowserSDK, ChipiClient, ChipiSDK, ChipiServerSDK, ChipiSessions, ChipiSkuPurchases, ChipiSkus, ChipiTransactions, ChipiWallets, decryptPrivateKey, encryptPrivateKey };
|
|
2099
|
+
export { ChipiBrowserSDK, ChipiClient, ChipiPaymasterAdapter, ChipiSDK, ChipiServerSDK, ChipiSessions, ChipiSkuPurchases, ChipiSkus, ChipiTransactions, ChipiWallets, X402Facilitator, decryptPrivateKey, encryptPrivateKey, signTypedData, x402Middleware };
|
|
1495
2100
|
//# sourceMappingURL=index.mjs.map
|
|
1496
2101
|
//# sourceMappingURL=index.mjs.map
|