@pafi-dev/issuer 0.3.0-beta.0 → 0.3.0-beta.10
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/dist/index.cjs +134 -752
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -508
- package/dist/index.d.ts +147 -508
- package/dist/index.js +128 -746
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -582,209 +582,103 @@ var RelayError = class extends Error {
|
|
|
582
582
|
};
|
|
583
583
|
|
|
584
584
|
// src/relay/relayService.ts
|
|
585
|
-
import { encodeFunctionData } from "viem";
|
|
586
585
|
import {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
RELAYER_V2_ABI,
|
|
586
|
+
encodeFunctionData,
|
|
587
|
+
erc20Abi
|
|
588
|
+
} from "viem";
|
|
589
|
+
import {
|
|
592
590
|
POINT_TOKEN_V2_ABI,
|
|
593
|
-
buildPartialUserOperation
|
|
591
|
+
buildPartialUserOperation,
|
|
592
|
+
signMintRequest as signMintRequest2
|
|
594
593
|
} from "@pafi-dev/core";
|
|
595
|
-
var DEFAULT_CONFIRMATION_TIMEOUT_MS = 6e4;
|
|
596
594
|
var RelayService = class {
|
|
597
|
-
relayAddress;
|
|
598
|
-
operatorWallet;
|
|
599
|
-
provider;
|
|
600
|
-
confirmationTimeoutMs;
|
|
601
|
-
simulateBeforeSubmit;
|
|
602
|
-
constructor(config) {
|
|
603
|
-
if (!config.relayAddress) {
|
|
604
|
-
throw new Error("RelayService: relayAddress is required");
|
|
605
|
-
}
|
|
606
|
-
if (!config.operatorWallet) {
|
|
607
|
-
throw new Error("RelayService: operatorWallet is required");
|
|
608
|
-
}
|
|
609
|
-
this.relayAddress = config.relayAddress;
|
|
610
|
-
this.operatorWallet = config.operatorWallet;
|
|
611
|
-
if (config.provider) this.provider = config.provider;
|
|
612
|
-
this.confirmationTimeoutMs = config.confirmationTimeoutMs ?? DEFAULT_CONFIRMATION_TIMEOUT_MS;
|
|
613
|
-
this.simulateBeforeSubmit = config.simulateBeforeSubmit ?? config.provider !== void 0;
|
|
614
|
-
}
|
|
615
|
-
/** Address the operator wallet is broadcasting from (for logging). */
|
|
616
|
-
operatorAddress() {
|
|
617
|
-
return this.operatorWallet.account?.address;
|
|
618
|
-
}
|
|
619
595
|
/**
|
|
620
|
-
* Build
|
|
621
|
-
*
|
|
622
|
-
* for audit before broadcasting.
|
|
596
|
+
* Build an unsigned UserOp for Scenario 1 (Mint) — sig-gated
|
|
597
|
+
* `PointToken.mint(to, amount, deadline, minterSig)`.
|
|
623
598
|
*/
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
return encodeMintAndSwap(params.mint, params.swap);
|
|
627
|
-
} catch (err) {
|
|
599
|
+
async prepareMint(params) {
|
|
600
|
+
if (!params.batchExecutorAddress) {
|
|
628
601
|
throw new RelayError(
|
|
629
602
|
"ENCODE_FAILED",
|
|
630
|
-
|
|
631
|
-
err
|
|
603
|
+
"prepareMint: batchExecutorAddress required"
|
|
632
604
|
);
|
|
633
605
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
* Submit a `mintAndSwap` transaction. Flow:
|
|
637
|
-
*
|
|
638
|
-
* 1. (optional) pre-flight simulate via provider
|
|
639
|
-
* 2. writeContract through the operator wallet
|
|
640
|
-
* 3. (optional) wait for the receipt and surface gasUsed / status
|
|
641
|
-
*
|
|
642
|
-
* Throws a typed `RelayError` on any failure so the MintingGateway can
|
|
643
|
-
* decide whether to release the ledger lock (`SUBMIT_FAILED` and
|
|
644
|
-
* `SIMULATION_FAILED` are safe to release; `TX_REVERTED` and `TIMEOUT`
|
|
645
|
-
* need manual review because the tx may still land).
|
|
646
|
-
*
|
|
647
|
-
* @deprecated Since 0.3.0 — will be replaced by `prepareMint()` +
|
|
648
|
-
* `prepareBurn()` in the v1.4 sponsored-UserOp flow. The SC team
|
|
649
|
-
* still needs to finalize Relayer v2 ABI before the replacements
|
|
650
|
-
* can ship (blocker B1). Kept for v0.2.x consumers. Removed in 2.0.
|
|
651
|
-
*/
|
|
652
|
-
async submitMintAndSwap(params) {
|
|
653
|
-
if (this.simulateBeforeSubmit && this.provider) {
|
|
654
|
-
const operatorAddr = this.operatorWallet.account?.address;
|
|
655
|
-
if (operatorAddr) {
|
|
656
|
-
try {
|
|
657
|
-
await coreSimulateMintAndSwap(
|
|
658
|
-
this.provider,
|
|
659
|
-
this.relayAddress,
|
|
660
|
-
params.mint,
|
|
661
|
-
params.swap,
|
|
662
|
-
operatorAddr
|
|
663
|
-
);
|
|
664
|
-
} catch (err) {
|
|
665
|
-
const reason = err instanceof SimulationError ? err.reason : errorMessage(err);
|
|
666
|
-
throw new RelayError(
|
|
667
|
-
"SIMULATION_FAILED",
|
|
668
|
-
`mintAndSwap would revert: ${reason}`,
|
|
669
|
-
err
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
606
|
+
if (!params.userAddress) {
|
|
607
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
673
608
|
}
|
|
674
|
-
|
|
675
|
-
try {
|
|
676
|
-
txHash = await this.operatorWallet.writeContract({
|
|
677
|
-
address: this.relayAddress,
|
|
678
|
-
abi: relayAbi,
|
|
679
|
-
functionName: "mintAndSwap",
|
|
680
|
-
args: [params.mint, params.swap],
|
|
681
|
-
...this.operatorWallet.account ? { account: this.operatorWallet.account } : {}
|
|
682
|
-
});
|
|
683
|
-
} catch (err) {
|
|
609
|
+
if (!params.pointTokenAddress) {
|
|
684
610
|
throw new RelayError(
|
|
685
|
-
"
|
|
686
|
-
|
|
687
|
-
err
|
|
611
|
+
"ENCODE_FAILED",
|
|
612
|
+
"prepareMint: pointTokenAddress required"
|
|
688
613
|
);
|
|
689
614
|
}
|
|
690
|
-
if (
|
|
691
|
-
|
|
615
|
+
if (params.amount <= 0n) {
|
|
616
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: amount must be positive");
|
|
692
617
|
}
|
|
693
|
-
|
|
694
|
-
const receipt = await this.provider.waitForTransactionReceipt({
|
|
695
|
-
hash: txHash,
|
|
696
|
-
timeout: this.confirmationTimeoutMs
|
|
697
|
-
});
|
|
698
|
-
if (receipt.status !== "success") {
|
|
699
|
-
throw new RelayError(
|
|
700
|
-
"TX_REVERTED",
|
|
701
|
-
`mintAndSwap reverted on-chain (tx=${txHash})`
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
return {
|
|
705
|
-
txHash,
|
|
706
|
-
blockNumber: receipt.blockNumber,
|
|
707
|
-
gasUsed: receipt.gasUsed,
|
|
708
|
-
status: receipt.status
|
|
709
|
-
};
|
|
710
|
-
} catch (err) {
|
|
711
|
-
if (err instanceof RelayError) throw err;
|
|
618
|
+
if (!params.issuerSignerWallet) {
|
|
712
619
|
throw new RelayError(
|
|
713
|
-
"
|
|
714
|
-
|
|
715
|
-
err
|
|
620
|
+
"ENCODE_FAILED",
|
|
621
|
+
"prepareMint: issuerSignerWallet required (for MintRequest EIP-712 signature)"
|
|
716
622
|
);
|
|
717
623
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
// v1.4 — Sponsored UserOp preparation (beta with mocked SC contracts)
|
|
721
|
-
// ==========================================================================
|
|
722
|
-
//
|
|
723
|
-
// These two methods build unsigned `PartialUserOperation` payloads for
|
|
724
|
-
// the Frontend to sign (via Privy) and submit to the Bundler. The
|
|
725
|
-
// Issuer Backend no longer broadcasts — that's the Frontend's job.
|
|
726
|
-
//
|
|
727
|
-
// Uses mocked Relayer v2 + PointToken ABIs from `@pafi-dev/core/contracts`.
|
|
728
|
-
// When SC delivers real ABIs, the imports swap but these method bodies
|
|
729
|
-
// stay the same (calldata encoder is ABI-driven).
|
|
730
|
-
// ==========================================================================
|
|
731
|
-
/**
|
|
732
|
-
* Build an unsigned UserOp for Scenario 1 (Mint).
|
|
733
|
-
*
|
|
734
|
-
* Flow:
|
|
735
|
-
* 1. Encode `Relayer.mint(request, userSig, issuerSig)` as the inner call
|
|
736
|
-
* 2. Optionally append a PT fee transfer from user → feeRecipient
|
|
737
|
-
* (fee recovery happens on-chain via BatchExecutor, not via an
|
|
738
|
-
* operator wallet)
|
|
739
|
-
* 3. Wrap all inner calls into `BatchExecutor.execute(calls[])`
|
|
740
|
-
* 4. Return a `PartialUserOperation` ready for:
|
|
741
|
-
* - gas estimation (Bundler)
|
|
742
|
-
* - paymaster sponsorship (PAFI Backend)
|
|
743
|
-
* - user signature (Privy)
|
|
744
|
-
*/
|
|
745
|
-
prepareMint(params) {
|
|
746
|
-
if (!params.relayerAddress) {
|
|
747
|
-
throw new RelayError("ENCODE_FAILED", "prepareMint: relayerAddress required");
|
|
624
|
+
if (params.deadline <= 0n) {
|
|
625
|
+
throw new RelayError("ENCODE_FAILED", "prepareMint: deadline must be positive");
|
|
748
626
|
}
|
|
749
|
-
|
|
627
|
+
let minterSig;
|
|
628
|
+
try {
|
|
629
|
+
const sig = await signMintRequest2(
|
|
630
|
+
params.issuerSignerWallet,
|
|
631
|
+
params.domain,
|
|
632
|
+
{
|
|
633
|
+
to: params.userAddress,
|
|
634
|
+
amount: params.amount,
|
|
635
|
+
nonce: params.mintRequestNonce,
|
|
636
|
+
deadline: params.deadline
|
|
637
|
+
}
|
|
638
|
+
);
|
|
639
|
+
minterSig = sig.serialized;
|
|
640
|
+
} catch (err) {
|
|
750
641
|
throw new RelayError(
|
|
751
642
|
"ENCODE_FAILED",
|
|
752
|
-
|
|
643
|
+
`prepareMint: failed to sign MintRequest: ${errorMessage(err)}`,
|
|
644
|
+
err
|
|
753
645
|
);
|
|
754
646
|
}
|
|
755
|
-
if (!params.userAddress) {
|
|
756
|
-
throw new RelayError("ENCODE_FAILED", "prepareMint: userAddress required");
|
|
757
|
-
}
|
|
758
647
|
let mintCallData;
|
|
759
648
|
try {
|
|
760
649
|
mintCallData = encodeFunctionData({
|
|
761
|
-
abi:
|
|
650
|
+
abi: POINT_TOKEN_V2_ABI,
|
|
762
651
|
functionName: "mint",
|
|
763
|
-
args: [params.
|
|
652
|
+
args: [params.userAddress, params.amount, params.deadline, minterSig]
|
|
764
653
|
});
|
|
765
654
|
} catch (err) {
|
|
766
655
|
throw new RelayError(
|
|
767
656
|
"ENCODE_FAILED",
|
|
768
|
-
`prepareMint: failed to encode
|
|
657
|
+
`prepareMint: failed to encode PointToken.mint: ${errorMessage(err)}`,
|
|
769
658
|
err
|
|
770
659
|
);
|
|
771
660
|
}
|
|
772
661
|
const operations = [
|
|
773
662
|
{
|
|
774
|
-
target: params.
|
|
663
|
+
target: params.pointTokenAddress,
|
|
775
664
|
value: 0n,
|
|
776
665
|
data: mintCallData
|
|
777
666
|
}
|
|
778
667
|
];
|
|
779
|
-
if (params.
|
|
668
|
+
if (params.feeAmount && params.feeAmount > 0n) {
|
|
669
|
+
if (!params.feeRecipient) {
|
|
670
|
+
throw new RelayError(
|
|
671
|
+
"ENCODE_FAILED",
|
|
672
|
+
"prepareMint: feeRecipient required when feeAmount > 0"
|
|
673
|
+
);
|
|
674
|
+
}
|
|
780
675
|
operations.push({
|
|
781
676
|
target: params.pointTokenAddress,
|
|
782
677
|
value: 0n,
|
|
783
678
|
data: encodeFunctionData({
|
|
784
|
-
abi:
|
|
785
|
-
functionName: "
|
|
786
|
-
|
|
787
|
-
args: [params.mintRequest.feeRecipient]
|
|
679
|
+
abi: erc20Abi,
|
|
680
|
+
functionName: "transfer",
|
|
681
|
+
args: [params.feeRecipient, params.feeAmount]
|
|
788
682
|
})
|
|
789
683
|
});
|
|
790
684
|
}
|
|
@@ -793,7 +687,7 @@ var RelayService = class {
|
|
|
793
687
|
nonce: params.aaNonce,
|
|
794
688
|
operations,
|
|
795
689
|
gasLimits: {
|
|
796
|
-
callGasLimit: params.callGasLimit ??
|
|
690
|
+
callGasLimit: params.callGasLimit ?? 300000n,
|
|
797
691
|
verificationGasLimit: params.verificationGasLimit ?? 150000n,
|
|
798
692
|
preVerificationGas: params.preVerificationGas ?? 50000n
|
|
799
693
|
}
|
|
@@ -803,13 +697,12 @@ var RelayService = class {
|
|
|
803
697
|
* Build an unsigned UserOp for Scenario 2 (Burn/Redeem).
|
|
804
698
|
*
|
|
805
699
|
* Two modes:
|
|
806
|
-
* - `mode: 'burn'` — direct `PointToken.burn(amount)`;
|
|
807
|
-
* via EIP-7702
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
* contract has to do it on-chain
|
|
700
|
+
* - `mode: 'burn'` — direct `PointToken.burn(from, amount)`; only
|
|
701
|
+
* usable if the caller (via EIP-7702) is whitelisted as a burner.
|
|
702
|
+
* Rare in v1.4; kept for admin/operator tools.
|
|
703
|
+
* - `mode: 'burnWithSig'` — `PointToken.burn(from, amount, deadline,
|
|
704
|
+
* burnerSig)`. Caller provides a pre-signed `BurnRequest` + sig
|
|
705
|
+
* bytes (typically from `PTRedeemHandler`).
|
|
813
706
|
*/
|
|
814
707
|
prepareBurn(params) {
|
|
815
708
|
if (!params.pointTokenAddress) {
|
|
@@ -824,19 +717,24 @@ var RelayService = class {
|
|
|
824
717
|
let burnCallData;
|
|
825
718
|
try {
|
|
826
719
|
if (params.mode === "burnWithSig") {
|
|
827
|
-
if (!params.
|
|
828
|
-
throw new Error("burnWithSig requires
|
|
720
|
+
if (!params.burnRequest || !params.burnerSignature) {
|
|
721
|
+
throw new Error("burnWithSig requires burnRequest + burnerSignature");
|
|
829
722
|
}
|
|
830
723
|
burnCallData = encodeFunctionData({
|
|
831
724
|
abi: POINT_TOKEN_V2_ABI,
|
|
832
|
-
functionName: "
|
|
833
|
-
args: [
|
|
725
|
+
functionName: "burn",
|
|
726
|
+
args: [
|
|
727
|
+
params.burnRequest.from,
|
|
728
|
+
params.burnRequest.amount,
|
|
729
|
+
params.burnRequest.deadline,
|
|
730
|
+
params.burnerSignature
|
|
731
|
+
]
|
|
834
732
|
});
|
|
835
733
|
} else {
|
|
836
734
|
burnCallData = encodeFunctionData({
|
|
837
735
|
abi: POINT_TOKEN_V2_ABI,
|
|
838
736
|
functionName: "burn",
|
|
839
|
-
args: [params.amount]
|
|
737
|
+
args: [params.userAddress, params.amount]
|
|
840
738
|
});
|
|
841
739
|
}
|
|
842
740
|
} catch (err) {
|
|
@@ -906,255 +804,6 @@ var FeeManager = class {
|
|
|
906
804
|
}
|
|
907
805
|
};
|
|
908
806
|
|
|
909
|
-
// src/gateway/types.ts
|
|
910
|
-
var MintingGatewayError = class extends Error {
|
|
911
|
-
code;
|
|
912
|
-
/**
|
|
913
|
-
* True if the ledger lock was released before this error was thrown,
|
|
914
|
-
* meaning the user can safely retry. False means the funds are still
|
|
915
|
-
* locked (e.g. tx may still land on-chain) and retry would double-spend.
|
|
916
|
-
*/
|
|
917
|
-
safeToRetry;
|
|
918
|
-
cause;
|
|
919
|
-
constructor(code, message, opts) {
|
|
920
|
-
super(message);
|
|
921
|
-
this.name = "MintingGatewayError";
|
|
922
|
-
this.code = code;
|
|
923
|
-
this.safeToRetry = opts.safeToRetry;
|
|
924
|
-
if (opts.cause !== void 0) this.cause = opts.cause;
|
|
925
|
-
}
|
|
926
|
-
};
|
|
927
|
-
|
|
928
|
-
// src/gateway/mintingGateway.ts
|
|
929
|
-
import {
|
|
930
|
-
verifyReceiverConsent,
|
|
931
|
-
encodeExtData
|
|
932
|
-
} from "@pafi-dev/core";
|
|
933
|
-
var DEFAULT_LOCK_BUFFER_MS = 6e4;
|
|
934
|
-
var MintingGateway = class {
|
|
935
|
-
ledger;
|
|
936
|
-
policy;
|
|
937
|
-
signer;
|
|
938
|
-
relayService;
|
|
939
|
-
now;
|
|
940
|
-
defaultLockBufferMs;
|
|
941
|
-
constructor(config) {
|
|
942
|
-
if (!config.ledger) throw new Error("MintingGateway: ledger required");
|
|
943
|
-
if (!config.policy) throw new Error("MintingGateway: policy required");
|
|
944
|
-
if (!config.signer) throw new Error("MintingGateway: signer required");
|
|
945
|
-
if (!config.relayService)
|
|
946
|
-
throw new Error("MintingGateway: relayService required");
|
|
947
|
-
this.ledger = config.ledger;
|
|
948
|
-
this.policy = config.policy;
|
|
949
|
-
this.signer = config.signer;
|
|
950
|
-
this.relayService = config.relayService;
|
|
951
|
-
this.now = config.now ?? (() => Date.now());
|
|
952
|
-
this.defaultLockBufferMs = config.defaultLockBufferMs ?? DEFAULT_LOCK_BUFFER_MS;
|
|
953
|
-
}
|
|
954
|
-
/**
|
|
955
|
-
* @deprecated Since 0.3.0 — will be renamed to `processMint()` once
|
|
956
|
-
* the SC team finalizes Relayer v2 ABI. The new flow drops the
|
|
957
|
-
* swap steps entirely (no more single-call mint+swap); users swap
|
|
958
|
-
* separately on PAFI Web. Kept here for v0.2.x consumers. Removed in 2.0.
|
|
959
|
-
*/
|
|
960
|
-
async processMintAndCashOut(request) {
|
|
961
|
-
const { receiverConsent, receiverSignature } = request;
|
|
962
|
-
if (!receiverConsent || !receiverSignature) {
|
|
963
|
-
throw new MintingGatewayError(
|
|
964
|
-
"INVALID_REQUEST",
|
|
965
|
-
"receiverConsent and receiverSignature are required",
|
|
966
|
-
{ safeToRetry: true }
|
|
967
|
-
);
|
|
968
|
-
}
|
|
969
|
-
if (receiverConsent.amount <= 0n) {
|
|
970
|
-
throw new MintingGatewayError(
|
|
971
|
-
"INVALID_REQUEST",
|
|
972
|
-
"consent amount must be positive",
|
|
973
|
-
{ safeToRetry: true }
|
|
974
|
-
);
|
|
975
|
-
}
|
|
976
|
-
if (receiverConsent.originalReceiver !== request.userAddress) {
|
|
977
|
-
throw new MintingGatewayError(
|
|
978
|
-
"INVALID_REQUEST",
|
|
979
|
-
"consent.originalReceiver must equal request.userAddress",
|
|
980
|
-
{ safeToRetry: true }
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
const nowSec = BigInt(Math.floor(this.now() / 1e3));
|
|
984
|
-
if (receiverConsent.deadline <= nowSec) {
|
|
985
|
-
throw new MintingGatewayError(
|
|
986
|
-
"CONSENT_EXPIRED",
|
|
987
|
-
"ReceiverConsent deadline has already passed",
|
|
988
|
-
{ safeToRetry: true }
|
|
989
|
-
);
|
|
990
|
-
}
|
|
991
|
-
const consentResult = await verifyReceiverConsent(
|
|
992
|
-
request.domain,
|
|
993
|
-
receiverConsent,
|
|
994
|
-
receiverSignature,
|
|
995
|
-
request.userAddress
|
|
996
|
-
);
|
|
997
|
-
if (!consentResult.isValid) {
|
|
998
|
-
throw new MintingGatewayError(
|
|
999
|
-
"INVALID_CONSENT_SIGNATURE",
|
|
1000
|
-
`ReceiverConsent signature did not recover to ${request.userAddress}`,
|
|
1001
|
-
{ safeToRetry: true }
|
|
1002
|
-
);
|
|
1003
|
-
}
|
|
1004
|
-
const policyDecision = await this.policy.evaluate({
|
|
1005
|
-
userAddress: request.userAddress,
|
|
1006
|
-
amount: receiverConsent.amount,
|
|
1007
|
-
pointTokenAddress: request.pointTokenAddress,
|
|
1008
|
-
chainId: request.chainId
|
|
1009
|
-
});
|
|
1010
|
-
if (!policyDecision.approved) {
|
|
1011
|
-
const code = policyDecision.reason?.toLowerCase().includes("insufficient") ? "INSUFFICIENT_BALANCE" : "POLICY_REJECTED";
|
|
1012
|
-
throw new MintingGatewayError(
|
|
1013
|
-
code,
|
|
1014
|
-
policyDecision.reason ?? "Minting request rejected by policy engine",
|
|
1015
|
-
{ safeToRetry: true }
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
const lockDurationMs = request.lockDurationMs ?? this.computeLockDurationMs(receiverConsent.deadline);
|
|
1019
|
-
let lockId;
|
|
1020
|
-
try {
|
|
1021
|
-
lockId = await this.ledger.lockForMinting(
|
|
1022
|
-
request.userAddress,
|
|
1023
|
-
receiverConsent.amount,
|
|
1024
|
-
lockDurationMs,
|
|
1025
|
-
request.pointTokenAddress
|
|
1026
|
-
);
|
|
1027
|
-
} catch (err) {
|
|
1028
|
-
throw new MintingGatewayError(
|
|
1029
|
-
"INSUFFICIENT_BALANCE",
|
|
1030
|
-
`Failed to lock ledger balance: ${errorMessage2(err)}`,
|
|
1031
|
-
{ safeToRetry: true, cause: err }
|
|
1032
|
-
);
|
|
1033
|
-
}
|
|
1034
|
-
try {
|
|
1035
|
-
let minterSignature;
|
|
1036
|
-
try {
|
|
1037
|
-
minterSignature = await this.signer.signMintRequest(request.domain, {
|
|
1038
|
-
to: request.userAddress,
|
|
1039
|
-
amount: receiverConsent.amount,
|
|
1040
|
-
nonce: receiverConsent.nonce,
|
|
1041
|
-
deadline: receiverConsent.deadline
|
|
1042
|
-
});
|
|
1043
|
-
} catch (err) {
|
|
1044
|
-
await this.releaseLockSafely(lockId);
|
|
1045
|
-
throw new MintingGatewayError(
|
|
1046
|
-
"SIGNER_FAILED",
|
|
1047
|
-
`Issuer signer failed: ${errorMessage2(err)}`,
|
|
1048
|
-
{ safeToRetry: true, cause: err }
|
|
1049
|
-
);
|
|
1050
|
-
}
|
|
1051
|
-
const mintParams = {
|
|
1052
|
-
pointToken: request.pointTokenAddress,
|
|
1053
|
-
receiver: request.userAddress,
|
|
1054
|
-
amount: receiverConsent.amount,
|
|
1055
|
-
deadline: receiverConsent.deadline,
|
|
1056
|
-
minterSig: minterSignature.serialized,
|
|
1057
|
-
receiverSig: receiverSignature,
|
|
1058
|
-
extData: receiverConsent.extData
|
|
1059
|
-
};
|
|
1060
|
-
const swapParams = {
|
|
1061
|
-
path: request.swapPath,
|
|
1062
|
-
deadline: request.swapDeadline
|
|
1063
|
-
};
|
|
1064
|
-
let relayResult;
|
|
1065
|
-
try {
|
|
1066
|
-
relayResult = await this.relayService.submitMintAndSwap({
|
|
1067
|
-
mint: mintParams,
|
|
1068
|
-
swap: swapParams
|
|
1069
|
-
});
|
|
1070
|
-
} catch (err) {
|
|
1071
|
-
await this.handleRelayFailure(err, lockId);
|
|
1072
|
-
}
|
|
1073
|
-
const result = {
|
|
1074
|
-
txHash: relayResult.txHash,
|
|
1075
|
-
lockId
|
|
1076
|
-
};
|
|
1077
|
-
if (relayResult.blockNumber !== void 0) {
|
|
1078
|
-
result.blockNumber = relayResult.blockNumber;
|
|
1079
|
-
}
|
|
1080
|
-
if (relayResult.gasUsed !== void 0) {
|
|
1081
|
-
result.gasUsed = relayResult.gasUsed;
|
|
1082
|
-
}
|
|
1083
|
-
return result;
|
|
1084
|
-
} catch (err) {
|
|
1085
|
-
if (err instanceof MintingGatewayError) throw err;
|
|
1086
|
-
await this.releaseLockSafely(lockId);
|
|
1087
|
-
throw new MintingGatewayError(
|
|
1088
|
-
"RELAY_SUBMIT_FAILED",
|
|
1089
|
-
`Unexpected error: ${errorMessage2(err)}`,
|
|
1090
|
-
{ safeToRetry: true, cause: err }
|
|
1091
|
-
);
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
// ---------------------------------------------------------------------------
|
|
1095
|
-
// Internals
|
|
1096
|
-
// ---------------------------------------------------------------------------
|
|
1097
|
-
computeLockDurationMs(consentDeadlineSec) {
|
|
1098
|
-
const nowMs = this.now();
|
|
1099
|
-
const deadlineMs = Number(consentDeadlineSec) * 1e3;
|
|
1100
|
-
const remaining = Math.max(0, deadlineMs - nowMs);
|
|
1101
|
-
return remaining + this.defaultLockBufferMs;
|
|
1102
|
-
}
|
|
1103
|
-
/**
|
|
1104
|
-
* Map a RelayError to a MintingGatewayError, releasing the lock only
|
|
1105
|
-
* when the tx definitely did not land. `TX_REVERTED` and `TIMEOUT`
|
|
1106
|
-
* leave the lock in place because the tx may still be in the mempool
|
|
1107
|
-
* or already mined — releasing would enable a double-spend on retry.
|
|
1108
|
-
* Always throws.
|
|
1109
|
-
*/
|
|
1110
|
-
async handleRelayFailure(err, lockId) {
|
|
1111
|
-
if (err instanceof RelayError) {
|
|
1112
|
-
switch (err.code) {
|
|
1113
|
-
case "ENCODE_FAILED":
|
|
1114
|
-
case "SIMULATION_FAILED":
|
|
1115
|
-
case "SUBMIT_FAILED":
|
|
1116
|
-
case "NOT_CONFIGURED":
|
|
1117
|
-
await this.releaseLockSafely(lockId);
|
|
1118
|
-
throw new MintingGatewayError(
|
|
1119
|
-
err.code === "SIMULATION_FAILED" ? "RELAY_SIMULATION_FAILED" : "RELAY_SUBMIT_FAILED",
|
|
1120
|
-
err.message,
|
|
1121
|
-
{ safeToRetry: true, cause: err }
|
|
1122
|
-
);
|
|
1123
|
-
case "TX_REVERTED":
|
|
1124
|
-
throw new MintingGatewayError("RELAY_REVERTED", err.message, {
|
|
1125
|
-
safeToRetry: false,
|
|
1126
|
-
cause: err
|
|
1127
|
-
});
|
|
1128
|
-
case "TIMEOUT":
|
|
1129
|
-
throw new MintingGatewayError("RELAY_TIMEOUT", err.message, {
|
|
1130
|
-
safeToRetry: false,
|
|
1131
|
-
cause: err
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
await this.releaseLockSafely(lockId);
|
|
1136
|
-
throw new MintingGatewayError(
|
|
1137
|
-
"RELAY_SUBMIT_FAILED",
|
|
1138
|
-
`Unexpected relay error: ${errorMessage2(err)}`,
|
|
1139
|
-
{ safeToRetry: true, cause: err }
|
|
1140
|
-
);
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Release a lock, swallowing any secondary error. We never want a lock
|
|
1144
|
-
* release failure to mask the original error — the lock will auto-expire
|
|
1145
|
-
* via TTL anyway.
|
|
1146
|
-
*/
|
|
1147
|
-
async releaseLockSafely(lockId) {
|
|
1148
|
-
try {
|
|
1149
|
-
await this.ledger.releaseLock(lockId);
|
|
1150
|
-
} catch {
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
};
|
|
1154
|
-
function errorMessage2(err) {
|
|
1155
|
-
return err instanceof Error ? err.message : String(err);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
807
|
// src/indexer/types.ts
|
|
1159
808
|
var InMemoryCursorStore = class {
|
|
1160
809
|
cursor;
|
|
@@ -1509,7 +1158,6 @@ import {
|
|
|
1509
1158
|
} from "@pafi-dev/core";
|
|
1510
1159
|
var IssuerApiHandlers = class {
|
|
1511
1160
|
authService;
|
|
1512
|
-
gateway;
|
|
1513
1161
|
ledger;
|
|
1514
1162
|
provider;
|
|
1515
1163
|
/**
|
|
@@ -1528,7 +1176,6 @@ var IssuerApiHandlers = class {
|
|
|
1528
1176
|
poolsProvider;
|
|
1529
1177
|
constructor(config) {
|
|
1530
1178
|
this.authService = config.authService;
|
|
1531
|
-
this.gateway = config.gateway;
|
|
1532
1179
|
this.ledger = config.ledger;
|
|
1533
1180
|
this.provider = config.provider;
|
|
1534
1181
|
const raw = config.pointTokenAddresses && config.pointTokenAddresses.length > 0 ? config.pointTokenAddresses : config.pointTokenAddress ? [config.pointTokenAddress] : [];
|
|
@@ -1710,55 +1357,13 @@ var IssuerApiHandlers = class {
|
|
|
1710
1357
|
}
|
|
1711
1358
|
};
|
|
1712
1359
|
}
|
|
1713
|
-
/**
|
|
1714
|
-
* `POST /claim-and-swap`
|
|
1715
|
-
*
|
|
1716
|
-
* @deprecated Since 0.3.0 — the single-call mint-then-swap flow is
|
|
1717
|
-
* retired in v1.4. Use the new `handleClaim()` (mint only) and let
|
|
1718
|
-
* the user swap separately on PAFI Web. See
|
|
1719
|
-
* [V1.4_V1.5_OVERVIEW.md §4] for the new scenario model. Will be
|
|
1720
|
-
* removed in 2.0.
|
|
1721
|
-
*
|
|
1722
|
-
* Legacy behavior: the terminal handler forwards the verified
|
|
1723
|
-
* consent to the MintingGateway, which runs the 11-step flow.
|
|
1724
|
-
*/
|
|
1725
|
-
async handleClaimAndSwap(userAddress, request) {
|
|
1726
|
-
if (request.chainId !== this.chainId) {
|
|
1727
|
-
throw new Error(
|
|
1728
|
-
`handleClaimAndSwap: unsupported chainId ${request.chainId}`
|
|
1729
|
-
);
|
|
1730
|
-
}
|
|
1731
|
-
const pointToken = getAddress6(request.pointTokenAddress);
|
|
1732
|
-
if (!this.supportedTokens.has(pointToken)) {
|
|
1733
|
-
throw new Error(
|
|
1734
|
-
`handleClaimAndSwap: unsupported pointToken ${pointToken}`
|
|
1735
|
-
);
|
|
1736
|
-
}
|
|
1737
|
-
const result = await this.gateway.processMintAndCashOut({
|
|
1738
|
-
userAddress: getAddress6(userAddress),
|
|
1739
|
-
pointTokenAddress: pointToken,
|
|
1740
|
-
chainId: request.chainId,
|
|
1741
|
-
domain: request.domain,
|
|
1742
|
-
receiverConsent: request.receiverConsent,
|
|
1743
|
-
receiverSignature: request.receiverSignature,
|
|
1744
|
-
swapPath: request.swapPath,
|
|
1745
|
-
swapDeadline: request.swapDeadline
|
|
1746
|
-
});
|
|
1747
|
-
const response = {
|
|
1748
|
-
txHash: result.txHash,
|
|
1749
|
-
lockId: result.lockId
|
|
1750
|
-
};
|
|
1751
|
-
if (result.blockNumber !== void 0)
|
|
1752
|
-
response.blockNumber = result.blockNumber;
|
|
1753
|
-
if (result.gasUsed !== void 0) response.gasUsed = result.gasUsed;
|
|
1754
|
-
return response;
|
|
1755
|
-
}
|
|
1756
1360
|
};
|
|
1757
1361
|
|
|
1758
1362
|
// src/api/handlers/ptRedeemHandler.ts
|
|
1759
1363
|
import { getAddress as getAddress7 } from "viem";
|
|
1760
|
-
import {
|
|
1364
|
+
import { signBurnRequest, POINT_TOKEN_V2_ABI as POINT_TOKEN_V2_ABI2 } from "@pafi-dev/core";
|
|
1761
1365
|
var DEFAULT_REDEEM_LOCK_MS = 15 * 60 * 1e3;
|
|
1366
|
+
var DEFAULT_SIG_DEADLINE_SEC = 15 * 60;
|
|
1762
1367
|
var PTRedeemError = class extends Error {
|
|
1763
1368
|
constructor(code, message) {
|
|
1764
1369
|
super(message);
|
|
@@ -1770,11 +1375,14 @@ var PTRedeemError = class extends Error {
|
|
|
1770
1375
|
var PTRedeemHandler = class {
|
|
1771
1376
|
ledger;
|
|
1772
1377
|
relayService;
|
|
1378
|
+
provider;
|
|
1773
1379
|
pointTokenAddress;
|
|
1774
1380
|
batchExecutorAddress;
|
|
1775
1381
|
chainId;
|
|
1776
1382
|
domain;
|
|
1383
|
+
burnerSignerWallet;
|
|
1777
1384
|
redeemLockDurationMs;
|
|
1385
|
+
signatureDeadlineSeconds;
|
|
1778
1386
|
now;
|
|
1779
1387
|
constructor(config) {
|
|
1780
1388
|
if (!config.ledger.reservePendingCredit) {
|
|
@@ -1783,46 +1391,68 @@ var PTRedeemHandler = class {
|
|
|
1783
1391
|
"PTRedeemHandler requires a ledger that implements reservePendingCredit() (v0.3.0+)"
|
|
1784
1392
|
);
|
|
1785
1393
|
}
|
|
1394
|
+
if (!config.burnerSignerWallet) {
|
|
1395
|
+
throw new PTRedeemError(
|
|
1396
|
+
"SIGNING_FAILED",
|
|
1397
|
+
"PTRedeemHandler requires burnerSignerWallet (issuer burner signer)"
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1786
1400
|
this.ledger = config.ledger;
|
|
1787
1401
|
this.relayService = config.relayService;
|
|
1402
|
+
this.provider = config.provider;
|
|
1788
1403
|
this.pointTokenAddress = getAddress7(config.pointTokenAddress);
|
|
1789
1404
|
this.batchExecutorAddress = getAddress7(config.batchExecutorAddress);
|
|
1790
1405
|
this.chainId = config.chainId;
|
|
1791
1406
|
this.domain = config.domain;
|
|
1407
|
+
this.burnerSignerWallet = config.burnerSignerWallet;
|
|
1792
1408
|
this.redeemLockDurationMs = config.redeemLockDurationMs ?? DEFAULT_REDEEM_LOCK_MS;
|
|
1409
|
+
this.signatureDeadlineSeconds = config.signatureDeadlineSeconds ?? DEFAULT_SIG_DEADLINE_SEC;
|
|
1793
1410
|
this.now = config.now ?? (() => Date.now());
|
|
1794
1411
|
}
|
|
1795
1412
|
async handle(request) {
|
|
1796
1413
|
if (request.amount <= 0n) {
|
|
1797
|
-
throw new PTRedeemError("
|
|
1798
|
-
}
|
|
1799
|
-
if (request.consent.amount !== request.amount) {
|
|
1800
|
-
throw new PTRedeemError(
|
|
1801
|
-
"AMOUNT_MISMATCH",
|
|
1802
|
-
`consent.amount (${request.consent.amount}) must match request.amount (${request.amount})`
|
|
1803
|
-
);
|
|
1414
|
+
throw new PTRedeemError("INVALID_AMOUNT", "redeem amount must be positive");
|
|
1804
1415
|
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1416
|
+
let burnNonce;
|
|
1417
|
+
try {
|
|
1418
|
+
burnNonce = await this.provider.readContract({
|
|
1419
|
+
address: this.pointTokenAddress,
|
|
1420
|
+
abi: POINT_TOKEN_V2_ABI2,
|
|
1421
|
+
functionName: "burnRequestNonces",
|
|
1422
|
+
args: [request.userAddress]
|
|
1423
|
+
});
|
|
1424
|
+
} catch (err) {
|
|
1807
1425
|
throw new PTRedeemError(
|
|
1808
|
-
"
|
|
1809
|
-
`
|
|
1426
|
+
"NONCE_READ_FAILED",
|
|
1427
|
+
`failed to read burnRequestNonces(${request.userAddress}): ${err instanceof Error ? err.message : String(err)}`
|
|
1810
1428
|
);
|
|
1811
1429
|
}
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
name: this.domain.name,
|
|
1815
|
-
chainId: this.chainId,
|
|
1816
|
-
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1817
|
-
},
|
|
1818
|
-
request.consent,
|
|
1819
|
-
request.consentSignature,
|
|
1820
|
-
request.userAddress
|
|
1430
|
+
const deadline = BigInt(
|
|
1431
|
+
Math.floor(this.now() / 1e3) + this.signatureDeadlineSeconds
|
|
1821
1432
|
);
|
|
1822
|
-
|
|
1433
|
+
const domain = {
|
|
1434
|
+
name: this.domain.name,
|
|
1435
|
+
chainId: this.chainId,
|
|
1436
|
+
verifyingContract: this.domain.verifyingContract ?? this.pointTokenAddress
|
|
1437
|
+
};
|
|
1438
|
+
const burnRequest = {
|
|
1439
|
+
from: request.userAddress,
|
|
1440
|
+
amount: request.amount,
|
|
1441
|
+
nonce: burnNonce,
|
|
1442
|
+
deadline
|
|
1443
|
+
};
|
|
1444
|
+
let burnerSignature;
|
|
1445
|
+
try {
|
|
1446
|
+
const sig = await signBurnRequest(
|
|
1447
|
+
this.burnerSignerWallet,
|
|
1448
|
+
domain,
|
|
1449
|
+
burnRequest
|
|
1450
|
+
);
|
|
1451
|
+
burnerSignature = sig.serialized;
|
|
1452
|
+
} catch (err) {
|
|
1823
1453
|
throw new PTRedeemError(
|
|
1824
|
-
"
|
|
1825
|
-
`
|
|
1454
|
+
"SIGNING_FAILED",
|
|
1455
|
+
`failed to sign BurnRequest: ${err instanceof Error ? err.message : String(err)}`
|
|
1826
1456
|
);
|
|
1827
1457
|
}
|
|
1828
1458
|
const lockId = await this.ledger.reservePendingCredit(
|
|
@@ -1837,29 +1467,17 @@ var PTRedeemHandler = class {
|
|
|
1837
1467
|
aaNonce: request.aaNonce,
|
|
1838
1468
|
pointTokenAddress: this.pointTokenAddress,
|
|
1839
1469
|
batchExecutorAddress: this.batchExecutorAddress,
|
|
1840
|
-
|
|
1841
|
-
|
|
1470
|
+
burnRequest,
|
|
1471
|
+
burnerSignature
|
|
1842
1472
|
});
|
|
1843
1473
|
return {
|
|
1844
1474
|
lockId,
|
|
1845
1475
|
userOp,
|
|
1846
|
-
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3)
|
|
1476
|
+
expiresInSeconds: Math.floor(this.redeemLockDurationMs / 1e3),
|
|
1477
|
+
signatureDeadline: deadline
|
|
1847
1478
|
};
|
|
1848
1479
|
}
|
|
1849
1480
|
};
|
|
1850
|
-
function parseSigStruct(serialized) {
|
|
1851
|
-
const raw = serialized.slice(2);
|
|
1852
|
-
if (raw.length !== 130) {
|
|
1853
|
-
throw new PTRedeemError(
|
|
1854
|
-
"INVALID_CONSENT",
|
|
1855
|
-
`signature must be 65 bytes, got ${raw.length / 2}`
|
|
1856
|
-
);
|
|
1857
|
-
}
|
|
1858
|
-
const r = `0x${raw.slice(0, 64)}`;
|
|
1859
|
-
const s = `0x${raw.slice(64, 128)}`;
|
|
1860
|
-
const v = parseInt(raw.slice(128, 130), 16);
|
|
1861
|
-
return { v, r, s };
|
|
1862
|
-
}
|
|
1863
1481
|
|
|
1864
1482
|
// src/api/handlers/topUpRedemptionHandler.ts
|
|
1865
1483
|
import { getAddress as getAddress8 } from "viem";
|
|
@@ -1905,24 +1523,10 @@ var TopUpRedemptionHandler = class {
|
|
|
1905
1523
|
shortfall
|
|
1906
1524
|
};
|
|
1907
1525
|
}
|
|
1908
|
-
if (request.redeemRequest.consent.amount < shortfall) {
|
|
1909
|
-
throw new TopUpRedemptionError(
|
|
1910
|
-
"CONSENT_AMOUNT_TOO_LOW",
|
|
1911
|
-
`consent.amount (${request.redeemRequest.consent.amount}) must cover shortfall (${shortfall})`
|
|
1912
|
-
);
|
|
1913
|
-
}
|
|
1914
|
-
if (request.redeemRequest.consent.amount !== shortfall) {
|
|
1915
|
-
throw new TopUpRedemptionError(
|
|
1916
|
-
"CONSENT_AMOUNT_TOO_LOW",
|
|
1917
|
-
`consent.amount (${request.redeemRequest.consent.amount}) must equal shortfall (${shortfall}) exactly \u2014 re-sign with correct amount`
|
|
1918
|
-
);
|
|
1919
|
-
}
|
|
1920
1526
|
const redeem = await this.ptRedeemHandler.handle({
|
|
1921
1527
|
userAddress: request.userAddress,
|
|
1922
1528
|
amount: shortfall,
|
|
1923
|
-
|
|
1924
|
-
consentSignature: request.redeemRequest.consentSignature,
|
|
1925
|
-
aaNonce: request.redeemRequest.aaNonce
|
|
1529
|
+
aaNonce: request.aaNonce
|
|
1926
1530
|
});
|
|
1927
1531
|
return {
|
|
1928
1532
|
action: "TOP_UP_STARTED",
|
|
@@ -2203,28 +1807,11 @@ var PafiBackendError = class extends Error {
|
|
|
2203
1807
|
code;
|
|
2204
1808
|
httpStatus;
|
|
2205
1809
|
details;
|
|
2206
|
-
/**
|
|
2207
|
-
* Seconds to wait before retry. Populated from the server body
|
|
2208
|
-
* (e.g. rate limit returns the number of seconds until UTC midnight).
|
|
2209
|
-
*/
|
|
2210
1810
|
retryAfter;
|
|
2211
|
-
/**
|
|
2212
|
-
* `safeToRetry` as reported by the server body. Prefer this over the
|
|
2213
|
-
* code-based heuristic when available — the server knows more about
|
|
2214
|
-
* whether the same request will succeed on retry.
|
|
2215
|
-
*/
|
|
2216
1811
|
serverSafeToRetry;
|
|
2217
|
-
/**
|
|
2218
|
-
* Whether the caller can safely retry the same request.
|
|
2219
|
-
*
|
|
2220
|
-
* If the server provided `safeToRetry` in the body, trust that.
|
|
2221
|
-
* Otherwise fall back to a code-based heuristic.
|
|
2222
|
-
*/
|
|
2223
1812
|
get safeToRetry() {
|
|
2224
1813
|
if (this.serverSafeToRetry !== void 0) return this.serverSafeToRetry;
|
|
2225
1814
|
switch (this.code) {
|
|
2226
|
-
case "PAYMASTER_UNAVAILABLE":
|
|
2227
|
-
case "PAYMASTER_TIMEOUT":
|
|
2228
1815
|
case "RATE_LIMITER_UNAVAILABLE":
|
|
2229
1816
|
case "INTERNAL_ERROR":
|
|
2230
1817
|
case "TIMEOUT":
|
|
@@ -2233,197 +1820,20 @@ var PafiBackendError = class extends Error {
|
|
|
2233
1820
|
case "RATE_LIMIT_EXCEEDED":
|
|
2234
1821
|
case "RATE_LIMIT_EXCEEDED_DAILY":
|
|
2235
1822
|
case "RATE_LIMIT_EXCEEDED_PER_USER":
|
|
1823
|
+
case "ISSUER_BUDGET_EXCEEDED":
|
|
2236
1824
|
return true;
|
|
2237
|
-
// after retryAfter
|
|
2238
1825
|
default:
|
|
2239
1826
|
return false;
|
|
2240
1827
|
}
|
|
2241
1828
|
}
|
|
2242
1829
|
};
|
|
2243
1830
|
|
|
2244
|
-
// src/pafi-backend/pafiBackendClient.ts
|
|
2245
|
-
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
2246
|
-
var RETRY_DEFAULTS = {
|
|
2247
|
-
maxAttempts: 1,
|
|
2248
|
-
initialDelayMs: 500,
|
|
2249
|
-
maxDelayMs: 1e4,
|
|
2250
|
-
maxRetryAfterMs: 3e4
|
|
2251
|
-
};
|
|
2252
|
-
var PafiBackendClient = class {
|
|
2253
|
-
url;
|
|
2254
|
-
issuerId;
|
|
2255
|
-
apiKey;
|
|
2256
|
-
fetchImpl;
|
|
2257
|
-
timeoutMs;
|
|
2258
|
-
retry;
|
|
2259
|
-
constructor(config) {
|
|
2260
|
-
if (!config.url) {
|
|
2261
|
-
throw new Error("PafiBackendClient: url is required");
|
|
2262
|
-
}
|
|
2263
|
-
if (!config.issuerId) {
|
|
2264
|
-
throw new Error("PafiBackendClient: issuerId is required");
|
|
2265
|
-
}
|
|
2266
|
-
if (!config.apiKey) {
|
|
2267
|
-
throw new Error("PafiBackendClient: apiKey is required");
|
|
2268
|
-
}
|
|
2269
|
-
this.url = config.url.replace(/\/+$/, "");
|
|
2270
|
-
this.issuerId = config.issuerId;
|
|
2271
|
-
this.apiKey = config.apiKey;
|
|
2272
|
-
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
2273
|
-
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2274
|
-
this.retry = { ...RETRY_DEFAULTS, ...config.retry ?? {} };
|
|
2275
|
-
if (!this.fetchImpl) {
|
|
2276
|
-
throw new Error(
|
|
2277
|
-
"PafiBackendClient: no fetch implementation available \u2014 pass `fetchImpl` or run on Node 18+"
|
|
2278
|
-
);
|
|
2279
|
-
}
|
|
2280
|
-
if (this.retry.maxAttempts < 1) {
|
|
2281
|
-
throw new Error("PafiBackendClient: retry.maxAttempts must be >= 1");
|
|
2282
|
-
}
|
|
2283
|
-
}
|
|
2284
|
-
/**
|
|
2285
|
-
* Request paymaster sponsorship for a pre-built UserOperation.
|
|
2286
|
-
* See [SPONSORED_PATH_FLOW.md §4.1] for the API contract.
|
|
2287
|
-
*
|
|
2288
|
-
* Retries automatically on transient failures (5xx, timeouts, network
|
|
2289
|
-
* errors, and errors the server flags with `safeToRetry: true`) up to
|
|
2290
|
-
* `retry.maxAttempts`. 4xx errors that are not `safeToRetry` fail fast.
|
|
2291
|
-
*
|
|
2292
|
-
* @throws PafiBackendError on final failure after exhausting retries
|
|
2293
|
-
*/
|
|
2294
|
-
async requestSponsorship(req) {
|
|
2295
|
-
return this.postWithRetry(
|
|
2296
|
-
"/paymaster/sponsor",
|
|
2297
|
-
req
|
|
2298
|
-
);
|
|
2299
|
-
}
|
|
2300
|
-
// -------------------------------------------------------------------------
|
|
2301
|
-
// Internals
|
|
2302
|
-
// -------------------------------------------------------------------------
|
|
2303
|
-
async postWithRetry(path, body) {
|
|
2304
|
-
let lastError;
|
|
2305
|
-
for (let attempt = 1; attempt <= this.retry.maxAttempts; attempt++) {
|
|
2306
|
-
try {
|
|
2307
|
-
return await this.post(path, body);
|
|
2308
|
-
} catch (err) {
|
|
2309
|
-
if (!(err instanceof PafiBackendError)) throw err;
|
|
2310
|
-
lastError = err;
|
|
2311
|
-
const isLastAttempt = attempt >= this.retry.maxAttempts;
|
|
2312
|
-
if (isLastAttempt || !err.safeToRetry) throw err;
|
|
2313
|
-
const delay = this.computeBackoff(attempt, err.retryAfter);
|
|
2314
|
-
if (delay === null) throw err;
|
|
2315
|
-
await this.sleep(delay);
|
|
2316
|
-
}
|
|
2317
|
-
}
|
|
2318
|
-
throw lastError;
|
|
2319
|
-
}
|
|
2320
|
-
/**
|
|
2321
|
-
* Pick the delay before the next retry.
|
|
2322
|
-
* - If the server sent `retryAfter` (seconds), honor it (capped by
|
|
2323
|
-
* `maxRetryAfterMs`) — returns null if the server wait exceeds the
|
|
2324
|
-
* cap, signalling the caller should give up.
|
|
2325
|
-
* - Otherwise: exponential backoff with ±20% jitter, capped at
|
|
2326
|
-
* `maxDelayMs`.
|
|
2327
|
-
*/
|
|
2328
|
-
computeBackoff(attempt, retryAfter) {
|
|
2329
|
-
if (retryAfter !== void 0) {
|
|
2330
|
-
const serverMs = retryAfter * 1e3;
|
|
2331
|
-
if (serverMs > this.retry.maxRetryAfterMs) return null;
|
|
2332
|
-
return serverMs;
|
|
2333
|
-
}
|
|
2334
|
-
const exp = this.retry.initialDelayMs * 2 ** (attempt - 1);
|
|
2335
|
-
const capped = Math.min(exp, this.retry.maxDelayMs);
|
|
2336
|
-
const jitter = capped * (0.8 + Math.random() * 0.4);
|
|
2337
|
-
return Math.round(jitter);
|
|
2338
|
-
}
|
|
2339
|
-
sleep(ms) {
|
|
2340
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2341
|
-
}
|
|
2342
|
-
async post(path, body) {
|
|
2343
|
-
const controller = new AbortController();
|
|
2344
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2345
|
-
let response;
|
|
2346
|
-
try {
|
|
2347
|
-
response = await this.fetchImpl(`${this.url}${path}`, {
|
|
2348
|
-
method: "POST",
|
|
2349
|
-
headers: {
|
|
2350
|
-
"Content-Type": "application/json",
|
|
2351
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
2352
|
-
"X-Issuer-Id": this.issuerId
|
|
2353
|
-
},
|
|
2354
|
-
body: JSON.stringify(body, this.bigintReplacer),
|
|
2355
|
-
signal: controller.signal
|
|
2356
|
-
});
|
|
2357
|
-
} catch (err) {
|
|
2358
|
-
if (err.name === "AbortError") {
|
|
2359
|
-
throw new PafiBackendError(
|
|
2360
|
-
"TIMEOUT",
|
|
2361
|
-
`PAFI Backend request timed out after ${this.timeoutMs}ms`,
|
|
2362
|
-
0
|
|
2363
|
-
);
|
|
2364
|
-
}
|
|
2365
|
-
throw new PafiBackendError(
|
|
2366
|
-
"NETWORK_ERROR",
|
|
2367
|
-
`PAFI Backend unreachable: ${err.message}`,
|
|
2368
|
-
0
|
|
2369
|
-
);
|
|
2370
|
-
} finally {
|
|
2371
|
-
clearTimeout(timeoutId);
|
|
2372
|
-
}
|
|
2373
|
-
const text = await response.text();
|
|
2374
|
-
if (!response.ok) {
|
|
2375
|
-
let code = "INTERNAL_ERROR";
|
|
2376
|
-
let message = text || response.statusText;
|
|
2377
|
-
let details;
|
|
2378
|
-
let retryAfter;
|
|
2379
|
-
let serverSafeToRetry;
|
|
2380
|
-
try {
|
|
2381
|
-
const parsed = JSON.parse(text);
|
|
2382
|
-
code = parsed.code ?? code;
|
|
2383
|
-
message = parsed.message ?? message;
|
|
2384
|
-
details = parsed.details;
|
|
2385
|
-
if (typeof parsed.retryAfter === "number") retryAfter = parsed.retryAfter;
|
|
2386
|
-
if (typeof parsed.safeToRetry === "boolean") serverSafeToRetry = parsed.safeToRetry;
|
|
2387
|
-
} catch {
|
|
2388
|
-
}
|
|
2389
|
-
throw new PafiBackendError(code, message, response.status, details, {
|
|
2390
|
-
...retryAfter !== void 0 ? { retryAfter } : {},
|
|
2391
|
-
...serverSafeToRetry !== void 0 ? { safeToRetry: serverSafeToRetry } : {}
|
|
2392
|
-
});
|
|
2393
|
-
}
|
|
2394
|
-
return JSON.parse(text, this.bigintReviver);
|
|
2395
|
-
}
|
|
2396
|
-
/** JSON replacer that stringifies bigints. Paired with bigintReviver. */
|
|
2397
|
-
bigintReplacer = (_key, value) => {
|
|
2398
|
-
return typeof value === "bigint" ? value.toString() : value;
|
|
2399
|
-
};
|
|
2400
|
-
/**
|
|
2401
|
-
* JSON reviver that coerces specific numeric-string fields back to
|
|
2402
|
-
* bigint. The server must send these fields as decimal strings.
|
|
2403
|
-
*/
|
|
2404
|
-
bigintReviver = (key, value) => {
|
|
2405
|
-
if (typeof value === "string" && (key.endsWith("GasLimit") || key === "nonce" || key === "callGasLimit" || key === "verificationGasLimit" || key === "preVerificationGas" || key === "maxFeePerGas" || key === "maxPriorityFeePerGas" || key === "paymasterVerificationGasLimit" || key === "paymasterPostOpGasLimit") && /^\d+$/.test(value)) {
|
|
2406
|
-
return BigInt(value);
|
|
2407
|
-
}
|
|
2408
|
-
return value;
|
|
2409
|
-
};
|
|
2410
|
-
};
|
|
2411
|
-
|
|
2412
1831
|
// src/config.ts
|
|
2413
1832
|
import { getAddress as getAddress9 } from "viem";
|
|
2414
1833
|
function createIssuerService(config) {
|
|
2415
1834
|
if (!config.provider) {
|
|
2416
1835
|
throw new Error("createIssuerService: provider is required");
|
|
2417
1836
|
}
|
|
2418
|
-
if (!config.operatorWallet) {
|
|
2419
|
-
throw new Error("createIssuerService: operatorWallet is required");
|
|
2420
|
-
}
|
|
2421
|
-
if (!config.signer) {
|
|
2422
|
-
throw new Error("createIssuerService: signer is required");
|
|
2423
|
-
}
|
|
2424
|
-
if (!config.relayAddress) {
|
|
2425
|
-
throw new Error("createIssuerService: relayAddress is required");
|
|
2426
|
-
}
|
|
2427
1837
|
if (!config.auth?.jwtSecret) {
|
|
2428
1838
|
throw new Error("createIssuerService: auth.jwtSecret is required");
|
|
2429
1839
|
}
|
|
@@ -2450,18 +1860,7 @@ function createIssuerService(config) {
|
|
|
2450
1860
|
authServiceConfig.jwtExpiresIn = config.auth.jwtExpiresIn;
|
|
2451
1861
|
}
|
|
2452
1862
|
const authService = new AuthService(authServiceConfig);
|
|
2453
|
-
const
|
|
2454
|
-
relayAddress: config.relayAddress,
|
|
2455
|
-
operatorWallet: config.operatorWallet,
|
|
2456
|
-
provider: config.provider
|
|
2457
|
-
};
|
|
2458
|
-
if (config.relay?.simulateBeforeSubmit !== void 0) {
|
|
2459
|
-
relayServiceConfig.simulateBeforeSubmit = config.relay.simulateBeforeSubmit;
|
|
2460
|
-
}
|
|
2461
|
-
if (config.relay?.confirmationTimeoutMs !== void 0) {
|
|
2462
|
-
relayServiceConfig.confirmationTimeoutMs = config.relay.confirmationTimeoutMs;
|
|
2463
|
-
}
|
|
2464
|
-
const relayService = new RelayService(relayServiceConfig);
|
|
1863
|
+
const relayService = new RelayService();
|
|
2465
1864
|
let feeManager;
|
|
2466
1865
|
if (config.fee) {
|
|
2467
1866
|
feeManager = new FeeManager({
|
|
@@ -2469,16 +1868,6 @@ function createIssuerService(config) {
|
|
|
2469
1868
|
provider: config.provider
|
|
2470
1869
|
});
|
|
2471
1870
|
}
|
|
2472
|
-
const gatewayConfig = {
|
|
2473
|
-
ledger,
|
|
2474
|
-
policy,
|
|
2475
|
-
signer: config.signer,
|
|
2476
|
-
relayService
|
|
2477
|
-
};
|
|
2478
|
-
if (config.gateway?.defaultLockBufferMs !== void 0) {
|
|
2479
|
-
gatewayConfig.defaultLockBufferMs = config.gateway.defaultLockBufferMs;
|
|
2480
|
-
}
|
|
2481
|
-
const gateway = new MintingGateway(gatewayConfig);
|
|
2482
1871
|
const indexers = /* @__PURE__ */ new Map();
|
|
2483
1872
|
for (const tokenAddress of tokenAddresses) {
|
|
2484
1873
|
const indexerConfig = {
|
|
@@ -2506,7 +1895,6 @@ function createIssuerService(config) {
|
|
|
2506
1895
|
const firstIndexer = indexers.get(tokenAddresses[0]);
|
|
2507
1896
|
const handlersConfig = {
|
|
2508
1897
|
authService,
|
|
2509
|
-
gateway,
|
|
2510
1898
|
ledger,
|
|
2511
1899
|
provider: config.provider,
|
|
2512
1900
|
pointTokenAddresses: tokenAddresses,
|
|
@@ -2526,10 +1914,8 @@ function createIssuerService(config) {
|
|
|
2526
1914
|
sessionStore,
|
|
2527
1915
|
ledger,
|
|
2528
1916
|
policy,
|
|
2529
|
-
signer: config.signer,
|
|
2530
1917
|
relayService,
|
|
2531
1918
|
feeManager,
|
|
2532
|
-
gateway,
|
|
2533
1919
|
indexers,
|
|
2534
1920
|
indexer: firstIndexer,
|
|
2535
1921
|
handlers
|
|
@@ -2549,13 +1935,10 @@ export {
|
|
|
2549
1935
|
IssuerApiHandlers,
|
|
2550
1936
|
MemoryPointLedger,
|
|
2551
1937
|
MemorySessionStore,
|
|
2552
|
-
MintingGateway,
|
|
2553
|
-
MintingGatewayError,
|
|
2554
1938
|
NonceManager,
|
|
2555
1939
|
PAFI_ISSUER_SDK_VERSION,
|
|
2556
1940
|
PTRedeemError,
|
|
2557
1941
|
PTRedeemHandler,
|
|
2558
|
-
PafiBackendClient,
|
|
2559
1942
|
PafiBackendError,
|
|
2560
1943
|
PointIndexer,
|
|
2561
1944
|
PrivateKeySigner,
|
|
@@ -2566,7 +1949,6 @@ export {
|
|
|
2566
1949
|
authenticateRequest,
|
|
2567
1950
|
createIssuerService,
|
|
2568
1951
|
createSubgraphNativeUsdtQuoter,
|
|
2569
|
-
createSubgraphPoolsProvider
|
|
2570
|
-
encodeExtData
|
|
1952
|
+
createSubgraphPoolsProvider
|
|
2571
1953
|
};
|
|
2572
1954
|
//# sourceMappingURL=index.js.map
|