@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.js
CHANGED
|
@@ -4,6 +4,7 @@ var shared = require('@chipi-stack/shared');
|
|
|
4
4
|
var CryptoES = require('crypto-es');
|
|
5
5
|
var starknet = require('starknet');
|
|
6
6
|
var types = require('@chipi-stack/types');
|
|
7
|
+
var core = require('@chipi-stack/core');
|
|
7
8
|
|
|
8
9
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
10
|
|
|
@@ -19,7 +20,7 @@ var ChipiClient = class {
|
|
|
19
20
|
this.customAlphaUrl = config.alphaUrl;
|
|
20
21
|
this.baseUrl = this.getBaseUrl();
|
|
21
22
|
this.nodeUrl = config.nodeUrl || shared.STARKNET_NETWORKS.MAINNET;
|
|
22
|
-
this.sdkVersion = "
|
|
23
|
+
this.sdkVersion = "14.0.0";
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Get the API public key (for internal SDK use)
|
|
@@ -30,10 +31,10 @@ var ChipiClient = class {
|
|
|
30
31
|
getBaseUrl() {
|
|
31
32
|
const version = `v${shared.API_VERSIONING.VERSION}`;
|
|
32
33
|
if (this.customAlphaUrl) {
|
|
33
|
-
const cleanUrl = this.customAlphaUrl.replace(/\/v\d
|
|
34
|
+
const cleanUrl = this.customAlphaUrl.replace(/\/v\d+\/?$/, "").replace(/\/$/, "");
|
|
34
35
|
return `${cleanUrl}/${version}`;
|
|
35
36
|
}
|
|
36
|
-
return `https://
|
|
37
|
+
return `https://api.chipipay.com/${version}`;
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
39
40
|
* Add API version query parameters to track the API version being used
|
|
@@ -100,7 +101,6 @@ var ChipiClient = class {
|
|
|
100
101
|
});
|
|
101
102
|
const data = await response.json();
|
|
102
103
|
if (!response.ok) {
|
|
103
|
-
console.error("there was an error", data);
|
|
104
104
|
const errorData = shared.validateErrorResponse(data);
|
|
105
105
|
throw new shared.ChipiApiError(
|
|
106
106
|
errorData.message,
|
|
@@ -194,16 +194,26 @@ var ChipiClient = class {
|
|
|
194
194
|
}
|
|
195
195
|
};
|
|
196
196
|
var encryptPrivateKey = (privateKey, password) => {
|
|
197
|
+
if (!privateKey || !password) {
|
|
198
|
+
throw new Error("Private key and password are required");
|
|
199
|
+
}
|
|
197
200
|
return CryptoES__default.default.AES.encrypt(privateKey, password).toString();
|
|
198
201
|
};
|
|
199
202
|
var decryptPrivateKey = (encryptedPrivateKey, password) => {
|
|
203
|
+
if (!encryptedPrivateKey || !password) {
|
|
204
|
+
throw new Error("Encrypted private key and password are required");
|
|
205
|
+
}
|
|
200
206
|
try {
|
|
201
207
|
const bytes = CryptoES__default.default.AES.decrypt(encryptedPrivateKey, password);
|
|
202
208
|
const decrypted = bytes.toString(CryptoES__default.default.enc.Utf8);
|
|
203
|
-
if (!decrypted)
|
|
209
|
+
if (!decrypted) {
|
|
210
|
+
throw new Error("Decryption resulted in empty string");
|
|
211
|
+
}
|
|
204
212
|
return decrypted;
|
|
205
213
|
} catch (error) {
|
|
206
|
-
|
|
214
|
+
if (error instanceof Error) {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
207
217
|
throw new Error(`Decryption failed: ${error}`);
|
|
208
218
|
}
|
|
209
219
|
};
|
|
@@ -232,6 +242,7 @@ var ChipiWallets = class {
|
|
|
232
242
|
userId,
|
|
233
243
|
walletType = "CHIPI",
|
|
234
244
|
// Default to CHIPI wallet
|
|
245
|
+
classHash: customClassHash,
|
|
235
246
|
usePasskey,
|
|
236
247
|
chain
|
|
237
248
|
} = params;
|
|
@@ -247,11 +258,8 @@ var ChipiWallets = class {
|
|
|
247
258
|
const provider = new starknet.RpcProvider({ nodeUrl: rpcUrl });
|
|
248
259
|
const privateKeyAX = this.getPrivateKeyAX();
|
|
249
260
|
const starkKeyPubAX = starknet.ec.starkCurve.getStarkKey(privateKeyAX);
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
walletType,
|
|
253
|
-
starkKeyPubAX
|
|
254
|
-
);
|
|
261
|
+
const constructorCallData = this.buildConstructorCallData(walletType, starkKeyPubAX);
|
|
262
|
+
const accountClassHash = customClassHash ?? (walletType === "READY" ? shared.WALLET_CLASS_HASHES.READY : shared.WALLET_CLASS_HASHES.CHIPI);
|
|
255
263
|
const publicKey = starknet.hash.calculateContractAddressFromHash(
|
|
256
264
|
starkKeyPubAX,
|
|
257
265
|
accountClassHash,
|
|
@@ -266,8 +274,8 @@ var ChipiWallets = class {
|
|
|
266
274
|
chain,
|
|
267
275
|
publicKey,
|
|
268
276
|
walletType,
|
|
269
|
-
starkKeyPubAX
|
|
270
|
-
|
|
277
|
+
starkKeyPubAX,
|
|
278
|
+
...customClassHash && { classHash: customClassHash }
|
|
271
279
|
}
|
|
272
280
|
});
|
|
273
281
|
const { typedData: typedData2, accountClassHash: accountClassHashResponse } = typedDataResponse;
|
|
@@ -363,7 +371,7 @@ var ChipiWallets = class {
|
|
|
363
371
|
if (err instanceof shared.ChipiApiError && err.status === 404) {
|
|
364
372
|
return null;
|
|
365
373
|
}
|
|
366
|
-
throw
|
|
374
|
+
throw err;
|
|
367
375
|
}
|
|
368
376
|
}
|
|
369
377
|
async updateWalletEncryption(params, bearerToken) {
|
|
@@ -391,7 +399,121 @@ var ChipiWallets = class {
|
|
|
391
399
|
});
|
|
392
400
|
return getBalanceResponse;
|
|
393
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Prepare a wallet upgrade (e.g., READY → CHIPI or CHIPI v29 → v33).
|
|
404
|
+
* Returns typed data that must be signed by the wallet owner.
|
|
405
|
+
*/
|
|
406
|
+
async prepareWalletUpgrade(params, bearerToken) {
|
|
407
|
+
return this.client.post({
|
|
408
|
+
endpoint: `${shared.API_ENDPOINTS.CHIPI_WALLETS}/prepare-upgrade`,
|
|
409
|
+
bearerToken,
|
|
410
|
+
body: params
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Execute a wallet upgrade after the owner has signed the typed data.
|
|
415
|
+
*/
|
|
416
|
+
async executeWalletUpgrade(params, bearerToken) {
|
|
417
|
+
return this.client.post({
|
|
418
|
+
endpoint: `${shared.API_ENDPOINTS.CHIPI_WALLETS}/upgrade`,
|
|
419
|
+
bearerToken,
|
|
420
|
+
body: params
|
|
421
|
+
});
|
|
422
|
+
}
|
|
394
423
|
};
|
|
424
|
+
function unwrapPrepareResponse(response) {
|
|
425
|
+
if (!response || typeof response !== "object") {
|
|
426
|
+
throw new Error(
|
|
427
|
+
"Invalid response from /transactions/prepare-typed-data: response is not an object"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if ("typedData" in response) {
|
|
431
|
+
const resp = response;
|
|
432
|
+
if (!resp.typedData || typeof resp.typedData !== "object" || !("types" in resp.typedData) || !("message" in resp.typedData)) {
|
|
433
|
+
throw new Error(
|
|
434
|
+
"Invalid response from /transactions/prepare-typed-data: typedData structure is invalid"
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
return { typedData: resp.typedData, walletType: resp.walletType };
|
|
438
|
+
}
|
|
439
|
+
if ("types" in response && "message" in response) {
|
|
440
|
+
return { typedData: response };
|
|
441
|
+
}
|
|
442
|
+
const apiMessage = typeof response.message === "string" ? response.message : void 0;
|
|
443
|
+
throw new Error(
|
|
444
|
+
apiMessage ?? "Invalid response from /transactions/prepare-typed-data: typedData is missing"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
function normalizeSignature(signature) {
|
|
448
|
+
if (Array.isArray(signature)) {
|
|
449
|
+
if (signature.length < 2) {
|
|
450
|
+
throw new Error(`Array signature must have at least 2 elements, got ${signature.length}`);
|
|
451
|
+
}
|
|
452
|
+
const r = signature[0];
|
|
453
|
+
const s = signature[1];
|
|
454
|
+
if (r == null || s == null) {
|
|
455
|
+
throw new Error(`Signature elements must not be null/undefined. Got r=${r}, s=${s}`);
|
|
456
|
+
}
|
|
457
|
+
return { r: String(r), s: String(s) };
|
|
458
|
+
}
|
|
459
|
+
if (signature && typeof signature === "object" && "r" in signature && "s" in signature) {
|
|
460
|
+
const sig = signature;
|
|
461
|
+
if (sig.r == null || sig.s == null) {
|
|
462
|
+
throw new Error(`Object signature has null/undefined fields. Got r=${sig.r}, s=${sig.s}`);
|
|
463
|
+
}
|
|
464
|
+
return {
|
|
465
|
+
r: String(sig.r),
|
|
466
|
+
s: String(sig.s)
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
throw new Error("Unrecognized signature format from account.signMessage()");
|
|
470
|
+
}
|
|
471
|
+
var ChipiPaymasterAdapter = class {
|
|
472
|
+
constructor(client, bearerToken) {
|
|
473
|
+
this.client = client;
|
|
474
|
+
this.bearerToken = bearerToken;
|
|
475
|
+
}
|
|
476
|
+
async execute({ account, calls }) {
|
|
477
|
+
const classHash = await account.getClassHashAt(account.address);
|
|
478
|
+
const walletType = shared.getWalletTypeFromClassHash(classHash);
|
|
479
|
+
const accountClassHash = walletType ? shared.WALLET_CLASS_HASHES[walletType] : classHash;
|
|
480
|
+
const prepareResponse = await this.client.post({
|
|
481
|
+
endpoint: "/transactions/prepare-typed-data",
|
|
482
|
+
bearerToken: this.bearerToken,
|
|
483
|
+
body: {
|
|
484
|
+
publicKey: account.address,
|
|
485
|
+
calls,
|
|
486
|
+
accountClassHash
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
const { typedData: typedData2, walletType: resolvedWalletType } = unwrapPrepareResponse(prepareResponse);
|
|
490
|
+
const signature = await account.signMessage(typedData2);
|
|
491
|
+
const normalizedSig = normalizeSignature(signature);
|
|
492
|
+
const result = await this.client.post({
|
|
493
|
+
endpoint: "/transactions/execute-sponsored-transaction",
|
|
494
|
+
bearerToken: this.bearerToken,
|
|
495
|
+
body: {
|
|
496
|
+
publicKey: account.address,
|
|
497
|
+
typedData: typedData2,
|
|
498
|
+
userSignature: normalizedSig,
|
|
499
|
+
walletType: resolvedWalletType ?? walletType,
|
|
500
|
+
saveToDatabase: true
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
if (!result || typeof result !== "object") {
|
|
504
|
+
throw new Error(
|
|
505
|
+
"Invalid response from /transactions/execute-sponsored-transaction: response is not an object"
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
const apiMessage = typeof result.message === "string" ? result.message : void 0;
|
|
509
|
+
if (!result.transactionHash) {
|
|
510
|
+
throw new Error(apiMessage ?? "Response does not contain transaction hash");
|
|
511
|
+
}
|
|
512
|
+
return result.transactionHash;
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
// src/execute-paymaster-transaction.ts
|
|
395
517
|
var executePaymasterTransaction = async ({
|
|
396
518
|
params,
|
|
397
519
|
bearerToken,
|
|
@@ -404,14 +526,15 @@ var executePaymasterTransaction = async ({
|
|
|
404
526
|
usePasskey ? "encryptKey is required when using passkey. The passkey authentication should have provided the encryptKey." : "encryptKey is required for transaction execution"
|
|
405
527
|
);
|
|
406
528
|
}
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
529
|
+
const walletType = wallet.walletType ?? "READY";
|
|
530
|
+
const isKnownType = Object.prototype.hasOwnProperty.call(shared.WALLET_RPC_ENDPOINTS, walletType) && Object.prototype.hasOwnProperty.call(shared.WALLET_CLASS_HASHES, walletType);
|
|
531
|
+
const rpcUrl = isKnownType ? shared.WALLET_RPC_ENDPOINTS[walletType] : shared.WALLET_RPC_ENDPOINTS["CHIPI"];
|
|
532
|
+
const accountClassHash = isKnownType ? shared.WALLET_CLASS_HASHES[walletType] : shared.WALLET_CLASS_HASHES["CHIPI"];
|
|
533
|
+
let privateKeyDecrypted;
|
|
534
|
+
try {
|
|
535
|
+
privateKeyDecrypted = decryptPrivateKey(wallet.encryptedPrivateKey, encryptKey);
|
|
536
|
+
} catch {
|
|
537
|
+
throw new Error("Failed to decrypt private key. Verify the encryptKey is correct.");
|
|
415
538
|
}
|
|
416
539
|
const provider = new starknet.RpcProvider({ nodeUrl: rpcUrl });
|
|
417
540
|
const account = new starknet.Account({
|
|
@@ -434,42 +557,47 @@ var executePaymasterTransaction = async ({
|
|
|
434
557
|
);
|
|
435
558
|
}
|
|
436
559
|
let typedData2;
|
|
437
|
-
let
|
|
560
|
+
let resolvedWalletType;
|
|
438
561
|
if ("typedData" in prepareResponse) {
|
|
439
562
|
const resp = prepareResponse;
|
|
440
563
|
typedData2 = resp.typedData;
|
|
441
|
-
walletType
|
|
564
|
+
if (resp.walletType && Object.prototype.hasOwnProperty.call(shared.WALLET_CLASS_HASHES, resp.walletType)) {
|
|
565
|
+
resolvedWalletType = resp.walletType;
|
|
566
|
+
}
|
|
442
567
|
} else if (prepareResponse && "types" in prepareResponse && "message" in prepareResponse) {
|
|
443
568
|
typedData2 = prepareResponse;
|
|
444
569
|
} else {
|
|
445
|
-
const
|
|
570
|
+
const apiMessage2 = prepareResponse && typeof prepareResponse.message === "string" ? prepareResponse.message : void 0;
|
|
446
571
|
throw new Error(
|
|
447
|
-
|
|
572
|
+
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."
|
|
448
573
|
);
|
|
449
574
|
}
|
|
450
|
-
if (!typedData2 || typeof typedData2 !== "object" || !("message" in typedData2)) {
|
|
575
|
+
if (!typedData2 || typeof typedData2 !== "object" || !("types" in typedData2) || !("message" in typedData2)) {
|
|
451
576
|
throw new Error(
|
|
452
|
-
"Invalid typedData received from backend: missing 'message' field. This may occur for older STARKNET wallets that don't support paymaster transactions."
|
|
577
|
+
"Invalid typedData received from backend: missing 'types' or 'message' field. This may occur for older STARKNET wallets that don't support paymaster transactions."
|
|
453
578
|
);
|
|
454
579
|
}
|
|
455
580
|
const userSignature = await account.signMessage(typedData2);
|
|
581
|
+
const normalizedSig = normalizeSignature(userSignature);
|
|
456
582
|
const result = await client.post({
|
|
457
583
|
endpoint: "/transactions/execute-sponsored-transaction",
|
|
458
584
|
bearerToken,
|
|
459
585
|
body: {
|
|
460
586
|
publicKey: wallet.publicKey,
|
|
461
587
|
typedData: typedData2,
|
|
462
|
-
userSignature:
|
|
463
|
-
r: userSignature.r.toString(),
|
|
464
|
-
s: userSignature.s.toString(),
|
|
465
|
-
recovery: userSignature.recovery
|
|
466
|
-
},
|
|
588
|
+
userSignature: normalizedSig,
|
|
467
589
|
saveToDatabase,
|
|
468
|
-
walletType
|
|
590
|
+
walletType: resolvedWalletType ?? walletType
|
|
469
591
|
}
|
|
470
592
|
});
|
|
471
|
-
if (!result
|
|
472
|
-
throw new Error(
|
|
593
|
+
if (!result || typeof result !== "object") {
|
|
594
|
+
throw new Error(
|
|
595
|
+
"Invalid response from /transactions/execute-sponsored-transaction: response is not an object"
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
const apiMessage = typeof result.message === "string" ? result.message : void 0;
|
|
599
|
+
if (typeof result.transactionHash !== "string" || result.transactionHash.trim().length === 0) {
|
|
600
|
+
throw new Error(apiMessage ?? "Response does not contain transaction hash");
|
|
473
601
|
}
|
|
474
602
|
return result.transactionHash;
|
|
475
603
|
} catch (error) {
|
|
@@ -484,16 +612,13 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
484
612
|
}) => {
|
|
485
613
|
const { encryptKey, wallet, session, calls, saveToDatabase } = params;
|
|
486
614
|
if (wallet.walletType !== "CHIPI") {
|
|
487
|
-
console.error(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
hint: "Session keys only work with CHIPI wallets (SNIP-9 compatible)"
|
|
495
|
-
}
|
|
496
|
-
);
|
|
615
|
+
console.error("[ChipiSDK:Session:Execute] Invalid wallet type for session execution", {
|
|
616
|
+
provided: wallet.walletType,
|
|
617
|
+
required: "CHIPI",
|
|
618
|
+
expectedClassHash: shared.WALLET_CLASS_HASHES.CHIPI,
|
|
619
|
+
walletAddress: wallet.publicKey?.slice(0, 15) + "...",
|
|
620
|
+
hint: "Session keys only work with CHIPI wallets (SNIP-9 compatible)"
|
|
621
|
+
});
|
|
497
622
|
throw new shared.ChipiSessionError(
|
|
498
623
|
`Session execution requires CHIPI wallet type. Got: "${wallet.walletType || "undefined"}"`,
|
|
499
624
|
shared.SESSION_ERRORS.INVALID_WALLET_TYPE_FOR_SESSION
|
|
@@ -512,18 +637,14 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
512
637
|
);
|
|
513
638
|
}
|
|
514
639
|
try {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
encryptKey
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
sessionPubKey: session.publicKey?.slice(0, 15) + "...",
|
|
524
|
-
hint: "Ensure the encryptKey matches the one used when creating the session"
|
|
525
|
-
}
|
|
526
|
-
);
|
|
640
|
+
let sessionPrivateKey;
|
|
641
|
+
try {
|
|
642
|
+
sessionPrivateKey = decryptPrivateKey(session.encryptedPrivateKey, encryptKey);
|
|
643
|
+
} catch {
|
|
644
|
+
console.error("[ChipiSDK:Session:Execute] Failed to decrypt session private key", {
|
|
645
|
+
sessionPubKey: session.publicKey?.slice(0, 15) + "...",
|
|
646
|
+
hint: "Ensure the encryptKey matches the one used when creating the session"
|
|
647
|
+
});
|
|
527
648
|
throw new shared.ChipiSessionError(
|
|
528
649
|
"Failed to decrypt session private key. Verify the encryptKey is correct.",
|
|
529
650
|
shared.SESSION_ERRORS.SESSION_DECRYPTION_FAILED
|
|
@@ -542,7 +663,7 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
542
663
|
);
|
|
543
664
|
}
|
|
544
665
|
const accountClassHash = shared.WALLET_CLASS_HASHES.CHIPI;
|
|
545
|
-
const
|
|
666
|
+
const prepareResponse = await client.post({
|
|
546
667
|
endpoint: "/transactions/prepare-typed-data",
|
|
547
668
|
bearerToken,
|
|
548
669
|
body: {
|
|
@@ -552,7 +673,31 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
552
673
|
walletType: "CHIPI"
|
|
553
674
|
}
|
|
554
675
|
});
|
|
555
|
-
|
|
676
|
+
if (!prepareResponse || typeof prepareResponse !== "object") {
|
|
677
|
+
throw new shared.ChipiSessionError(
|
|
678
|
+
"Invalid response from /transactions/prepare-typed-data: response is not an object",
|
|
679
|
+
shared.SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
let sessionTypedData;
|
|
683
|
+
if ("typedData" in prepareResponse && prepareResponse.typedData) {
|
|
684
|
+
const candidate = prepareResponse.typedData;
|
|
685
|
+
if (!candidate || typeof candidate !== "object" || !("types" in candidate) || !("message" in candidate)) {
|
|
686
|
+
throw new shared.ChipiSessionError(
|
|
687
|
+
"Invalid response from /transactions/prepare-typed-data: typedData structure is invalid",
|
|
688
|
+
shared.SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
sessionTypedData = candidate;
|
|
692
|
+
} else if ("types" in prepareResponse && "message" in prepareResponse) {
|
|
693
|
+
sessionTypedData = prepareResponse;
|
|
694
|
+
} else {
|
|
695
|
+
throw new shared.ChipiSessionError(
|
|
696
|
+
"Invalid response from /transactions/prepare-typed-data: typedData is missing",
|
|
697
|
+
shared.SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const msgHash = starknet.typedData.getMessageHash(sessionTypedData, wallet.publicKey);
|
|
556
701
|
const { r, s } = starknet.ec.starkCurve.sign(msgHash, sessionPrivateKey);
|
|
557
702
|
const sessionSignature = [
|
|
558
703
|
session.publicKey,
|
|
@@ -565,7 +710,7 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
565
710
|
bearerToken,
|
|
566
711
|
body: {
|
|
567
712
|
publicKey: wallet.publicKey,
|
|
568
|
-
|
|
713
|
+
typedData: sessionTypedData,
|
|
569
714
|
// Session signature format - array of 4 hex strings
|
|
570
715
|
userSignature: sessionSignature,
|
|
571
716
|
saveToDatabase,
|
|
@@ -574,14 +719,21 @@ var executePaymasterTransactionWithSession = async ({
|
|
|
574
719
|
// Flag to indicate session signature format
|
|
575
720
|
}
|
|
576
721
|
});
|
|
577
|
-
if (!result
|
|
578
|
-
|
|
579
|
-
"
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
722
|
+
if (!result || typeof result !== "object") {
|
|
723
|
+
throw new shared.ChipiSessionError(
|
|
724
|
+
"Invalid response from /transactions/execute-sponsored-transaction: response is not an object",
|
|
725
|
+
shared.SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
if (typeof result.transactionHash !== "string" || result.transactionHash.trim().length === 0) {
|
|
729
|
+
const apiMessage = typeof result.message === "string" ? result.message : void 0;
|
|
730
|
+
console.error("[ChipiSDK:Session:Execute] No transaction hash in response", {
|
|
731
|
+
result
|
|
732
|
+
});
|
|
733
|
+
throw new shared.ChipiSessionError(
|
|
734
|
+
apiMessage ?? "Response does not contain transaction hash",
|
|
735
|
+
shared.SESSION_ERRORS.SESSION_INVALID_RESPONSE
|
|
583
736
|
);
|
|
584
|
-
throw new Error("Response does not contain transaction hash");
|
|
585
737
|
}
|
|
586
738
|
return result.transactionHash;
|
|
587
739
|
} catch (error) {
|
|
@@ -719,6 +871,24 @@ var ChipiTransactions = class {
|
|
|
719
871
|
});
|
|
720
872
|
return response;
|
|
721
873
|
}
|
|
874
|
+
/**
|
|
875
|
+
* Get a single transaction by hash or internal ID
|
|
876
|
+
*/
|
|
877
|
+
async getTransaction(hashOrId, bearerToken) {
|
|
878
|
+
return this.client.get({
|
|
879
|
+
endpoint: `${shared.API_ENDPOINTS.TRANSACTIONS}/${hashOrId}`,
|
|
880
|
+
bearerToken
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Get on-chain transaction status
|
|
885
|
+
*/
|
|
886
|
+
async getTransactionStatus(hash3, bearerToken) {
|
|
887
|
+
return this.client.get({
|
|
888
|
+
endpoint: `${shared.API_ENDPOINTS.TRANSACTIONS}/${hash3}/status`,
|
|
889
|
+
bearerToken
|
|
890
|
+
});
|
|
891
|
+
}
|
|
722
892
|
};
|
|
723
893
|
var ChipiExchanges = class {
|
|
724
894
|
constructor(client) {
|
|
@@ -747,32 +917,21 @@ var ChipiSkuPurchases = class {
|
|
|
747
917
|
* @returns Promise<SkuPurchaseResponse> - The purchase response with transaction details
|
|
748
918
|
*/
|
|
749
919
|
async purchaseSku(params, bearerToken) {
|
|
750
|
-
const {
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
token
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
currency,
|
|
763
|
-
bearerToken
|
|
764
|
-
);
|
|
765
|
-
const tokenAddress = types.STARKNET_CONTRACTS[token].contractAddress;
|
|
766
|
-
if (usdAmount <= 0) {
|
|
767
|
-
throw new Error(
|
|
768
|
-
`Invalid USD amount: ${usdAmount}. Amount must be positive.`
|
|
769
|
-
);
|
|
920
|
+
const { skuId, skuReference, currencyAmount, currency, chain, token, encryptKey, wallet } = params;
|
|
921
|
+
if (chain !== types.Chain.STARKNET) {
|
|
922
|
+
throw new Error(`Unsupported chain for SKU purchases: ${chain}`);
|
|
923
|
+
}
|
|
924
|
+
const tokenConfig = types.STARKNET_CONTRACTS[token];
|
|
925
|
+
if (!tokenConfig?.contractAddress) {
|
|
926
|
+
throw new Error(`Unsupported token for Starknet SKU purchases: ${token}`);
|
|
927
|
+
}
|
|
928
|
+
const usdAmount = await this.exchanges.getUsdAmount(currencyAmount, currency, bearerToken);
|
|
929
|
+
const tokenAddress = tokenConfig.contractAddress;
|
|
930
|
+
if (!Number.isFinite(usdAmount) || usdAmount <= 0) {
|
|
931
|
+
throw new Error(`Invalid USD amount: ${usdAmount}. Amount must be positive.`);
|
|
770
932
|
}
|
|
771
933
|
const minUsdAmount = Math.max(usdAmount, 1e-6);
|
|
772
|
-
const parsedAmount = shared.formatAmount(
|
|
773
|
-
minUsdAmount.toString(),
|
|
774
|
-
types.STARKNET_CONTRACTS[token].decimals
|
|
775
|
-
);
|
|
934
|
+
const parsedAmount = shared.formatAmount(minUsdAmount.toString(), tokenConfig.decimals);
|
|
776
935
|
const calls = [
|
|
777
936
|
{
|
|
778
937
|
contractAddress: tokenAddress,
|
|
@@ -934,6 +1093,12 @@ var ChipiSessions = class {
|
|
|
934
1093
|
*/
|
|
935
1094
|
createSessionKey(params) {
|
|
936
1095
|
const { encryptKey, durationSeconds = shared.SESSION_DEFAULTS.DURATION_SECONDS } = params;
|
|
1096
|
+
if (typeof durationSeconds !== "number" || !Number.isFinite(durationSeconds) || !Number.isInteger(durationSeconds) || durationSeconds <= 0) {
|
|
1097
|
+
throw new shared.ChipiSessionError(
|
|
1098
|
+
`Invalid session duration: ${durationSeconds}. Must be a positive integer.`,
|
|
1099
|
+
shared.SESSION_ERRORS.SESSION_CREATION_FAILED
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
937
1102
|
try {
|
|
938
1103
|
const rawPrivateKey = starknet.ec.starkCurve.utils.randomPrivateKey();
|
|
939
1104
|
const privateKeyHex = this.toHex(rawPrivateKey);
|
|
@@ -947,13 +1112,10 @@ var ChipiSessions = class {
|
|
|
947
1112
|
};
|
|
948
1113
|
} catch (error) {
|
|
949
1114
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
950
|
-
console.error(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
durationRequested: durationSeconds
|
|
955
|
-
}
|
|
956
|
-
);
|
|
1115
|
+
console.error("[ChipiSDK:Session:Create] Failed to generate session key", {
|
|
1116
|
+
error: err.message,
|
|
1117
|
+
durationRequested: durationSeconds
|
|
1118
|
+
});
|
|
957
1119
|
throw new shared.ChipiSessionError(
|
|
958
1120
|
`Failed to create session key: ${err.message}`,
|
|
959
1121
|
shared.SESSION_ERRORS.SESSION_CREATION_FAILED
|
|
@@ -988,22 +1150,15 @@ var ChipiSessions = class {
|
|
|
988
1150
|
*/
|
|
989
1151
|
async addSessionKeyToContract(params, bearerToken) {
|
|
990
1152
|
const { encryptKey, wallet, sessionConfig } = params;
|
|
991
|
-
this.validateChipiWallet(
|
|
992
|
-
wallet.walletType,
|
|
993
|
-
"AddToContract",
|
|
994
|
-
wallet.publicKey
|
|
995
|
-
);
|
|
1153
|
+
this.validateChipiWallet(wallet.walletType, "AddToContract", wallet.publicKey);
|
|
996
1154
|
try {
|
|
997
|
-
console.log(
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
allowedEntrypoints: sessionConfig.allowedEntrypoints.length
|
|
1005
|
-
}
|
|
1006
|
-
);
|
|
1155
|
+
console.log("[ChipiSDK:Session:AddToContract] Registering session on-chain", {
|
|
1156
|
+
walletAddress: wallet.publicKey.slice(0, 15) + "...",
|
|
1157
|
+
sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "...",
|
|
1158
|
+
validUntil: new Date(sessionConfig.validUntil * 1e3).toISOString(),
|
|
1159
|
+
maxCalls: sessionConfig.maxCalls,
|
|
1160
|
+
allowedEntrypoints: sessionConfig.allowedEntrypoints.length
|
|
1161
|
+
});
|
|
1007
1162
|
const calldata = [
|
|
1008
1163
|
sessionConfig.sessionPublicKey,
|
|
1009
1164
|
starknet.num.toHex(sessionConfig.validUntil),
|
|
@@ -1029,13 +1184,10 @@ var ChipiSessions = class {
|
|
|
1029
1184
|
bearerToken,
|
|
1030
1185
|
client: this.client
|
|
1031
1186
|
});
|
|
1032
|
-
console.log(
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "..."
|
|
1037
|
-
}
|
|
1038
|
-
);
|
|
1187
|
+
console.log("[ChipiSDK:Session:AddToContract] Session registered successfully", {
|
|
1188
|
+
txHash,
|
|
1189
|
+
sessionPubKey: sessionConfig.sessionPublicKey.slice(0, 15) + "..."
|
|
1190
|
+
});
|
|
1039
1191
|
return txHash;
|
|
1040
1192
|
} catch (error) {
|
|
1041
1193
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
@@ -1183,17 +1335,33 @@ var ChipiSessions = class {
|
|
|
1183
1335
|
return sessionData;
|
|
1184
1336
|
} catch (error) {
|
|
1185
1337
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1186
|
-
|
|
1338
|
+
const sessionNotFoundPatterns = [
|
|
1339
|
+
/entry point.*not found/i,
|
|
1340
|
+
/requested entrypoint was not found/i,
|
|
1341
|
+
/invalid message selector/i,
|
|
1342
|
+
/contract not found/i,
|
|
1343
|
+
/is not deployed/i,
|
|
1344
|
+
/revert.*get_session_data/i
|
|
1345
|
+
];
|
|
1346
|
+
const isSessionNotFound = sessionNotFoundPatterns.some((p) => p.test(err.message));
|
|
1347
|
+
if (isSessionNotFound) {
|
|
1348
|
+
console.log("[ChipiSDK:Session:GetData] Session not found on contract", {
|
|
1349
|
+
walletAddress: walletAddress?.slice(0, 15) + "...",
|
|
1350
|
+
sessionPubKey: sessionPublicKey?.slice(0, 15) + "..."
|
|
1351
|
+
});
|
|
1352
|
+
return {
|
|
1353
|
+
isActive: false,
|
|
1354
|
+
validUntil: 0,
|
|
1355
|
+
remainingCalls: 0,
|
|
1356
|
+
allowedEntrypoints: []
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
console.error("[ChipiSDK:Session:GetData] RPC/network error", {
|
|
1187
1360
|
error: err.message,
|
|
1188
1361
|
walletAddress: walletAddress?.slice(0, 15) + "...",
|
|
1189
1362
|
sessionPubKey: sessionPublicKey?.slice(0, 15) + "..."
|
|
1190
1363
|
});
|
|
1191
|
-
|
|
1192
|
-
isActive: false,
|
|
1193
|
-
validUntil: 0,
|
|
1194
|
-
remainingCalls: 0,
|
|
1195
|
-
allowedEntrypoints: []
|
|
1196
|
-
};
|
|
1364
|
+
throw err;
|
|
1197
1365
|
}
|
|
1198
1366
|
}
|
|
1199
1367
|
/**
|
|
@@ -1257,6 +1425,240 @@ var ChipiSessions = class {
|
|
|
1257
1425
|
});
|
|
1258
1426
|
}
|
|
1259
1427
|
};
|
|
1428
|
+
var InMemoryNonceStore = class {
|
|
1429
|
+
constructor() {
|
|
1430
|
+
this.used = /* @__PURE__ */ new Set();
|
|
1431
|
+
}
|
|
1432
|
+
has(nonce) {
|
|
1433
|
+
return this.used.has(nonce);
|
|
1434
|
+
}
|
|
1435
|
+
consume(nonce) {
|
|
1436
|
+
if (this.used.has(nonce)) return false;
|
|
1437
|
+
this.used.add(nonce);
|
|
1438
|
+
return true;
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
var X402Facilitator = class _X402Facilitator {
|
|
1442
|
+
constructor(client, bearerToken, config) {
|
|
1443
|
+
this.client = client;
|
|
1444
|
+
this.bearerToken = bearerToken;
|
|
1445
|
+
this.nonceStore = config?.nonceStore ?? new InMemoryNonceStore();
|
|
1446
|
+
this.provider = config?.provider;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Factory method for simpler instantiation.
|
|
1450
|
+
*
|
|
1451
|
+
* @example
|
|
1452
|
+
* ```typescript
|
|
1453
|
+
* const facilitator = X402Facilitator.create({
|
|
1454
|
+
* apiKey: process.env.CHIPI_API_KEY!,
|
|
1455
|
+
* bearerToken: process.env.CHIPI_BEARER_TOKEN!,
|
|
1456
|
+
* });
|
|
1457
|
+
* ```
|
|
1458
|
+
*/
|
|
1459
|
+
static create(options) {
|
|
1460
|
+
const client = new ChipiClient({ apiPublicKey: options.apiKey });
|
|
1461
|
+
return new _X402Facilitator(client, options.bearerToken, {
|
|
1462
|
+
nonceStore: options.nonceStore,
|
|
1463
|
+
provider: options.provider
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Verify a payment signature is valid (doesn't execute the transfer).
|
|
1468
|
+
*
|
|
1469
|
+
* Checks:
|
|
1470
|
+
* 1. Nonce has not been used (replay protection)
|
|
1471
|
+
* 2. Payment has not expired (validUntil > now)
|
|
1472
|
+
* 3. Asset is USDC mainnet
|
|
1473
|
+
* 4. Amount is positive
|
|
1474
|
+
* 5. Addresses are non-empty
|
|
1475
|
+
*/
|
|
1476
|
+
async verify(payment) {
|
|
1477
|
+
const { payload } = payment;
|
|
1478
|
+
if (payment.x402Version !== 1) {
|
|
1479
|
+
return { isValid: false, invalidReason: `Unsupported x402 version: ${payment.x402Version}` };
|
|
1480
|
+
}
|
|
1481
|
+
if (payment.network !== "starknet-mainnet") {
|
|
1482
|
+
return { isValid: false, invalidReason: `Unsupported network: ${payment.network}` };
|
|
1483
|
+
}
|
|
1484
|
+
if (await this.nonceStore.has(payload.nonce)) {
|
|
1485
|
+
return { isValid: false, invalidReason: "Nonce already used (replay)" };
|
|
1486
|
+
}
|
|
1487
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1488
|
+
if (payload.validUntil <= now) {
|
|
1489
|
+
return { isValid: false, invalidReason: "Payment has expired (validUntil in the past)" };
|
|
1490
|
+
}
|
|
1491
|
+
const normalizedAsset = payload.asset.toLowerCase().replace(/^0x0+/, "0x");
|
|
1492
|
+
const normalizedUsdc = shared.CONTRACT_ADDRESSES.USDC_MAINNET.toLowerCase().replace(/^0x0+/, "0x");
|
|
1493
|
+
if (normalizedAsset !== normalizedUsdc) {
|
|
1494
|
+
return {
|
|
1495
|
+
isValid: false,
|
|
1496
|
+
invalidReason: `Unsupported asset: ${payload.asset}. Only USDC is accepted.`
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
let amount;
|
|
1500
|
+
try {
|
|
1501
|
+
amount = BigInt(payload.amount);
|
|
1502
|
+
} catch {
|
|
1503
|
+
return { isValid: false, invalidReason: "Invalid payment amount format" };
|
|
1504
|
+
}
|
|
1505
|
+
if (amount <= 0n) {
|
|
1506
|
+
return { isValid: false, invalidReason: "Payment amount must be positive" };
|
|
1507
|
+
}
|
|
1508
|
+
if (!payload.fromAddress || !payload.toAddress) {
|
|
1509
|
+
return { isValid: false, invalidReason: "Missing fromAddress or toAddress" };
|
|
1510
|
+
}
|
|
1511
|
+
const sigResult = await this.verifySignature(payment);
|
|
1512
|
+
if (!sigResult.valid) {
|
|
1513
|
+
return { isValid: false, invalidReason: sigResult.reason };
|
|
1514
|
+
}
|
|
1515
|
+
return { isValid: true };
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Verify the SNIP-12 signature on a payment payload.
|
|
1519
|
+
*
|
|
1520
|
+
* If an RPC provider is configured, reconstructs the typed data, computes the
|
|
1521
|
+
* message hash, and calls `is_valid_signature()` on the sender's account contract.
|
|
1522
|
+
* Otherwise falls back to a presence-only check (backwards compatible).
|
|
1523
|
+
*/
|
|
1524
|
+
async verifySignature(payment) {
|
|
1525
|
+
if (!this.provider) {
|
|
1526
|
+
if (!payment.payload?.signature?.r || !payment.payload?.signature?.s) {
|
|
1527
|
+
return { valid: false, reason: "Missing signature" };
|
|
1528
|
+
}
|
|
1529
|
+
return { valid: true };
|
|
1530
|
+
}
|
|
1531
|
+
const payload = payment.payload;
|
|
1532
|
+
if (!payload?.signature?.r || !payload?.signature?.s) {
|
|
1533
|
+
return { valid: false, reason: "Missing signature" };
|
|
1534
|
+
}
|
|
1535
|
+
const paymentTypedData = core.buildX402TypedData({
|
|
1536
|
+
from_address: payload.fromAddress,
|
|
1537
|
+
to_address: payload.toAddress,
|
|
1538
|
+
amount: payload.amount,
|
|
1539
|
+
asset: payload.asset,
|
|
1540
|
+
valid_until: String(payload.validUntil),
|
|
1541
|
+
nonce: payload.nonce
|
|
1542
|
+
});
|
|
1543
|
+
const msgHash = starknet.typedData.getMessageHash(paymentTypedData, payload.fromAddress);
|
|
1544
|
+
try {
|
|
1545
|
+
const result = await this.provider.callContract({
|
|
1546
|
+
contractAddress: payload.fromAddress,
|
|
1547
|
+
entrypoint: "is_valid_signature",
|
|
1548
|
+
calldata: [msgHash, "2", payload.signature.r, payload.signature.s]
|
|
1549
|
+
});
|
|
1550
|
+
const VALID = "0x56414c4944";
|
|
1551
|
+
if (result[0] === VALID || result[0] === BigInt(VALID).toString()) {
|
|
1552
|
+
return { valid: true };
|
|
1553
|
+
}
|
|
1554
|
+
return { valid: false, reason: "Invalid SNIP-12 signature" };
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1557
|
+
return { valid: false, reason: `Signature verification failed: ${message}` };
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Settle a verified payment by executing the USDC transfer via paymaster.
|
|
1562
|
+
*
|
|
1563
|
+
* IMPORTANT: Mark nonce as consumed BEFORE executing to prevent concurrent replays.
|
|
1564
|
+
* If the transfer fails after nonce consumption, the nonce is still burned
|
|
1565
|
+
* (caller should implement idempotency at a higher level if needed).
|
|
1566
|
+
*/
|
|
1567
|
+
async settle(payment) {
|
|
1568
|
+
const { payload } = payment;
|
|
1569
|
+
const verification = await this.verify(payment);
|
|
1570
|
+
if (!verification.isValid) {
|
|
1571
|
+
return { success: false, errorReason: verification.invalidReason };
|
|
1572
|
+
}
|
|
1573
|
+
const consumed = await this.nonceStore.consume(payload.nonce);
|
|
1574
|
+
if (!consumed) {
|
|
1575
|
+
return { success: false, errorReason: "Nonce already consumed (concurrent replay)" };
|
|
1576
|
+
}
|
|
1577
|
+
try {
|
|
1578
|
+
const amountU256 = starknet.uint256.bnToUint256(BigInt(payload.amount));
|
|
1579
|
+
const transferCall = {
|
|
1580
|
+
contractAddress: payload.asset,
|
|
1581
|
+
entrypoint: "transfer",
|
|
1582
|
+
calldata: [payload.toAddress, amountU256.low.toString(), amountU256.high.toString()]
|
|
1583
|
+
};
|
|
1584
|
+
const prepareResponse = await this.client.post({
|
|
1585
|
+
endpoint: "/transactions/prepare-typed-data",
|
|
1586
|
+
bearerToken: this.bearerToken,
|
|
1587
|
+
body: {
|
|
1588
|
+
publicKey: payload.fromAddress,
|
|
1589
|
+
calls: [transferCall]
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
const result = await this.client.post({
|
|
1593
|
+
endpoint: "/transactions/execute-sponsored-transaction",
|
|
1594
|
+
bearerToken: this.bearerToken,
|
|
1595
|
+
body: {
|
|
1596
|
+
publicKey: payload.fromAddress,
|
|
1597
|
+
typedData: prepareResponse.typedData,
|
|
1598
|
+
userSignature: payload.signature,
|
|
1599
|
+
walletType: prepareResponse.walletType,
|
|
1600
|
+
saveToDatabase: true
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
if (typeof result?.transactionHash !== "string" || !result.transactionHash.trim()) {
|
|
1604
|
+
return {
|
|
1605
|
+
success: false,
|
|
1606
|
+
errorReason: result?.message ?? "No transaction hash returned from paymaster"
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
return {
|
|
1610
|
+
success: true,
|
|
1611
|
+
transactionHash: result.transactionHash,
|
|
1612
|
+
networkId: "starknet-mainnet"
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1616
|
+
return {
|
|
1617
|
+
success: false,
|
|
1618
|
+
errorReason: `Settlement failed: ${message}`
|
|
1619
|
+
};
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
var signTypedData = async ({
|
|
1624
|
+
params
|
|
1625
|
+
}) => {
|
|
1626
|
+
try {
|
|
1627
|
+
const { encryptKey, wallet, typedData: typedData2 } = params;
|
|
1628
|
+
if (!encryptKey) {
|
|
1629
|
+
throw new Error("encryptKey is required for signing typed data");
|
|
1630
|
+
}
|
|
1631
|
+
if (!wallet || !wallet.encryptedPrivateKey || !wallet.publicKey) {
|
|
1632
|
+
throw new Error("wallet with publicKey and encryptedPrivateKey is required");
|
|
1633
|
+
}
|
|
1634
|
+
if (!typedData2) {
|
|
1635
|
+
throw new Error("typedData is required for signing");
|
|
1636
|
+
}
|
|
1637
|
+
const walletType = wallet.walletType ?? "READY";
|
|
1638
|
+
if (!Object.prototype.hasOwnProperty.call(shared.WALLET_RPC_ENDPOINTS, walletType) || !Object.prototype.hasOwnProperty.call(shared.WALLET_CLASS_HASHES, walletType)) {
|
|
1639
|
+
throw new Error(`Unsupported wallet type: "${String(walletType)}"`);
|
|
1640
|
+
}
|
|
1641
|
+
const rpcUrl = shared.WALLET_RPC_ENDPOINTS[walletType];
|
|
1642
|
+
let privateKeyDecrypted;
|
|
1643
|
+
try {
|
|
1644
|
+
privateKeyDecrypted = decryptPrivateKey(wallet.encryptedPrivateKey, encryptKey);
|
|
1645
|
+
} catch {
|
|
1646
|
+
throw new Error("Failed to decrypt private key. Verify the encryptKey is correct.");
|
|
1647
|
+
}
|
|
1648
|
+
const provider = new starknet.RpcProvider({ nodeUrl: rpcUrl });
|
|
1649
|
+
const account = new starknet.Account({
|
|
1650
|
+
provider,
|
|
1651
|
+
address: wallet.publicKey,
|
|
1652
|
+
signer: privateKeyDecrypted
|
|
1653
|
+
});
|
|
1654
|
+
const signature = await account.signMessage(typedData2);
|
|
1655
|
+
const normalizedSig = normalizeSignature(signature);
|
|
1656
|
+
return normalizedSig;
|
|
1657
|
+
} catch (error) {
|
|
1658
|
+
console.error("Error signing typed data", error);
|
|
1659
|
+
throw error;
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1260
1662
|
|
|
1261
1663
|
// src/chipi-sdk.ts
|
|
1262
1664
|
var ChipiSDK = class {
|
|
@@ -1279,6 +1681,9 @@ var ChipiSDK = class {
|
|
|
1279
1681
|
this.recordSendTransaction = this.recordSendTransaction.bind(this);
|
|
1280
1682
|
this.purchaseSku = this.purchaseSku.bind(this);
|
|
1281
1683
|
this.getUsdAmount = this.getUsdAmount.bind(this);
|
|
1684
|
+
this.signTypedData = this.signTypedData.bind(this);
|
|
1685
|
+
this.createPaymasterAdapter = this.createPaymasterAdapter.bind(this);
|
|
1686
|
+
this.createX402Facilitator = this.createX402Facilitator.bind(this);
|
|
1282
1687
|
}
|
|
1283
1688
|
/**
|
|
1284
1689
|
* Resolve bearer token - uses provided token or falls back to apiSecretKey
|
|
@@ -1292,6 +1697,43 @@ var ChipiSDK = class {
|
|
|
1292
1697
|
}
|
|
1293
1698
|
return token;
|
|
1294
1699
|
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Create a PaymasterAdapter for use with TxBuilder.sendSponsored().
|
|
1702
|
+
*
|
|
1703
|
+
* @example
|
|
1704
|
+
* ```typescript
|
|
1705
|
+
* const sdk = new ChipiServerSDK({ apiPublicKey: "pk_...", apiSecretKey: "sk_..." });
|
|
1706
|
+
* const paymaster = sdk.createPaymasterAdapter();
|
|
1707
|
+
*
|
|
1708
|
+
* const txHash = await new TxBuilder(account, { paymaster })
|
|
1709
|
+
* .transfer(USDC, [{ to, amount }])
|
|
1710
|
+
* .sendSponsored(); // gasless!
|
|
1711
|
+
* ```
|
|
1712
|
+
*/
|
|
1713
|
+
createPaymasterAdapter(bearerToken) {
|
|
1714
|
+
const token = this.resolveBearerToken(bearerToken);
|
|
1715
|
+
return new ChipiPaymasterAdapter(this.client, token);
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Create an X402Facilitator for verifying and settling x402 payments.
|
|
1719
|
+
*
|
|
1720
|
+
* @example
|
|
1721
|
+
* ```typescript
|
|
1722
|
+
* const facilitator = sdk.createX402Facilitator();
|
|
1723
|
+
*
|
|
1724
|
+
* // With custom nonce store (Redis)
|
|
1725
|
+
* const facilitator = sdk.createX402Facilitator(bearerToken, {
|
|
1726
|
+
* nonceStore: {
|
|
1727
|
+
* has: async (nonce) => (await redis.sismember("x402:nonces", nonce)) === 1,
|
|
1728
|
+
* consume: async (nonce) => (await redis.sadd("x402:nonces", nonce)) === 1,
|
|
1729
|
+
* },
|
|
1730
|
+
* });
|
|
1731
|
+
* ```
|
|
1732
|
+
*/
|
|
1733
|
+
createX402Facilitator(bearerToken, config) {
|
|
1734
|
+
const token = this.resolveBearerToken(bearerToken);
|
|
1735
|
+
return new X402Facilitator(this.client, token, config);
|
|
1736
|
+
}
|
|
1295
1737
|
/**
|
|
1296
1738
|
* Execute a gasless transaction
|
|
1297
1739
|
*/
|
|
@@ -1347,6 +1789,29 @@ var ChipiSDK = class {
|
|
|
1347
1789
|
this.resolveBearerToken(bearerToken)
|
|
1348
1790
|
);
|
|
1349
1791
|
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Sign SNIP-12 typed data without executing a transaction.
|
|
1794
|
+
*
|
|
1795
|
+
* Purely local operation: decrypt private key → sign → return { r, s }.
|
|
1796
|
+
* No API calls are made. Useful for x402 payment signatures and other
|
|
1797
|
+
* off-chain signing flows.
|
|
1798
|
+
*
|
|
1799
|
+
* @example
|
|
1800
|
+
* ```typescript
|
|
1801
|
+
* const { r, s } = await sdk.signTypedData({
|
|
1802
|
+
* params: {
|
|
1803
|
+
* encryptKey: pin,
|
|
1804
|
+
* wallet,
|
|
1805
|
+
* typedData: snip12TypedData,
|
|
1806
|
+
* },
|
|
1807
|
+
* });
|
|
1808
|
+
* ```
|
|
1809
|
+
*/
|
|
1810
|
+
async signTypedData({
|
|
1811
|
+
params
|
|
1812
|
+
}) {
|
|
1813
|
+
return signTypedData({ params });
|
|
1814
|
+
}
|
|
1350
1815
|
/**
|
|
1351
1816
|
* Transfer tokens
|
|
1352
1817
|
*/
|
|
@@ -1406,7 +1871,6 @@ var ChipiSDK = class {
|
|
|
1406
1871
|
params,
|
|
1407
1872
|
bearerToken
|
|
1408
1873
|
}) {
|
|
1409
|
-
console.log("createWallet params:", params);
|
|
1410
1874
|
return this.wallets.createWallet({
|
|
1411
1875
|
...params,
|
|
1412
1876
|
bearerToken: this.resolveBearerToken(bearerToken)
|
|
@@ -1422,10 +1886,7 @@ var ChipiSDK = class {
|
|
|
1422
1886
|
});
|
|
1423
1887
|
}
|
|
1424
1888
|
async getTransactionList(query, bearerToken) {
|
|
1425
|
-
return this.transactions.getTransactionList(
|
|
1426
|
-
query,
|
|
1427
|
-
this.resolveBearerToken(bearerToken)
|
|
1428
|
-
);
|
|
1889
|
+
return this.transactions.getTransactionList(query, this.resolveBearerToken(bearerToken));
|
|
1429
1890
|
}
|
|
1430
1891
|
async getWallet(params, bearerToken) {
|
|
1431
1892
|
return this.wallets.getWallet(params, this.resolveBearerToken(bearerToken));
|
|
@@ -1443,19 +1904,13 @@ var ChipiSDK = class {
|
|
|
1443
1904
|
params,
|
|
1444
1905
|
bearerToken
|
|
1445
1906
|
}) {
|
|
1446
|
-
return this.skuPurchases.purchaseSku(
|
|
1447
|
-
params,
|
|
1448
|
-
this.resolveBearerToken(bearerToken)
|
|
1449
|
-
);
|
|
1907
|
+
return this.skuPurchases.purchaseSku(params, this.resolveBearerToken(bearerToken));
|
|
1450
1908
|
}
|
|
1451
1909
|
/**
|
|
1452
1910
|
* Get a SKU purchase by id
|
|
1453
1911
|
*/
|
|
1454
1912
|
async getSkuPurchase(id, bearerToken) {
|
|
1455
|
-
return this.skuPurchases.getSkuPurchase(
|
|
1456
|
-
id,
|
|
1457
|
-
this.resolveBearerToken(bearerToken)
|
|
1458
|
-
);
|
|
1913
|
+
return this.skuPurchases.getSkuPurchase(id, this.resolveBearerToken(bearerToken));
|
|
1459
1914
|
}
|
|
1460
1915
|
/// SKUS
|
|
1461
1916
|
async getSkuList(params, bearerToken) {
|
|
@@ -1464,6 +1919,33 @@ var ChipiSDK = class {
|
|
|
1464
1919
|
async getSku(id, bearerToken) {
|
|
1465
1920
|
return this.skus.getSku(id, this.resolveBearerToken(bearerToken));
|
|
1466
1921
|
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Get a single transaction by hash or internal ID
|
|
1924
|
+
*/
|
|
1925
|
+
async getTransaction(hashOrId, bearerToken) {
|
|
1926
|
+
return this.transactions.getTransaction(hashOrId, this.resolveBearerToken(bearerToken));
|
|
1927
|
+
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Get on-chain transaction status (supports polling for confirmation)
|
|
1930
|
+
*/
|
|
1931
|
+
async getTransactionStatus(hash3, bearerToken) {
|
|
1932
|
+
return this.transactions.getTransactionStatus(hash3, this.resolveBearerToken(bearerToken));
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Prepare a wallet upgrade (e.g., READY → CHIPI or CHIPI v29 → v33).
|
|
1936
|
+
* Returns typed data that must be signed by the wallet owner.
|
|
1937
|
+
* Server-only: requires API secret key.
|
|
1938
|
+
*/
|
|
1939
|
+
async prepareWalletUpgrade(params, bearerToken) {
|
|
1940
|
+
return this.wallets.prepareWalletUpgrade(params, this.resolveBearerToken(bearerToken));
|
|
1941
|
+
}
|
|
1942
|
+
/**
|
|
1943
|
+
* Execute a wallet upgrade after signing.
|
|
1944
|
+
* Server-only: requires API secret key.
|
|
1945
|
+
*/
|
|
1946
|
+
async executeWalletUpgrade(params, bearerToken) {
|
|
1947
|
+
return this.wallets.executeWalletUpgrade(params, this.resolveBearerToken(bearerToken));
|
|
1948
|
+
}
|
|
1467
1949
|
async getUsdAmount(currencyAmount, currency, bearerToken) {
|
|
1468
1950
|
return this.exchanges.getUsdAmount(
|
|
1469
1951
|
currencyAmount,
|
|
@@ -1495,6 +1977,129 @@ var ChipiBrowserSDK = class extends ChipiSDK {
|
|
|
1495
1977
|
// All methods are inherited from ChipiSDK
|
|
1496
1978
|
// bearerToken parameter is required for each authenticated request
|
|
1497
1979
|
};
|
|
1980
|
+
function amountToBaseUnits(amount, decimals) {
|
|
1981
|
+
const [whole, frac = ""] = amount.split(".");
|
|
1982
|
+
const fracPadded = (frac + "0".repeat(decimals)).slice(0, decimals);
|
|
1983
|
+
return String(BigInt(whole + fracPadded));
|
|
1984
|
+
}
|
|
1985
|
+
function buildPaymentRequirement(config, resource) {
|
|
1986
|
+
const amountBaseUnits = amountToBaseUnits(config.amount, shared.TOKEN_DECIMALS.USDC);
|
|
1987
|
+
return {
|
|
1988
|
+
scheme: "exact",
|
|
1989
|
+
network: "starknet-mainnet",
|
|
1990
|
+
maxAmountRequired: amountBaseUnits,
|
|
1991
|
+
resource,
|
|
1992
|
+
description: config.description,
|
|
1993
|
+
payTo: config.payTo,
|
|
1994
|
+
maxTimeoutSeconds: 300,
|
|
1995
|
+
asset: shared.CONTRACT_ADDRESSES.USDC_MAINNET
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function isFlatConfig(config) {
|
|
1999
|
+
return "amount" in config && "recipient" in config;
|
|
2000
|
+
}
|
|
2001
|
+
function resolveConfig(config) {
|
|
2002
|
+
if (isFlatConfig(config)) {
|
|
2003
|
+
return {
|
|
2004
|
+
facilitator: config.facilitator,
|
|
2005
|
+
paymentConfig: {
|
|
2006
|
+
amount: config.amount,
|
|
2007
|
+
payTo: config.recipient,
|
|
2008
|
+
description: config.description
|
|
2009
|
+
},
|
|
2010
|
+
verifyOnly: config.verifyOnly
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
return config;
|
|
2014
|
+
}
|
|
2015
|
+
function x402Middleware(config) {
|
|
2016
|
+
const resolved = resolveConfig(config);
|
|
2017
|
+
return async (req, res, next) => {
|
|
2018
|
+
if (req.method === "OPTIONS") {
|
|
2019
|
+
return next();
|
|
2020
|
+
}
|
|
2021
|
+
const paymentHeader = req.headers?.["x-payment"] ?? req.headers?.["X-PAYMENT"];
|
|
2022
|
+
if (!paymentHeader) {
|
|
2023
|
+
const resource = req.protocol && req.get ? `${req.protocol}://${req.get("host")}${req.originalUrl}` : req.url ?? "";
|
|
2024
|
+
const requirement2 = buildPaymentRequirement(resolved.paymentConfig, resource);
|
|
2025
|
+
return res.status(402).set("Payment-Required", JSON.stringify(requirement2)).json({
|
|
2026
|
+
error: "Payment Required",
|
|
2027
|
+
paymentRequirement: requirement2
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
let payment;
|
|
2031
|
+
try {
|
|
2032
|
+
payment = typeof paymentHeader === "string" ? JSON.parse(paymentHeader) : paymentHeader;
|
|
2033
|
+
} catch {
|
|
2034
|
+
return res.status(400).json({ error: "Malformed X-PAYMENT header: invalid JSON" });
|
|
2035
|
+
}
|
|
2036
|
+
const payload = payment?.payload;
|
|
2037
|
+
const toAddress = typeof payload?.toAddress === "string" ? payload.toAddress : null;
|
|
2038
|
+
const amount = typeof payload?.amount === "string" ? payload.amount : null;
|
|
2039
|
+
const asset = typeof payload?.asset === "string" ? payload.asset : null;
|
|
2040
|
+
if (!toAddress || !amount || !asset) {
|
|
2041
|
+
return res.status(400).json({ error: "Malformed X-PAYMENT payload: missing required fields" });
|
|
2042
|
+
}
|
|
2043
|
+
const requirement = buildPaymentRequirement(resolved.paymentConfig, "");
|
|
2044
|
+
const normalizeAddr = (a) => a.toLowerCase().replace(/^0x0+/, "0x");
|
|
2045
|
+
if (normalizeAddr(toAddress) !== normalizeAddr(resolved.paymentConfig.payTo)) {
|
|
2046
|
+
return res.status(402).json({
|
|
2047
|
+
error: "Payment recipient mismatch",
|
|
2048
|
+
reason: `Expected ${resolved.paymentConfig.payTo}, got ${toAddress}`
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
let paidAmount;
|
|
2052
|
+
try {
|
|
2053
|
+
paidAmount = BigInt(amount);
|
|
2054
|
+
} catch {
|
|
2055
|
+
return res.status(400).json({ error: "Malformed X-PAYMENT payload", reason: "amount must be an integer string" });
|
|
2056
|
+
}
|
|
2057
|
+
if (paidAmount < BigInt(requirement.maxAmountRequired)) {
|
|
2058
|
+
return res.status(402).json({
|
|
2059
|
+
error: "Payment amount insufficient",
|
|
2060
|
+
reason: `Required ${requirement.maxAmountRequired} base units, got ${amount}`
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
if (normalizeAddr(asset) !== normalizeAddr(requirement.asset)) {
|
|
2064
|
+
return res.status(402).json({
|
|
2065
|
+
error: "Payment asset mismatch",
|
|
2066
|
+
reason: `Expected USDC, got ${asset}`
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
try {
|
|
2070
|
+
const verified = await resolved.facilitator.verify(payment);
|
|
2071
|
+
if (!verified.isValid) {
|
|
2072
|
+
return res.status(402).json({
|
|
2073
|
+
error: "Payment verification failed",
|
|
2074
|
+
reason: verified.invalidReason
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
if (!resolved.verifyOnly) {
|
|
2078
|
+
const settled = await resolved.facilitator.settle(payment);
|
|
2079
|
+
if (!settled.success) {
|
|
2080
|
+
return res.status(402).json({
|
|
2081
|
+
error: "Payment settlement failed",
|
|
2082
|
+
reason: settled.errorReason
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
req.x402 = {
|
|
2086
|
+
txHash: settled.transactionHash,
|
|
2087
|
+
payment,
|
|
2088
|
+
networkId: settled.networkId
|
|
2089
|
+
};
|
|
2090
|
+
} else {
|
|
2091
|
+
req.x402 = { payment };
|
|
2092
|
+
}
|
|
2093
|
+
} catch (err) {
|
|
2094
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2095
|
+
return res.status(500).json({
|
|
2096
|
+
error: "Payment processing error",
|
|
2097
|
+
reason: message
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
next();
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1498
2103
|
|
|
1499
2104
|
Object.defineProperty(exports, "Chain", {
|
|
1500
2105
|
enumerable: true,
|
|
@@ -1506,6 +2111,7 @@ Object.defineProperty(exports, "ChainToken", {
|
|
|
1506
2111
|
});
|
|
1507
2112
|
exports.ChipiBrowserSDK = ChipiBrowserSDK;
|
|
1508
2113
|
exports.ChipiClient = ChipiClient;
|
|
2114
|
+
exports.ChipiPaymasterAdapter = ChipiPaymasterAdapter;
|
|
1509
2115
|
exports.ChipiSDK = ChipiSDK;
|
|
1510
2116
|
exports.ChipiServerSDK = ChipiServerSDK;
|
|
1511
2117
|
exports.ChipiSessions = ChipiSessions;
|
|
@@ -1513,7 +2119,10 @@ exports.ChipiSkuPurchases = ChipiSkuPurchases;
|
|
|
1513
2119
|
exports.ChipiSkus = ChipiSkus;
|
|
1514
2120
|
exports.ChipiTransactions = ChipiTransactions;
|
|
1515
2121
|
exports.ChipiWallets = ChipiWallets;
|
|
2122
|
+
exports.X402Facilitator = X402Facilitator;
|
|
1516
2123
|
exports.decryptPrivateKey = decryptPrivateKey;
|
|
1517
2124
|
exports.encryptPrivateKey = encryptPrivateKey;
|
|
2125
|
+
exports.signTypedData = signTypedData;
|
|
2126
|
+
exports.x402Middleware = x402Middleware;
|
|
1518
2127
|
//# sourceMappingURL=index.js.map
|
|
1519
2128
|
//# sourceMappingURL=index.js.map
|